From 2cb14e5e8efcb6cce0b130ccfe327f40bdaf48c0 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 3 Nov 2017 15:28:28 -0700 Subject: [PATCH] Handle the script infos that are opened with non rooted disk path Fixes #19588 --- .../unittests/tsserverProjectSystem.ts | 3 +- src/server/editorServices.ts | 47 ++++++++++++++----- src/server/project.ts | 10 ++-- src/server/scriptInfo.ts | 4 +- .../reference/api/tsserverlibrary.d.ts | 10 +++- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index c6257cc0243..57ad8271dc0 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -2837,8 +2837,9 @@ namespace ts.projectSystem { // Run the last one = get error request host.runQueuedTimeoutCallbacks(newTimeoutId); - host.checkTimeoutQueueLength(2); + assert.isFalse(hasError); + host.checkTimeoutQueueLength(2); checkErrorMessage(host, "syntaxDiag", { file: untitledFile, diagnostics: [] }); host.clearOutput(); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f749c27cbbc..8a9d171b9f6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -355,6 +355,10 @@ namespace ts.server { * list of open files */ readonly openFiles: ScriptInfo[] = []; + /** + * Map of open files that are opened without complete path but have projectRoot as current directory + */ + private readonly openFilesWithNonRootedDiskPath = createMap(); private compilerOptionsForInferredProjects: CompilerOptions; private compilerOptionsForInferredProjectsPerProjectRoot = createMap(); @@ -932,12 +936,15 @@ namespace ts.server { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. - info.close(); + const fileExists = this.host.fileExists(info.fileName); + info.close(fileExists); this.stopWatchingConfigFilesForClosedScriptInfo(info); unorderedRemoveItem(this.openFiles, info); - - const fileExists = this.host.fileExists(info.fileName); + const canonicalFileName = this.toCanonicalFileName(info.fileName); + if (this.openFilesWithNonRootedDiskPath.get(canonicalFileName) === info) { + this.openFilesWithNonRootedDiskPath.delete(canonicalFileName); + } // collect all projects that should be removed let projectsToRemove: Project[]; @@ -1518,7 +1525,7 @@ namespace ts.server { else { const scriptKind = propertyReader.getScriptKind(f, this.hostConfiguration.extraFileExtensions); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.directoryStructureHost); + scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, project.currentDirectory, scriptKind, hasMixedContent, project.directoryStructureHost); path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { @@ -1672,9 +1679,9 @@ namespace ts.server { } /*@internal*/ - getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, hostToQueryFileExistsOn: DirectoryStructureHost) { + getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, currentDirectory: string, hostToQueryFileExistsOn: DirectoryStructureHost) { return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( - toNormalizedPath(uncheckedFileName), /*scriptKind*/ undefined, + toNormalizedPath(uncheckedFileName), currentDirectory, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn ); } @@ -1705,20 +1712,26 @@ namespace ts.server { } /*@internal*/ - getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { - return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); + getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) { + return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); } /*@internal*/ - getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { - return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); + getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) { + return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); } getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { + return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); + } + + private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { 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); + const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); if (!info) { + Debug.assert(isRootedDiskPath(fileName) || openedByClient, "Script info with relative file name can only be open script info"); + Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "Open script files with non rooted disk path opened with current directory context cannot have same canonical names"); const isDynamic = isDynamicFileName(fileName); // If the file is not opened by client and the file doesnot exist on the disk, return if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) { @@ -1729,6 +1742,10 @@ namespace ts.server { if (!openedByClient) { this.watchClosedScriptInfo(info); } + else if (!isRootedDiskPath(fileName) && currentDirectory !== this.currentDirectory) { + // File that is opened by user but isn't rooted disk path + this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info); + } } if (openedByClient && !info.isScriptOpen()) { // Opening closed script info @@ -1745,8 +1762,12 @@ namespace ts.server { return info; } + /** + * This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred + */ getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - return this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName)); + return !isRootedDiskPath(fileName) && this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)) || + this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName)); } getScriptInfoForPath(fileName: Path) { @@ -1926,7 +1947,7 @@ namespace ts.server { let sendConfigFileDiagEvent = false; let configFileErrors: ReadonlyArray; - const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, fileContent, scriptKind, hasMixedContent); + const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent); let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName); if (!project) { configFileName = this.getConfigFileNameForFile(info, projectRootPath); diff --git a/src/server/project.ts b/src/server/project.ts index a42926e64ea..c6632ca22f1 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -287,7 +287,7 @@ namespace ts.server { } private getOrCreateScriptInfoAndAttachToProject(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.directoryStructureHost); + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost); if (scriptInfo) { const existingValue = this.rootFilesMap.get(scriptInfo.path); if (existingValue !== scriptInfo && existingValue !== undefined) { @@ -367,7 +367,7 @@ namespace ts.server { /*@internal*/ toPath(fileName: string) { - return this.projectService.toPath(fileName); + return toPath(fileName, this.currentDirectory, this.projectService.toCanonicalFileName); } /*@internal*/ @@ -660,7 +660,7 @@ namespace ts.server { } containsFile(filename: NormalizedPath, requireOpen?: boolean) { - const info = this.projectService.getScriptInfoForNormalizedPath(filename); + const info = this.projectService.getScriptInfoForPath(this.toPath(filename)); if (info && (info.isScriptOpen() || !requireOpen)) { return this.containsScriptInfo(info); } @@ -857,7 +857,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.getOrCreateScriptInfoNotOpenedByClient(inserted, this.directoryStructureHost); + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost); scriptInfo.attachToProject(this); }, removed => this.detachScriptInfoFromProject(removed) @@ -903,7 +903,7 @@ namespace ts.server { } getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(fileName); + const scriptInfo = this.projectService.getScriptInfoForPath(this.toPath(fileName)); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 9f029c00f0a..f800a1117d0 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -248,9 +248,9 @@ namespace ts.server { } } - public close() { + public close(fileExists = true) { this.textStorage.isOpen = false; - if (this.isDynamicOrHasMixedContent()) { + if (this.isDynamicOrHasMixedContent() || !fileExists) { if (this.textStorage.reload("")) { this.markContainingProjectsAsDirty(); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ce37e446884..40e518afb8d 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7031,7 +7031,7 @@ declare namespace ts.server { constructor(host: ServerHost, fileName: NormalizedPath, scriptKind: ScriptKind, hasMixedContent: boolean, path: Path); isScriptOpen(): boolean; open(newText: string): void; - close(): void; + close(fileExists?: boolean): void; getSnapshot(): IScriptSnapshot; getFormatCodeSettings(): FormatCodeSettings; attachToProject(project: Project): boolean; @@ -7459,6 +7459,10 @@ declare namespace ts.server { * list of open files */ readonly openFiles: ScriptInfo[]; + /** + * Map of open files that are opened without complete path but have projectRoot as current directory + */ + private readonly openFilesWithNonRootedDiskPath; private compilerOptionsForInferredProjects; private compilerOptionsForInferredProjectsPerProjectRoot; /** @@ -7598,6 +7602,10 @@ declare namespace ts.server { private watchClosedScriptInfo(info); private stopWatchingScriptInfo(info); getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost): ScriptInfo; + private getOrCreateScriptInfoWorker(fileName, currentDirectory, openedByClient, fileContent?, scriptKind?, hasMixedContent?, hostToQueryFileExistsOn?); + /** + * This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred + */ getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo; getScriptInfoForPath(fileName: Path): ScriptInfo; setHostConfiguration(args: protocol.ConfigureRequestArguments): void;