diff --git a/src/server/session.ts b/src/server/session.ts index a598178514b23..6caf006e0737f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -380,7 +380,7 @@ namespace ts.server { // correct results to all other projects. const defaultProjectResults = perProjectResults.get(defaultProject); - if (defaultProjectResults?.[0].references[0]?.isDefinition === undefined) { + if (defaultProjectResults?.[0]?.references[0]?.isDefinition === undefined) { // Clear all isDefinition properties perProjectResults.forEach(projectResults => { for (const referencedSymbol of projectResults) { @@ -531,26 +531,35 @@ namespace ts.server { defaultDefinition : defaultProject.getLanguageService().getSourceMapper().tryGetSourcePosition(defaultDefinition!)); - // Track which projects we have already searched so that we don't repeat searches. - // We store the project key, rather than the project, because that's what `loadAncestorProjectTree` wants. - // (For that same reason, we don't use `resultsMap` for this check.) - const searchedProjects = new Set(); + // The keys of resultsMap allow us to check which projects have already been searched, but we also + // maintain a set of strings because that's what `loadAncestorProjectTree` wants. + const searchedProjectKeys = new Set(); onCancellation: while (queue.length) { while (queue.length) { if (cancellationToken.isCancellationRequested()) break onCancellation; + let skipCount = 0; + for (; skipCount < queue.length && resultsMap.has(queue[skipCount].project); skipCount++); + + if (skipCount === queue.length) { + queue.length = 0; + break; + } + + if (skipCount > 0) { + queue.splice(0, skipCount); + } + + // NB: we may still skip if it's a project reference redirect const { project, location } = queue.shift()!; if (isLocationProjectReferenceRedirect(project, location)) continue; - if (!tryAddToSet(searchedProjects, getProjectKey(project))) continue; - const projectResults = searchPosition(project, location); - if (projectResults) { - resultsMap.set(project, projectResults); - } + resultsMap.set(project, projectResults ?? emptyArray); + searchedProjectKeys.add(getProjectKey(project)); } // At this point, we know about all projects passed in as arguments and any projects in which @@ -559,10 +568,10 @@ namespace ts.server { // containing `initialLocation`. if (defaultDefinition) { // This seems to mean "load all projects downstream from any member of `seenProjects`". - projectService.loadAncestorProjectTree(searchedProjects); + projectService.loadAncestorProjectTree(searchedProjectKeys); projectService.forEachEnabledProject(project => { if (cancellationToken.isCancellationRequested()) return; // There's no mechanism for skipping the remaining projects - if (searchedProjects.has(getProjectKey(project))) return; // Can loop forever without this (enqueue here, dequeue above, repeat) + if (resultsMap.has(project)) return; // Can loop forever without this (enqueue here, dequeue above, repeat) const location = mapDefinitionInProject(defaultDefinition, project, getGeneratedDefinition, getSourceDefinition); if (location) { queue.push({ project, location }); @@ -573,9 +582,10 @@ namespace ts.server { // In the common case where there's only one project, return a simpler result to make // it easier for the caller to skip post-processing. - if (searchedProjects.size === 1) { + if (resultsMap.size === 1) { const it = resultsMap.values().next(); - return it.done ? emptyArray : it.value; // There may not be any results at all + Debug.assert(!it.done); + return it.value; } return resultsMap; @@ -593,7 +603,7 @@ namespace ts.server { const originalScriptInfo = projectService.getScriptInfo(originalLocation.fileName)!; for (const project of originalScriptInfo.containingProjects) { - if (!project.isOrphan()) { + if (!project.isOrphan() && !resultsMap.has(project)) { // Optimization: don't enqueue if will be discarded queue.push({ project, location: originalLocation }); } } @@ -602,7 +612,7 @@ namespace ts.server { if (symlinkedProjectsMap) { symlinkedProjectsMap.forEach((symlinkedProjects, symlinkedPath) => { for (const symlinkedProject of symlinkedProjects) { - if (!symlinkedProject.isOrphan()) { + if (!symlinkedProject.isOrphan() && !resultsMap.has(symlinkedProject)) { // Optimization: don't enqueue if will be discarded queue.push({ project: symlinkedProject, location: { fileName: symlinkedPath as string, pos: originalLocation.pos } }); } }