diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 158db9ec84f..f07ec178381 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -356,7 +356,7 @@ namespace ts.projectSystem { } function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) { - checkFileNames("Open files", projectService.openFiles.map(info => info.fileName), expectedFiles.map(file => file.path)); + checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); } /** @@ -4327,6 +4327,40 @@ namespace ts.projectSystem { checkWatchedDirectories(host, [], /*recursive*/ false); checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); }); + + it("should use projectRootPath when searching for inferred project again 2", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true }); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project with project root path set + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.equal(service.inferredProjects[0].projectRootPath, projectDir); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); + }); }); describe("cancellationToken", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f749c27cbbc..b7617e1fa17 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -352,9 +352,9 @@ namespace ts.server { */ readonly configuredProjects = createMap(); /** - * list of open files + * Open files: with value being project root path, and key being Path of the file that is open */ - readonly openFiles: ScriptInfo[] = []; + readonly openFiles = createMap(); private compilerOptionsForInferredProjects: CompilerOptions; private compilerOptionsForInferredProjectsPerProjectRoot = createMap(); @@ -582,7 +582,7 @@ namespace ts.server { const event: ProjectsUpdatedInBackgroundEvent = { eventName: ProjectsUpdatedInBackgroundEvent, data: { - openFiles: this.openFiles.map(f => f.fileName) + openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path).fileName) } }; this.eventHandler(event); @@ -891,7 +891,7 @@ namespace ts.server { } /*@internal*/ - assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath?: string) { + assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { Debug.assert(info.isOrphan()); const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) || @@ -935,7 +935,7 @@ namespace ts.server { info.close(); this.stopWatchingConfigFilesForClosedScriptInfo(info); - unorderedRemoveItem(this.openFiles, info); + this.openFiles.delete(info.path); const fileExists = this.host.fileExists(info.fileName); @@ -974,11 +974,12 @@ namespace ts.server { } // collect orphaned files and assign them to inferred project just like we treat open of a file - for (const f of this.openFiles) { + this.openFiles.forEach((projectRootPath, path) => { + const f = this.getScriptInfoForPath(path as Path); if (f.isOrphan()) { - this.assignOrphanScriptInfoToInferredProject(f); + this.assignOrphanScriptInfoToInferredProject(f, projectRootPath); } - } + }); // Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project) // is postponed to next file open so that if file from same project is opened, @@ -1172,7 +1173,7 @@ namespace ts.server { * This is called by inferred project whenever script info is added as a root */ /* @internal */ - startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { + startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { Debug.assert(info.isScriptOpen()); this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); @@ -1194,7 +1195,7 @@ namespace ts.server { !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo); } - }); + }, projectRootPath); } /** @@ -1262,7 +1263,7 @@ namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) { + private getConfigFileNameForFile(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { Debug.assert(info.isScriptOpen()); this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); const configFileName = this.forEachConfigFileLocation(info, @@ -1301,9 +1302,9 @@ namespace ts.server { printProjects(this.inferredProjects, counter); this.logger.info("Open files: "); - for (const rootFile of this.openFiles) { - this.logger.info(`\t${rootFile.fileName}`); - } + this.openFiles.forEach((projectRootPath, path) => { + this.logger.info(`\tFileName: ${this.getScriptInfoForPath(path as Path).fileName} ProjectRootPath: ${projectRootPath}`); + }); this.logger.endGroup(); } @@ -1605,7 +1606,7 @@ namespace ts.server { }); } - private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: string | undefined): InferredProject | undefined { + private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined { if (!this.useInferredProjectPerProjectRoot) { return undefined; } @@ -1659,7 +1660,7 @@ namespace ts.server { return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true); } - private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject { + private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject { const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects; const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory); if (isSingleInferredProject) { @@ -1796,23 +1797,19 @@ namespace ts.server { // as there is no need to load contents of the files from the disk // Reload Projects - this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); + this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false, returnTrue); this.refreshInferredProjects(); } private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) { // Get open files to reload projects for - const openFiles = mapDefinedIter( - configFileExistenceInfo.openFilesImpactedByConfigFile.entries(), - ([path, isRootOfInferredProject]) => { - if (!ignoreIfNotRootOfInferredProject || isRootOfInferredProject) { - const info = this.getScriptInfoForPath(path as Path); - Debug.assert(!!info); - return info; - } - } + this.reloadConfiguredProjectForFiles( + configFileExistenceInfo.openFilesImpactedByConfigFile, + /*delayReload*/ true, + ignoreIfNotRootOfInferredProject ? + isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project + returnTrue // Reload all the open files impacted by config file ); - this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); this.delayInferredProjectsRefresh(); } @@ -1821,16 +1818,24 @@ namespace ts.server { * If the config file is found and it refers to existing project, it reloads it either immediately * or schedules it for reload depending on delayReload option * If the there is no existing project it just opens the configured project for the config file + * reloadForInfo provides a way to filter out files to reload configured project for */ - private reloadConfiguredProjectForFiles(openFiles: ReadonlyArray, delayReload: boolean) { + private reloadConfiguredProjectForFiles(openFiles: Map, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean) { const updatedProjects = createMap(); // try to reload config file for all open files - for (const info of openFiles) { + openFiles.forEach((openFileValue, path) => { + // Filter out the files that need to be ignored + if (!shouldReloadProjectFor(openFileValue)) { + return; + } + + const info = this.getScriptInfoForPath(path as Path); + Debug.assert(info.isScriptOpen()); // This tries to search for a tsconfig.json for the given file. If we found it, // 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); + const configFileName = this.getConfigFileNameForFile(info, this.openFiles.get(path)); if (configFileName) { const project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { @@ -1848,7 +1853,7 @@ namespace ts.server { updatedProjects.set(configFileName, true); } } - } + }); } /** @@ -1893,16 +1898,17 @@ namespace ts.server { this.logger.info("refreshInferredProjects: updating project structure from ..."); this.printProjects(); - for (const info of this.openFiles) { + this.openFiles.forEach((projectRootPath, path) => { + const info = this.getScriptInfoForPath(path as Path); // collect all orphaned script infos from open files if (info.isOrphan()) { - this.assignOrphanScriptInfoToInferredProject(info); + this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); } else { // Or remove the root of inferred project if is referenced in more than one projects this.removeRootOfInferredProjectIfNowPartOfOtherProject(info); } - } + }); for (const p of this.inferredProjects) { p.updateGraph(); @@ -1956,7 +1962,7 @@ namespace ts.server { this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); } Debug.assert(!info.isOrphan()); - this.openFiles.push(info); + this.openFiles.set(info.path, projectRootPath); if (sendConfigFileDiagEvent) { configFileErrors = project.getAllProjectErrors(); diff --git a/src/server/project.ts b/src/server/project.ts index 7542b30df00..58f15472e58 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1064,7 +1064,7 @@ namespace ts.server { projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, - projectRootPath: string | undefined, + projectRootPath: NormalizedPath | undefined, currentDirectory: string | undefined) { super(InferredProject.newName(), ProjectKind.Inferred, @@ -1080,7 +1080,8 @@ namespace ts.server { } addRoot(info: ScriptInfo) { - this.projectService.startWatchingConfigFilesForInferredProjectRoot(info); + Debug.assert(info.isScriptOpen()); + this.projectService.startWatchingConfigFilesForInferredProjectRoot(info, this.projectService.openFiles.get(info.path)); 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 0f20bfc8687..6b341d79397 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7435,9 +7435,9 @@ declare namespace ts.server { */ readonly configuredProjects: Map; /** - * list of open files + * Open files: with value being project root path, and key being Path of the file that is open */ - readonly openFiles: ScriptInfo[]; + readonly openFiles: Map; private compilerOptionsForInferredProjects; private compilerOptionsForInferredProjectsPerProjectRoot; /** @@ -7556,7 +7556,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, projectRootPath); private printProjects(); private findConfiguredProjectByProjectName(configFileName); private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); @@ -7592,8 +7592,9 @@ declare namespace ts.server { * If the config file is found and it refers to existing project, it reloads it either immediately * or schedules it for reload depending on delayReload option * If the there is no existing project it just opens the configured project for the config file + * reloadForInfo provides a way to filter out files to reload configured project for */ - private reloadConfiguredProjectForFiles(openFiles, delayReload); + private reloadConfiguredProjectForFiles(openFiles, delayReload, shouldReloadProjectFor); /** * Remove the root of inferred project if script info is part of another project */