Script infos while opening/closing shouldnt mark project as dirty if the contents dont change

This commit is contained in:
Sheetal Nandi
2017-08-25 20:20:14 -07:00
parent 17565d8407
commit 10ea5bf460
6 changed files with 168 additions and 134 deletions

View File

@@ -61,7 +61,7 @@ namespace ts {
typingsInstaller: undefined
};
const projectService = new server.ProjectService(svcOpts);
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, serverHost);
const rootScriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(server.toNormalizedPath(rootFile), /*openedByClient*/ true);
const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo);
project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } );

View File

@@ -21,7 +21,7 @@ namespace ts.textStorage {
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path));
const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path));
ts1.useScriptVersionCache();
ts1.useScriptVersionCache_TestOnly();
ts2.useText();
const lineMap = computeLineStarts(f.content);
@@ -55,16 +55,16 @@ namespace ts.textStorage {
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path));
ts1.getSnapshot();
assert.isTrue(!ts1.hasScriptVersionCache(), "should not have script version cache - 1");
assert.isTrue(!ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 1");
ts1.edit(0, 5, " ");
assert.isTrue(ts1.hasScriptVersionCache(), "have script version cache - 1");
assert.isTrue(ts1.hasScriptVersionCache_TestOnly(), "have script version cache - 1");
ts1.useText();
assert.isTrue(!ts1.hasScriptVersionCache(), "should not have script version cache - 2");
assert.isTrue(!ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 2");
ts1.getLineInfo(0);
assert.isTrue(ts1.hasScriptVersionCache(), "have script version cache - 2");
assert.isTrue(ts1.hasScriptVersionCache_TestOnly(), "have script version cache - 2");
});
});
}
}

View File

