diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e960a8cbb88..5f04b643a35 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -4074,11 +4074,11 @@ namespace ts.projectSystem { // Open file1 -> configFile verifyConfigFileName(file1, "/a", configFile); verifyConfigFileName(file1, "/a/b", configFile); - verifyConfigFileName(file1, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); + verifyConfigFileName(file1, "/a/B", configFile); // Open file2 use root "/a/b" verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); - verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? undefined : configFile); + verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); function verifyConfigFileName(file: FileOrFolder, projectRoot: string, expectedConfigFile: FileOrFolder | undefined) { @@ -5607,6 +5607,88 @@ namespace ts.projectSystem { checkWatchedDirectories(host, [], /*recursive*/ false); checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); }); + + describe("when the opened file is not from project root", () => { + const projectRoot = "/a/b/projects/project"; + const file: FileOrFolder = { + path: `${projectRoot}/src/index.ts`, + content: "let y = 10" + }; + const tsconfig: FileOrFolder = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [file, libFile]; + const filesWithConfig = files.concat(tsconfig); + const dirOfFile = getDirectoryPath(file.path); + + function openClientFile(files: FileOrFolder[]) { + const host = createServerHost(files); + const projectService = createProjectService(host); + + projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); + return { host, projectService }; + } + + function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) { + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); + const project = projectService.configuredProjects.get(tsconfig.path); + assert.isDefined(project); + + if (orphanInferredProject) { + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.isOrphan()); + } + + checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); + checkWatchedFiles(host, [libFile.path, tsconfig.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); + } + + function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) { + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + assert.isDefined(project); + + const filesToWatch = [libFile.path]; + forEachAncestorDirectory(dirOfFile, ancestor => { + filesToWatch.push(combinePaths(ancestor, "tsconfig.json")); + filesToWatch.push(combinePaths(ancestor, "jsconfig.json")); + }); + + checkProjectActualFiles(project, [file.path, libFile.path]); + checkWatchedFiles(host, filesToWatch); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); + } + + it("tsconfig for the file exists", () => { + const { host, projectService } = openClientFile(filesWithConfig); + verifyConfiguredProject(host, projectService); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + }); + + it("tsconfig for the file does not exist", () => { + const { host, projectService } = openClientFile(files); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + }); + }); }); describe("tsserverProjectSystem cancellationToken", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2f53faae2cf..4a91b287986 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1197,8 +1197,11 @@ namespace ts.server { const projectRootPath = this.openFiles.get(info.path); let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); + const isSearchPathInProjectRoot = () => containsPath(projectRootPath, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); - while (!projectRootPath || containsPath(projectRootPath, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames)) { + // If projectRootPath doesnt contain info.path, then do normal search for config file + const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot(); + do { const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName); const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json")); @@ -1217,7 +1220,7 @@ namespace ts.server { break; } searchPath = parentPath; - } + } while (anySearchPathOk || isSearchPathInProjectRoot()); return undefined; } @@ -1235,7 +1238,7 @@ namespace ts.server { this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.configFileExists(configFileName, canonicalConfigFilePath, info), + this.configFileExists(configFileName, canonicalConfigFilePath, info) ); if (configFileName) { this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`);