diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 8038e7276aef6..fc79dc7c49159 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -629,6 +629,8 @@ namespace ts.server { * Open files: with value being project root path, and key being Path of the file that is open */ readonly openFiles: Map = new Map(); + /* @internal */ + readonly configFileForOpenFiles: ESMap = new Map(); /** * Map of open files that are opened without complete path but have projectRoot as current directory */ @@ -1461,6 +1463,7 @@ namespace ts.server { } this.openFiles.delete(info.path); + this.configFileForOpenFiles.delete(info.path); if (!skipAssignOrphanScriptInfosToInferredProject && ensureProjectsForOpenFiles) { this.assignOrphanScriptInfosToInferredProject(); @@ -1791,7 +1794,11 @@ namespace ts.server { * otherwise just file name */ private getConfigFileNameForFile(info: OpenScriptInfoOrClosedOrConfigFileInfo) { - if (isOpenScriptInfo(info)) Debug.assert(info.isScriptOpen()); + if (isOpenScriptInfo(info)) { + Debug.assert(info.isScriptOpen()); + const result = this.configFileForOpenFiles.get(info.path); + if (result !== undefined) return result || undefined; + } this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => this.configFileExists(configFileName, canonicalConfigFilePath, info)); @@ -1801,6 +1808,9 @@ namespace ts.server { else { this.logger.info(`For info: ${info.fileName} :: No config files found.`); } + if (isOpenScriptInfo(info)) { + this.configFileForOpenFiles.set(info.path, configFileName || false); + } return configFileName; } @@ -2171,14 +2181,14 @@ namespace ts.server { * Read the config file of the project again by clearing the cache and update the project graph */ /* @internal */ - reloadConfiguredProject(project: ConfiguredProject, reason: string) { + reloadConfiguredProject(project: ConfiguredProject, reason: string, isInitialLoad: boolean) { // At this point, there is no reason to not have configFile in the host const host = project.getCachedDirectoryStructureHost(); // Clear the cache since we are reloading the project from disk host.clearCache(); const configFileName = project.getConfigFilePath(); - this.logger.info(`Reloading configured project ${configFileName}`); + this.logger.info(`${isInitialLoad ? "Loading" : "Reloading"} configured project ${configFileName}`); // Load project from the disk this.loadConfiguredProject(project, reason); @@ -2795,11 +2805,13 @@ namespace ts.server { const reloadChildProject = (child: ConfiguredProject) => { if (!updatedProjects.has(child.canonicalConfigFilePath)) { updatedProjects.set(child.canonicalConfigFilePath, true); - this.reloadConfiguredProject(child, reason); + this.reloadConfiguredProject(child, reason, /*isInitialLoad*/ false); } }; // try to reload config file for all open files openFiles.forEach((openFileValue, path) => { + // Invalidate default config file name for open file + this.configFileForOpenFiles.delete(path); // Filter out the files that need to be ignored if (!shouldReloadProjectFor(openFileValue)) { return; @@ -2823,7 +2835,7 @@ namespace ts.server { } else { // reload from the disk - this.reloadConfiguredProject(project, reason); + this.reloadConfiguredProject(project, reason, /*isInitialLoad*/ false); // If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly if (!projectContainsInfoDirectly(project, info)) { const referencedProject = forEachResolvedProjectReferenceProject( diff --git a/src/server/project.ts b/src/server/project.ts index c8adceaaed51d..16f3178d7f786 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -2102,6 +2102,7 @@ namespace ts.server { * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { + const isInitialLoad = this.isInitialLoadPending(); this.isInitialLoadPending = returnFalse; const reloadLevel = this.pendingReload; this.pendingReload = ConfigFileProgramReloadLevel.None; @@ -2115,7 +2116,7 @@ namespace ts.server { this.openFileWatchTriggered.clear(); const reason = Debug.checkDefined(this.pendingReloadReason); this.pendingReloadReason = undefined; - this.projectService.reloadConfiguredProject(this, reason); + this.projectService.reloadConfiguredProject(this, reason, isInitialLoad); result = true; break; default: diff --git a/src/server/session.ts b/src/server/session.ts index c559fb8faa989..57ba10fd59bda 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -284,34 +284,43 @@ namespace ts.server { return deduplicate(outputs, equateValues); } - function combineProjectOutputFromEveryProject(projectService: ProjectService, action: (project: Project) => readonly T[], areEqual: (a: T, b: T) => boolean) { - const outputs: T[] = []; + type CombineOutputResult = { project: Project; result: readonly T[]; }[]; + function combineOutputResultContains(outputs: CombineOutputResult, output: T, areEqual: (a: T, b: T) => boolean) { + return outputs.some(({ result }) => contains(result, output, areEqual)); + } + function addToCombineOutputResult(outputs: CombineOutputResult, project: Project, result: readonly T[]) { + if (result.length) outputs.push({ project, result }); + } + + function combineProjectOutputFromEveryProject(projectService: ProjectService, action: (project: Project) => readonly T[], areEqual: (a: T, b: T) => boolean): CombineOutputResult { + const outputs: CombineOutputResult = []; projectService.loadAncestorProjectTree(); projectService.forEachEnabledProject(project => { const theseOutputs = action(project); - outputs.push(...theseOutputs.filter(output => !outputs.some(o => areEqual(o, output)))); + addToCombineOutputResult(outputs, project, filter(theseOutputs, output => !combineOutputResultContains(outputs, output, areEqual))); }); return outputs; } + function flattenCombineOutputResult(outputs: CombineOutputResult): readonly T[] { + return flatMap(outputs, ({ result }) => result); + } + function combineProjectOutputWhileOpeningReferencedProjects( projects: Projects, defaultProject: Project, action: (project: Project) => readonly T[], getLocation: (t: T) => DocumentPosition, resultsEqual: (a: T, b: T) => boolean, - ): T[] { - const outputs: T[] = []; + ): CombineOutputResult { + const outputs: CombineOutputResult = []; combineProjectOutputWorker( projects, defaultProject, /*initialLocation*/ undefined, (project, _, tryAddToTodo) => { - for (const output of action(project)) { - if (!contains(outputs, output, resultsEqual) && !tryAddToTodo(project, getLocation(output))) { - outputs.push(output); - } - } + const theseOutputs = action(project); + addToCombineOutputResult(outputs, project, filter(theseOutputs, output => !combineOutputResultContains(outputs, output, resultsEqual) && !tryAddToTodo(project, getLocation(output)))); }, ); return outputs; @@ -326,7 +335,6 @@ namespace ts.server { hostPreferences: UserPreferences ): readonly RenameLocation[] { const outputs: RenameLocation[] = []; - combineProjectOutputWorker( projects, defaultProject, @@ -1930,38 +1938,42 @@ namespace ts.server { private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): readonly protocol.NavtoItem[] | readonly NavigateToItem[] { const full = this.getFullNavigateToItems(args); - return !simplifiedResult ? full : full.map((navItem) => { - const { file, project } = this.getFileAndProject({ file: navItem.fileName }); - const scriptInfo = project.getScriptInfo(file)!; - const bakedItem: protocol.NavtoItem = { - name: navItem.name, - kind: navItem.kind, - kindModifiers: navItem.kindModifiers, - isCaseSensitive: navItem.isCaseSensitive, - matchKind: navItem.matchKind, - file: navItem.fileName, - start: scriptInfo.positionToLineOffset(navItem.textSpan.start), - end: scriptInfo.positionToLineOffset(textSpanEnd(navItem.textSpan)) - }; - if (navItem.kindModifiers && (navItem.kindModifiers !== "")) { - bakedItem.kindModifiers = navItem.kindModifiers; - } - if (navItem.containerName && (navItem.containerName.length > 0)) { - bakedItem.containerName = navItem.containerName; - } - if (navItem.containerKind && (navItem.containerKind.length > 0)) { - bakedItem.containerKind = navItem.containerKind; - } - return bakedItem; - }); + return !simplifiedResult ? + flattenCombineOutputResult(full) : + flatMap( + full, + ({ project, result }) => result.map(navItem => { + const scriptInfo = project.getScriptInfo(navItem.fileName)!; + const bakedItem: protocol.NavtoItem = { + name: navItem.name, + kind: navItem.kind, + kindModifiers: navItem.kindModifiers, + isCaseSensitive: navItem.isCaseSensitive, + matchKind: navItem.matchKind, + file: navItem.fileName, + start: scriptInfo.positionToLineOffset(navItem.textSpan.start), + end: scriptInfo.positionToLineOffset(textSpanEnd(navItem.textSpan)) + }; + if (navItem.kindModifiers && (navItem.kindModifiers !== "")) { + bakedItem.kindModifiers = navItem.kindModifiers; + } + if (navItem.containerName && (navItem.containerName.length > 0)) { + bakedItem.containerName = navItem.containerName; + } + if (navItem.containerKind && (navItem.containerKind.length > 0)) { + bakedItem.containerKind = navItem.containerKind; + } + return bakedItem; + }) + ); } - private getFullNavigateToItems(args: protocol.NavtoRequestArgs): readonly NavigateToItem[] { + private getFullNavigateToItems(args: protocol.NavtoRequestArgs): CombineOutputResult { const { currentFileOnly, searchValue, maxResultCount, projectFileName } = args; if (currentFileOnly) { Debug.assertDefined(args.file); const { file, project } = this.getFileAndProject(args as protocol.FileRequestArgs); - return project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file); + return [{ project, result: project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file) }]; } else if (!args.file && !projectFileName) { return combineProjectOutputFromEveryProject( @@ -2082,10 +2094,13 @@ namespace ts.server { const newPath = toNormalizedPath(args.newFilePath); const formatOptions = this.getHostFormatOptions(); const preferences = this.getHostPreferences(); - const changes = combineProjectOutputFromEveryProject( - this.projectService, - project => project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences), - (a, b) => a.fileName === b.fileName); + const changes = flattenCombineOutputResult( + combineProjectOutputFromEveryProject( + this.projectService, + project => project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences), + (a, b) => a.fileName === b.fileName + ) + ); return simplifiedResult ? changes.map(c => this.mapTextChangeToCodeEdit(c)) : changes; } diff --git a/src/testRunner/unittests/tsserver/navTo.ts b/src/testRunner/unittests/tsserver/navTo.ts index e447a312e2fd8..1b606199d3836 100644 --- a/src/testRunner/unittests/tsserver/navTo.ts +++ b/src/testRunner/unittests/tsserver/navTo.ts @@ -61,8 +61,12 @@ namespace ts.projectSystem { export const ghijkl = a.abcdef;` }; const host = createServerHost([configFile1, file1, configFile2, file2]); - const session = createSession(host); + const logger = createHasErrorMessageLogger().logger; + const logs: string[] = []; + logger.info = s => logs.push(s); + const session = createSession(host, { logger }); openFilesForSession([file1, file2], session); + logs.length = 0; const request = makeSessionRequest(CommandNames.Navto, { searchValue: "abcdef", file: file1.path }); const items = session.executeCommand(request).response as protocol.NavtoItem[]; @@ -70,6 +74,62 @@ export const ghijkl = a.abcdef;` const item = items[0]; assert.strictEqual(item.name, "abcdef"); assert.strictEqual(item.file, file1.path); + assert.deepEqual(logs, []); + }); + + it("should de-duplicate symbols when searching all projects", () => { + const solutionConfig: File = { + path: "/tsconfig.json", + content: JSON.stringify({ + references: [{ path: "./a" }, { path: "./b" }], + files: [], + }) + }; + const configFile1: File = { + path: "/a/tsconfig.json", + content: `{ + "compilerOptions": { + "composite": true + } +}` + }; + const file1: File = { + path: "/a/index.ts", + content: "export const abcdef = 1;" + }; + const configFile2: File = { + path: "/b/tsconfig.json", + content: `{ + "compilerOptions": { + "composite": true + }, + "references": [ + { "path": "../a" } + ] +}` + }; + const file2: File = { + path: "/b/index.ts", + content: `import a = require("../a"); +export const ghijkl = a.abcdef;` + }; + const host = createServerHost([configFile1, file1, configFile2, file2, solutionConfig]); + const logger = createHasErrorMessageLogger().logger; + const logs: string[] = []; + logger.info = s => logs.push(s); + const session = createSession(host, { logger }); + openFilesForSession([file1], session); + logs.length = 0; + + const request = makeSessionRequest(CommandNames.Navto, { searchValue: "abcdef" }); + const items = session.executeCommand(request).response as protocol.NavtoItem[]; + assert.strictEqual(items.length, 1); + const item = items[0]; + assert.strictEqual(item.name, "abcdef"); + assert.strictEqual(item.file, file1.path); + // Cannt check logs as empty since it loads projects and writes information for those in the log + assert.isFalse(contains(logs, "Search path: /a")); + assert.isFalse(contains(logs, "For info: /a/index.ts :: Config file name: /a/tsconfig.json")); }); it("should work with Deprecated", () => {