@@ -687,7 +687,7 @@ namespace ts.server {
else {
// file has been changed which might affect the set of referenced files in projects that include
// this file and set of inferred projects
info.reloadFromFile();
info.delayReloadNonMixedContentFile();
this.delayUpdateProjectGraphs(info.containingProjects);
}
}
@@ -753,7 +753,7 @@ namespace ts.server {
const configFileSpecs = project.configFileSpecs;
const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilationSettings(), project.getCachedPartialSystem(), this.hostConfiguration.extraFileExtensions);
project.updateErrorOnNoInputFiles(result.fileNames.length !== 0);
this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined);
this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader);
this.delayUpdateProjectGraphAndInferredProjectsRefresh(project);
}
},
@@ -1352,7 +1352,7 @@ namespace ts.server {
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader),
options.compileOnSave === undefined ? true : options.compileOnSave);
this.addFilesToNonInferredProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition);
this.addFilesToNonInferredProjectAndUpdateGraph(project, files, externalFilePropertyReader, typeAcquisition);
this.externalProjects.push(project);
this.sendProjectTelemetry(project.externalProjectName, project);
return project;
@@ -1401,14 +1401,14 @@ namespace ts.server {
}
}
private addFilesToNonInferredProjectAndUpdateGraph<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, clientFileName: string, typeAcquisition: TypeAcquisition): void {
this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName);
private addFilesToNonInferredProjectAndUpdateGraph<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, typeAcquisition: TypeAcquisition): void {
this.updateNonInferredProjectFiles(project, files, propertyReader);
project.setTypeAcquisition(typeAcquisition);
// This doesnt need scheduling since its either creation or reload of the project
project.updateGraph();
}
private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) {
private createConfiguredProject(configFileName: NormalizedPath) {
const cachedPartialSystem = createCachedPartialSystem(this.host);
const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedPartialSystem);
this.logger.info(`Opened configuration file ${configFileName}`);
@@ -1438,14 +1438,14 @@ namespace ts.server {
}
project.setProjectErrors(configFileErrors);
this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition);
this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, projectOptions.typeAcquisition);
this.configuredProjects.set(project.canonicalConfigFilePath, project);
this.setConfigFileExistenceByNewConfiguredProject(project);
this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions);
return project;
}
private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject, files: T[], propertyReader: FilePropertyReader<T>, clientFileName?: string) {
private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject, files: T[], propertyReader: FilePropertyReader<T>) {
const projectRootFilesMap = project.getRootFilesMap();
const newRootScriptInfoMap = createMap<ProjectRoot>();
@@ -1467,7 +1467,7 @@ namespace ts.server {
else {
const scriptKind = propertyReader.getScriptKind(f);
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.partialSystem);
scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.partialSystem);
path = scriptInfo.path;
// If this script info is not already a root add it
if (!project.isRoot(scriptInfo)) {
@@ -1509,7 +1509,7 @@ namespace ts.server {
if (compileOnSave !== undefined) {
project.compileOnSaveEnabled = compileOnSave;
}
this.addFilesToNonInferredProjectAndUpdateGraph(project, newUncheckedFiles, propertyReader, /*clientFileName*/ undefined, newTypeAcquisition);
this.addFilesToNonInferredProjectAndUpdateGraph(project, newUncheckedFiles, propertyReader, newTypeAcquisition);
}
/**
@@ -1617,15 +1617,11 @@ namespace ts.server {
return project;
}
/**
* @param uncheckedFileName is absolute pathname
* @param fileContent is a known version of the file content that is more up to date than the one on disk
*/
/*@internal*/
getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: PartialSystem) {
return this.getOrCreateScriptInfoForNormalizedPath(
toNormalizedPath(uncheckedFileName), openedByClient, /*fileContent*/ undefined,
/*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn
getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, hostToQueryFileExistsOn: PartialSystem) {
return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
toNormalizedPath(uncheckedFileName), /*scriptKind*/ undefined,
/*hasMixedContent*/ undefined, hostToQueryFileExistsOn
);
}
@@ -1654,38 +1650,41 @@ namespace ts.server {
}
}
getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) {
return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
}
getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) {
return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
}
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) {
Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content");
const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
let info = this.getScriptInfoForPath(path);
if (!info) {
if (openedByClient || (hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path);
this.filenameToScriptInfo.set(info.path, info);
if (openedByClient) {
if (fileContent === undefined) {
// if file is opened by client and its content is not specified - use file text
fileContent = this.host.readFile(fileName) || "";
}
}
else {
this.watchClosedScriptInfo(info);
}
// If the file is not opened by client and the file doesnot exist on the disk, return
if (!openedByClient && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
return;
}
info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path);
this.filenameToScriptInfo.set(info.path, info);
if (!openedByClient) {
this.watchClosedScriptInfo(info);
}
}
if (info) {
if (openedByClient && !info.isScriptOpen()) {
this.stopWatchingScriptInfo(info);
info.open(fileContent);
if (hasMixedContent) {
info.registerFileUpdate();
}
}
else if (fileContent !== undefined) {
info.reload(fileContent);
if (openedByClient && !info.isScriptOpen()) {
// Opening closed script info
// either it was created just now, or was part of projects but was closed
this.stopWatchingScriptInfo(info);
info.open(fileContent);
if (hasMixedContent) {
info.registerFileUpdate();
}
}
else {
Debug.assert(fileContent === undefined);
}
return info;
}
@@ -1730,9 +1729,16 @@ namespace ts.server {
/**
* This function rebuilds the project for every file opened by the client
* This does not reload contents of open files from disk. But we could do that if needed
*/
reloadProjects() {
this.logger.info("reload projects.");
// If we want this to also reload open files from disk, we could do that,
// but then we need to make sure we arent calling this function
// (and would separate out below reloading of projects to be called when immediate reload is needed)
// as there is no need to load contents of the files from the disk
// Reload Projects
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false);
this.refreshInferredProjects();
}
@@ -1771,7 +1777,7 @@ namespace ts.server {
if (configFileName) {
const project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
this.createConfiguredProject(configFileName, info.fileName);
this.createConfiguredProject(configFileName);
updatedProjects.set(configFileName, true);
}
else if (!updatedProjects.has(configFileName)) {
@@ -1862,14 +1868,14 @@ namespace ts.server {
let configFileName: NormalizedPath;
let configFileErrors: ReadonlyArray<Diagnostic>;
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, fileContent, scriptKind, hasMixedContent);
let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName);
if (!project) {
configFileName = this.getConfigFileNameForFile(info, projectRootPath);
if (configFileName) {
project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
project = this.createConfiguredProject(configFileName, fileName);
project = this.createConfiguredProject(configFileName);
// even if opening config file was successful, it could still
// contain errors that were tolerated.

View File

@@ -263,7 +263,7 @@ namespace ts.server {
}
private getScriptInfoLSHost(fileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.partialSystem);
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.partialSystem);
if (scriptInfo) {
const existingValue = this.rootFilesMap.get(scriptInfo.path);
if (existingValue !== undefined && existingValue !== scriptInfo) {
@@ -804,7 +804,7 @@ namespace ts.server {
// by the LSHost for files in the program when the program is retrieved above but
// the program doesn't contain external files so this must be done explicitly.
inserted => {
const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.partialSystem);
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.partialSystem);
scriptInfo.attachToProject(this);
},
removed => {
@@ -845,9 +845,8 @@ namespace ts.server {
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(
fileName, /*openedByClient*/ false, /*fileContent*/ undefined,
/*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.partialSystem
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
fileName, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.partialSystem
);
if (scriptInfo && !scriptInfo.isAttached(this)) {
return Errors.ThrowProjectDoesNotContainDocument(fileName, this);

View File

@@ -4,13 +4,19 @@ namespace ts.server {
/* @internal */
export class TextStorage {
// Generated only on demand and removes the text if any edits
private svc: ScriptVersionCache | undefined;
private svcVersion = 0;
// Store text when there is no svc or svc has no change, on edit, remove the text
private text: string;
private lineMap: number[];
private textVersion = 0;
public isOpen: boolean;
private ownFileText: boolean;
private pendingReloadFromDisk: boolean;
constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath) {
}
@@ -20,43 +26,75 @@ namespace ts.server {
: `Text-${this.textVersion}`;
}
public hasScriptVersionCache() {
public hasScriptVersionCache_TestOnly() {
return this.svc !== undefined;
}
public useScriptVersionCache(newText?: string) {
this.switchToScriptVersionCache(newText);
public useScriptVersionCache_TestOnly() {
this.switchToScriptVersionCache();
}
public useText(newText?: string) {
this.svc = undefined;
this.setText(newText);
this.text = newText;
this.lineMap = undefined;
this.textVersion++;
}
public edit(start: number, end: number, newText: string) {
this.switchToScriptVersionCache().edit(start, end - start, newText);
this.ownFileText = false;
this.text = undefined;
this.lineMap = undefined;
}
public reload(text: string) {
if (this.svc) {
this.svc.reload(text);
}
else {
this.setText(text);
/** returns true if text changed */
public reload(newText: string) {
Debug.assert(newText !== undefined);
// Reload always has fresh content
this.pendingReloadFromDisk = false;
// If text changed set the text
// This also ensures that if we had switched to version cache,
// we are switching back to text.
// The change to version cache will happen when needed
// Thus avoiding the computation if there are no changes
if (this.text !== newText) {
this.useText(newText);
// We cant guarantee new text is own file text
this.ownFileText = false;
return true;
}
}
public reloadFromFile(tempFileName?: string) {
if (this.svc || (tempFileName !== this.fileName)) {
this.reload(this.getFileText(tempFileName));
/** returns true if text changed */
public reloadFromDisk() {
let reloaded = false;
if (!this.pendingReloadFromDisk && !this.ownFileText) {
reloaded = this.reload(this.getFileText());
this.ownFileText = true;
}
else {
this.setText(undefined);
return reloaded;
}
public delayReloadFromFileIntoText() {
this.pendingReloadFromDisk = true;
}
/** returns true if text changed */
public reloadFromFile(tempFileName: string) {
let reloaded = false;
// Reload if different file or we dont know if we are working with own file text
if (tempFileName !== this.fileName || !this.ownFileText) {
reloaded = this.reload(this.getFileText(tempFileName));
this.ownFileText = !tempFileName || tempFileName === this.fileName;
}
return reloaded;
}
public getSnapshot(): IScriptSnapshot {
return this.svc
return this.useScriptVersionCacheIfValidOrOpen()
? this.svc.getSnapshot()
: ScriptSnapshot.fromString(this.getOrLoadText());
}
@@ -68,7 +106,7 @@ namespace ts.server {
* @param line 0 based index
*/
lineToTextSpan(line: number): TextSpan {
if (!this.svc) {
if (!this.useScriptVersionCacheIfValidOrOpen()) {
const lineMap = this.getLineMap();
const start = lineMap[line]; // -1 since line is 1-based
const end = line + 1 < lineMap.length ? lineMap[line + 1] : this.text.length;
@@ -82,7 +120,7 @@ namespace ts.server {
* @param offset 1 based index
*/
lineOffsetToPosition(line: number, offset: number): number {
if (!this.svc) {
if (!this.useScriptVersionCacheIfValidOrOpen()) {
return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text);
}
@@ -91,7 +129,7 @@ namespace ts.server {
}
positionToLineOffset(position: number): protocol.Location {
if (!this.svc) {
if (!this.useScriptVersionCacheIfValidOrOpen()) {
const { line, character } = computeLineAndCharacterOfPosition(this.getLineMap(), position);
return { line: line + 1, offset: character + 1 };
}
@@ -102,43 +140,39 @@ namespace ts.server {
return this.host.readFile(tempFileName || this.fileName) || "";
}
private ensureNoScriptVersionCache() {
Debug.assert(!this.svc, "ScriptVersionCache should not be set");
}
private switchToScriptVersionCache(newText?: string): ScriptVersionCache {
if (!this.svc) {
this.svc = ScriptVersionCache.fromString(newText !== undefined ? newText : this.getOrLoadText());
private switchToScriptVersionCache(): ScriptVersionCache {
if (!this.svc || this.pendingReloadFromDisk) {
this.svc = ScriptVersionCache.fromString(this.getOrLoadText());
this.svcVersion++;
this.text = undefined;
}
return this.svc;
}
private useScriptVersionCacheIfValidOrOpen(): ScriptVersionCache | undefined {
// If this is open script, use the cache
if (this.isOpen) {
return this.switchToScriptVersionCache();
}
// Else if the svc is uptodate with the text, we are good
return !this.pendingReloadFromDisk && this.svc;
}
private getOrLoadText() {
this.ensureNoScriptVersionCache();
if (this.text === undefined) {
this.setText(this.getFileText());
if (this.text === undefined || this.pendingReloadFromDisk) {
Debug.assert(!this.svc || this.pendingReloadFromDisk, "ScriptVersionCache should not be set when reloading from disk");
this.reload(this.getFileText());
this.ownFileText = true;
}
return this.text;
}
private getLineMap() {
this.ensureNoScriptVersionCache();
Debug.assert(!this.svc, "ScriptVersionCache should not be set");
return this.lineMap || (this.lineMap = computeLineStarts(this.getOrLoadText()));
}
private setText(newText: string) {
this.ensureNoScriptVersionCache();
if (newText === undefined || this.text !== newText) {
this.text = newText;
this.lineMap = undefined;
this.textVersion++;
}
}
}
export class ScriptInfo {
/**
* All projects that include this file
@@ -150,8 +184,6 @@ namespace ts.server {
fileWatcher: FileWatcher;
private textStorage: TextStorage;
private isOpen: boolean;
constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
@@ -169,19 +201,28 @@ namespace ts.server {
}
public isScriptOpen() {
return this.isOpen;
return this.textStorage.isOpen;
}
public open(newText: string) {
this.isOpen = true;
this.textStorage.useScriptVersionCache(newText);
this.markContainingProjectsAsDirty();
this.textStorage.isOpen = true;
if (newText !== undefined &&
this.textStorage.reload(newText)) {
// reload new contents only if the existing contents changed
this.markContainingProjectsAsDirty();
}
}
public close() {
this.isOpen = false;
this.textStorage.useText(this.hasMixedContent ? "" : undefined);
this.markContainingProjectsAsDirty();
this.textStorage.isOpen = false;
if (this.hasMixedContent) {
if (this.textStorage.reload("")) {
this.markContainingProjectsAsDirty();
}
}
else if (this.textStorage.reloadFromDisk()) {
this.markContainingProjectsAsDirty();
}
}
public getSnapshot() {
@@ -293,26 +334,31 @@ namespace ts.server {
return this.textStorage.getVersion();
}
reload(script: string) {
this.textStorage.reload(script);
this.markContainingProjectsAsDirty();
}
saveTo(fileName: string) {
const snap = this.textStorage.getSnapshot();
this.host.writeFile(fileName, snap.getText(0, snap.getLength()));
}
/*@internal*/
delayReloadNonMixedContentFile() {
Debug.assert(!this.hasMixedContent);
this.textStorage.delayReloadFromFileIntoText();
this.markContainingProjectsAsDirty();
}
reloadFromFile(tempFileName?: NormalizedPath) {
if (this.hasMixedContent) {
this.reload("");
this.textStorage.reload("");
this.markContainingProjectsAsDirty();
}
else {
this.textStorage.reloadFromFile(tempFileName);
this.markContainingProjectsAsDirty();
if (this.textStorage.reloadFromFile(tempFileName)) {
this.markContainingProjectsAsDirty();
}
}
}
/*@internal*/
getLineInfo(line: number): AbsolutePositionAndLineText {
return this.textStorage.getLineInfo(line);
}

View File

@@ -2,6 +2,7 @@
/// <reference path="..\services\services.ts" />
/// <reference path="session.ts" />
/*@internal*/
namespace ts.server {
const lineCollectionCapacity = 4;
@@ -285,24 +286,6 @@ namespace ts.server {
}
}
// reload whole script, leaving no change history behind reload
reload(script: string) {
this.currentVersion++;
this.changes = []; // history wiped out by reload
const snap = new LineIndexSnapshot(this.currentVersion, this, new LineIndex());
// delete all versions
for (let i = 0; i < this.versions.length; i++) {
this.versions[i] = undefined;
}
this.versions[this.currentVersionToIndex()] = snap;
const lm = LineIndex.linesFromText(script);
snap.index.load(lm.lines);
this.minVersion = this.currentVersion;
}
getSnapshot(): IScriptSnapshot { return this._getSnapshot(); }
private _getSnapshot(): LineIndexSnapshot {
@@ -843,4 +826,4 @@ namespace ts.server {
return 1;
}
}
}
}