diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e09dff2a6d5..53dd667eb4d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -3655,11 +3655,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) { @@ -4998,6 +4998,83 @@ 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) { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(tsconfig.path); + assert.isDefined(project); + + checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); + checkWatchedFiles(host, [libFile.path, tsconfig.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [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); + }); + + it("tsconfig for the file does not exist", () => { + const { host, projectService } = openClientFile(files); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + }); + }); }); describe("tsserverProjectSystem cancellationToken", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c525d5a3bc8..607c3c17046 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1156,7 +1156,7 @@ namespace ts.server { * This is called by inferred project whenever script info is added as a root */ /* @internal */ - startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { + startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); @@ -1178,7 +1178,7 @@ namespace ts.server { !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo); } - }, projectRootPath); + }); } /** @@ -1209,12 +1209,14 @@ namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private forEachConfigFileLocation(info: ScriptInfo, - action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void, - projectRootPath?: NormalizedPath) { - let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); + private forEachConfigFileLocation(info: ScriptInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) { + const projectRootPath = this.openFiles.get(info.path); + let searchPath = asNormalizedPath(this.getNormalizedAbsolutePath(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")); @@ -1233,7 +1235,7 @@ namespace ts.server { break; } searchPath = parentPath; - } + } while (anySearchPathOk || isSearchPathInProjectRoot()); return undefined; } @@ -1246,13 +1248,12 @@ namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private getConfigFileNameForFile(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { + private getConfigFileNameForFile(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.configFileExists(configFileName, canonicalConfigFilePath, info), - projectRootPath + this.configFileExists(configFileName, canonicalConfigFilePath, info) ); if (configFileName) { this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`); @@ -1906,7 +1907,7 @@ namespace ts.server { // we first detect if there is already a configured project created for it: if so, // we re- read the tsconfig file content and update the project only if we havent already done so // otherwise we create a new one. - const configFileName = this.getConfigFileNameForFile(info, this.openFiles.get(path)); + const configFileName = this.getConfigFileNameForFile(info); if (configFileName) { const project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { @@ -2012,9 +2013,10 @@ namespace ts.server { let configFileErrors: ReadonlyArray; const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent); + this.openFiles.set(info.path, projectRootPath); let project: ConfiguredProject | ExternalProject = this.findExternalProjetContainingOpenScriptInfo(info); if (!project) { - configFileName = this.getConfigFileNameForFile(info, projectRootPath); + configFileName = this.getConfigFileNameForFile(info); if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { @@ -2047,7 +2049,6 @@ namespace ts.server { } Debug.assert(!info.isOrphan()); - this.openFiles.set(info.path, projectRootPath); // Remove the configured projects that have zero references from open files. // This was postponed from closeOpenFile to after opening next file, diff --git a/src/server/project.ts b/src/server/project.ts index dc949b4b221..d884c777f79 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1200,7 +1200,7 @@ namespace ts.server { addRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); - this.projectService.startWatchingConfigFilesForInferredProjectRoot(info, this.projectService.openFiles.get(info.path)); + this.projectService.startWatchingConfigFilesForInferredProjectRoot(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 86a33c179a3..97f20a2ac15 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7891,7 +7891,7 @@ declare namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private forEachConfigFileLocation(info, action, projectRootPath?); + private forEachConfigFileLocation(info, action); /** * This function tries to search for a tsconfig.json for the given file. * This is different from the method the compiler uses because @@ -7900,7 +7900,7 @@ declare namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private getConfigFileNameForFile(info, projectRootPath); + private getConfigFileNameForFile(info); private printProjects(); private findConfiguredProjectByProjectName(configFileName); private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);