diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f7d220b789461..00f9f4a73e29b 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1141,7 +1141,7 @@ namespace ts.server { const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) || this.getOrCreateSingleInferredProjectIfEnabled() || - this.getOrCreateSingleInferredWithoutProjectRoot(info.isDynamic ? this.currentDirectory : getDirectoryPath(info.path)); + this.getOrCreateSingleInferredWithoutProjectRoot(info.isDynamic ? projectRootPath || this.currentDirectory : getDirectoryPath(info.path)); project.addRoot(info); if (info.containingProjects[0] !== project) { @@ -1503,6 +1503,8 @@ namespace ts.server { Debug.assert(!isOpenScriptInfo(info) || this.openFiles.has(info.path)); const projectRootPath = this.openFiles.get(info.path); + const scriptInfo = Debug.assertDefined(this.getScriptInfo(info.path)); + if (scriptInfo.isDynamic) return undefined; let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); @@ -1943,7 +1945,9 @@ namespace ts.server { } private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined { - if (info.isDynamic || !this.useInferredProjectPerProjectRoot) { + if (!this.useInferredProjectPerProjectRoot || + // Its a dynamic info opened without project root + (info.isDynamic && projectRootPath === undefined)) { return undefined; } @@ -2228,7 +2232,7 @@ namespace ts.server { const isDynamic = isDynamicFileName(fileName); Debug.assert(isRootedDiskPath(fileName) || isDynamic || openedByClient, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nScript info with non-dynamic relative file name can only be open script info or in context of host currentDirectory`); Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nOpen script files with non rooted disk path opened with current directory context cannot have same canonical names`); - Debug.assert(!isDynamic || this.currentDirectory === currentDirectory, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always have current directory context since containing external project name will always match the script info name.`); + Debug.assert(!isDynamic || this.currentDirectory === currentDirectory || this.useInferredProjectPerProjectRoot, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`); // 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)) { return; @@ -2239,7 +2243,7 @@ namespace ts.server { if (!openedByClient) { this.watchClosedScriptInfo(info); } - else if (!isRootedDiskPath(fileName) && !isDynamic) { + else if (!isRootedDiskPath(fileName) && (!isDynamic || this.currentDirectory !== currentDirectory)) { // File that is opened by user but isn't rooted disk path this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info); } diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index 56bbe989bfc7d..bd724bd77e3d8 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -1131,6 +1131,62 @@ var x = 10;` checkProjectActualFiles(project, [file.path, libFile.path]); }); + describe("dynamic file with projectRootPath", () => { + const file: File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const configProjectFile: File = { + path: `${projectRoot}/a.ts`, + content: "let y = 10;" + }; + it("with useInferredProjectPerProjectRoot", () => { + const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const session = createSession(host, { useInferredProjectPerProjectRoot: true }); + openFilesForSession([{ file: file.path, projectRootPath: projectRoot }], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); + + session.executeCommandSeq({ + command: protocol.CommandTypes.GetOutliningSpans, + arguments: { + file: file.path + } + }); + + // Without project root + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, libFile.path]); + }); + + it("fails when useInferredProjectPerProjectRoot is false", () => { + const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + try { + projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, projectRoot); + } + catch (e) { + assert.strictEqual( + e.message.replace(/\r?\n/, "\n"), + `Debug Failure. False expression: \nVerbose Debug Information: {"fileName":"^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js","currentDirectory":"/user/username/projects/myproject","hostCurrentDirectory":"/","openKeys":[]}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.` + ); + } + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, libFile.path]); + }); + }); + it("files opened, closed affecting multiple projects", () => { const file: File = { path: "/a/b/projects/config/file.ts",