Skip to content

Commit 418989b

Browse files
authored
Tidy up navto and find-file-references (#48106)
They were using the same helpers as FAR and rename, but they actually behave differently. Decoupling them helps avoid some unnecessary work and will make it easier to clean up FAR in the future. Scoping NavTo results to a single project (rather than, e.g. all loaded projects) makes the behavior more logical and the implementation simpler.
1 parent 20c93d3 commit 418989b

File tree

2 files changed

+140
-132
lines changed

2 files changed

+140
-132
lines changed

src/server/session.ts

Lines changed: 137 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -294,70 +294,35 @@ namespace ts.server {
294294
return deduplicate(outputs, equateValues);
295295
}
296296

297-
type CombineOutputResult<T> = { project: Project; result: readonly T[]; }[];
298-
function combineOutputResultContains<T>(outputs: CombineOutputResult<T>, output: T, areEqual: (a: T, b: T) => boolean) {
299-
return outputs.some(({ result }) => contains(result, output, areEqual));
300-
}
301-
function addToCombineOutputResult<T>(outputs: CombineOutputResult<T>, project: Project, result: readonly T[]) {
302-
if (result.length) outputs.push({ project, result });
303-
}
304-
305-
function combineProjectOutputFromEveryProject<T>(projectService: ProjectService, action: (project: Project) => readonly T[], areEqual: (a: T, b: T) => boolean): CombineOutputResult<T> {
306-
const outputs: CombineOutputResult<T> = [];
307-
projectService.loadAncestorProjectTree();
308-
projectService.forEachEnabledProject(project => {
309-
const theseOutputs = action(project);
310-
addToCombineOutputResult(outputs, project, filter(theseOutputs, output => !combineOutputResultContains(outputs, output, areEqual)));
311-
});
312-
return outputs;
313-
}
314-
315-
function flattenCombineOutputResult<T>(outputs: CombineOutputResult<T>): readonly T[] {
316-
return flatMap(outputs, ({ result }) => result);
317-
}
318-
319-
function combineProjectOutputWhileOpeningReferencedProjects<T>(
320-
projects: Projects,
321-
defaultProject: Project,
322-
action: (project: Project) => readonly T[],
323-
getLocation: (t: T) => DocumentPosition,
324-
resultsEqual: (a: T, b: T) => boolean,
325-
): CombineOutputResult<T> {
326-
const outputs: CombineOutputResult<T> = [];
327-
combineProjectOutputWorker(
328-
projects,
329-
defaultProject,
330-
/*initialLocation*/ undefined,
331-
(project, _, tryAddToTodo) => {
332-
const theseOutputs = action(project);
333-
addToCombineOutputResult(outputs, project, filter(theseOutputs, output => !combineOutputResultContains(outputs, output, resultsEqual) && !tryAddToTodo(project, getLocation(output))));
334-
},
335-
);
336-
return outputs;
337-
}
297+
interface ProjectNavigateToItems {
298+
project: Project;
299+
navigateToItems: readonly NavigateToItem[];
300+
};
338301

339302
function combineProjectOutputForRenameLocations(
340303
projects: Projects,
341304
defaultProject: Project,
342305
initialLocation: DocumentPosition,
343306
findInStrings: boolean,
344307
findInComments: boolean,
345-
hostPreferences: UserPreferences
308+
{ providePrefixAndSuffixTextForRename }: UserPreferences
346309
): readonly RenameLocation[] {
347310
const outputs: RenameLocation[] = [];
348311
combineProjectOutputWorker(
349312
projects,
350313
defaultProject,
351314
initialLocation,
352315
(project, location, tryAddToTodo) => {
353-
for (const output of project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments, hostPreferences.providePrefixAndSuffixTextForRename) || emptyArray) {
354-
if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) {
355-
outputs.push(output);
316+
const projectOutputs = project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments, providePrefixAndSuffixTextForRename);
317+
if (projectOutputs) {
318+
for (const output of projectOutputs) {
319+
if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) {
320+
outputs.push(output);
321+
}
356322
}
357323
}
358324
},
359325
);
360-
361326
return outputs;
362327
}
363328

@@ -381,27 +346,30 @@ namespace ts.server {
381346
initialLocation,
382347
(project, location, getMappedLocation) => {
383348
logger.info(`Finding references to ${location.fileName} position ${location.pos} in project ${project.getProjectName()}`);
384-
for (const outputReferencedSymbol of project.getLanguageService().findReferences(location.fileName, location.pos) || emptyArray) {
385-
const mappedDefinitionFile = getMappedLocation(project, documentSpanLocation(outputReferencedSymbol.definition));
386-
const definition: ReferencedSymbolDefinitionInfo = mappedDefinitionFile === undefined ?
387-
outputReferencedSymbol.definition :
388-
{
389-
...outputReferencedSymbol.definition,
390-
textSpan: createTextSpan(mappedDefinitionFile.pos, outputReferencedSymbol.definition.textSpan.length),
391-
fileName: mappedDefinitionFile.fileName,
392-
contextSpan: getMappedContextSpan(outputReferencedSymbol.definition, project)
393-
};
394-
395-
let symbolToAddTo = find(outputs, o => documentSpansEqual(o.definition, definition));
396-
if (!symbolToAddTo) {
397-
symbolToAddTo = { definition, references: [] };
398-
outputs.push(symbolToAddTo);
399-
}
349+
const projectOutputs = project.getLanguageService().findReferences(location.fileName, location.pos);
350+
if (projectOutputs) {
351+
for (const referencedSymbol of projectOutputs) {
352+
const mappedDefinitionFile = getMappedLocation(project, documentSpanLocation(referencedSymbol.definition));
353+
const definition: ReferencedSymbolDefinitionInfo = mappedDefinitionFile === undefined ?
354+
referencedSymbol.definition :
355+
{
356+
...referencedSymbol.definition,
357+
textSpan: createTextSpan(mappedDefinitionFile.pos, referencedSymbol.definition.textSpan.length),
358+
fileName: mappedDefinitionFile.fileName,
359+
contextSpan: getMappedContextSpan(referencedSymbol.definition, project)
360+
};
361+
362+
let symbolToAddTo = find(outputs, o => documentSpansEqual(o.definition, definition));
363+
if (!symbolToAddTo) {
364+
symbolToAddTo = { definition, references: [] };
365+
outputs.push(symbolToAddTo);
366+
}
400367

401-
for (const ref of outputReferencedSymbol.references) {
402-
// If it's in a mapped file, that is added to the todo list by `getMappedLocation`.
403-
if (!contains(symbolToAddTo.references, ref, documentSpansEqual) && !getMappedLocation(project, documentSpanLocation(ref))) {
404-
symbolToAddTo.references.push(ref);
368+
for (const ref of referencedSymbol.references) {
369+
// If it's in a mapped file, that is added to the todo list by `getMappedLocation`.
370+
if (!contains(symbolToAddTo.references, ref, documentSpansEqual) && !getMappedLocation(project, documentSpanLocation(ref))) {
371+
symbolToAddTo.references.push(ref);
372+
}
405373
}
406374
}
407375
}
@@ -411,29 +379,6 @@ namespace ts.server {
411379
return outputs.filter(o => o.references.length !== 0);
412380
}
413381

414-
function combineProjectOutputForFileReferences(
415-
projects: Projects,
416-
defaultProject: Project,
417-
fileName: string
418-
): readonly ReferenceEntry[] {
419-
const outputs: ReferenceEntry[] = [];
420-
421-
combineProjectOutputWorker(
422-
projects,
423-
defaultProject,
424-
/*initialLocation*/ undefined,
425-
project => {
426-
for (const referenceEntry of project.getLanguageService().getFileReferences(fileName) || emptyArray) {
427-
if (!contains(outputs, referenceEntry, documentSpansEqual)) {
428-
outputs.push(referenceEntry);
429-
}
430-
}
431-
},
432-
);
433-
434-
return outputs;
435-
}
436-
437382
interface ProjectAndLocation<TLocation extends DocumentPosition | undefined> {
438383
readonly project: Project;
439384
readonly location: TLocation;
@@ -1610,11 +1555,22 @@ namespace ts.server {
16101555

16111556
private getFileReferences(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.FileReferencesResponseBody | readonly ReferenceEntry[] {
16121557
const projects = this.getProjects(args);
1613-
const references = combineProjectOutputForFileReferences(
1614-
projects,
1615-
this.getDefaultProject(args),
1616-
args.file,
1617-
);
1558+
const fileName = args.file;
1559+
1560+
const references: ReferenceEntry[] = [];
1561+
1562+
forEachProjectInProjects(projects, /*path*/ undefined, project => {
1563+
if (project.getCancellationToken().isCancellationRequested()) return;
1564+
1565+
const projectOutputs = project.getLanguageService().getFileReferences(fileName);
1566+
if (projectOutputs) {
1567+
for (const referenceEntry of projectOutputs) {
1568+
if (!contains(references, referenceEntry, documentSpansEqual)) {
1569+
references.push(referenceEntry);
1570+
}
1571+
}
1572+
}
1573+
});
16181574

16191575
if (!simplifiedResult) return references;
16201576
const refs = references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry));
@@ -2092,10 +2048,10 @@ namespace ts.server {
20922048
private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): readonly protocol.NavtoItem[] | readonly NavigateToItem[] {
20932049
const full = this.getFullNavigateToItems(args);
20942050
return !simplifiedResult ?
2095-
flattenCombineOutputResult(full) :
2051+
flatMap(full, ({ navigateToItems }) => navigateToItems) :
20962052
flatMap(
20972053
full,
2098-
({ project, result }) => result.map(navItem => {
2054+
({ project, navigateToItems }) => navigateToItems.map(navItem => {
20992055
const scriptInfo = project.getScriptInfo(navItem.fileName)!;
21002056
const bakedItem: protocol.NavtoItem = {
21012057
name: navItem.name,
@@ -2121,26 +2077,72 @@ namespace ts.server {
21212077
);
21222078
}
21232079

2124-
private getFullNavigateToItems(args: protocol.NavtoRequestArgs): CombineOutputResult<NavigateToItem> {
2080+
private getFullNavigateToItems(args: protocol.NavtoRequestArgs): ProjectNavigateToItems[] {
21252081
const { currentFileOnly, searchValue, maxResultCount, projectFileName } = args;
2082+
21262083
if (currentFileOnly) {
21272084
Debug.assertIsDefined(args.file);
21282085
const { file, project } = this.getFileAndProject(args as protocol.FileRequestArgs);
2129-
return [{ project, result: project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file) }];
2130-
}
2131-
else if (!args.file && !projectFileName) {
2132-
return combineProjectOutputFromEveryProject(
2133-
this.projectService,
2134-
project => project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*filename*/ undefined, /*excludeDts*/ project.isNonTsProject()),
2135-
navigateToItemIsEqualTo);
2136-
}
2137-
const fileArgs = args as protocol.FileRequestArgs;
2138-
return combineProjectOutputWhileOpeningReferencedProjects<NavigateToItem>(
2139-
this.getProjects(fileArgs),
2140-
this.getDefaultProject(fileArgs),
2141-
project => project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*fileName*/ undefined, /*excludeDts*/ project.isNonTsProject()),
2142-
documentSpanLocation,
2143-
navigateToItemIsEqualTo);
2086+
return [{ project, navigateToItems: project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file) }];
2087+
}
2088+
2089+
const outputs: ProjectNavigateToItems[] = [];
2090+
2091+
// This is effectively a hashset with `name` as the custom hash and `navigateToItemIsEqualTo` as the custom equals.
2092+
// `name` is a very cheap hash function, but we could incorporate other properties to reduce collisions.
2093+
const seenItems = new Map<string, NavigateToItem[]>(); // name to items with that name
2094+
2095+
if (!args.file && !projectFileName) {
2096+
// VS Code's `Go to symbol in workspaces` sends request like this
2097+
2098+
// TODO (https://github.com/microsoft/TypeScript/issues/47839)
2099+
// This appears to have been intended to search all projects but, in practice, it seems to only search
2100+
// those that are downstream from already-loaded projects.
2101+
// Filtering by !isSourceOfProjectReferenceRedirect is new, but seems appropriate and consistent with
2102+
// the case below.
2103+
this.projectService.loadAncestorProjectTree();
2104+
this.projectService.forEachEnabledProject(project => addItemsForProject(project));
2105+
}
2106+
else {
2107+
// VS's `Go to symbol` sends requests with just a project and doesn't want cascading since it will
2108+
// send a separate request for each project of interest
2109+
2110+
// TODO (https://github.com/microsoft/TypeScript/issues/47839)
2111+
// This doesn't really make sense unless it's a single project matching `projectFileName`
2112+
const projects = this.getProjects(args as protocol.FileRequestArgs);
2113+
forEachProjectInProjects(projects, /*path*/ undefined, project => addItemsForProject(project));
2114+
}
2115+
2116+
return outputs;
2117+
2118+
// Mutates `outputs`
2119+
function addItemsForProject(project: Project) {
2120+
const projectItems = project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*filename*/ undefined, /*excludeDts*/ project.isNonTsProject());
2121+
const unseenItems = filter(projectItems, item => tryAddSeenItem(item) && !getMappedLocation(documentSpanLocation(item), project));
2122+
if (unseenItems.length) {
2123+
outputs.push({ project, navigateToItems: unseenItems });
2124+
}
2125+
}
2126+
2127+
// Returns true if the item had not been seen before
2128+
// Mutates `seenItems`
2129+
function tryAddSeenItem(item: NavigateToItem) {
2130+
const name = item.name;
2131+
if (!seenItems.has(name)) {
2132+
seenItems.set(name, [item]);
2133+
return true;
2134+
}
2135+
2136+
const seen = seenItems.get(name)!;
2137+
for (const seenItem of seen) {
2138+
if (navigateToItemIsEqualTo(seenItem, item)) {
2139+
return false;
2140+
}
2141+
}
2142+
2143+
seen.push(item);
2144+
return true;
2145+
}
21442146

21452147
function navigateToItemIsEqualTo(a: NavigateToItem, b: NavigateToItem): boolean {
21462148
if (a === b) {
@@ -2255,14 +2257,29 @@ namespace ts.server {
22552257
const newPath = toNormalizedPath(args.newFilePath);
22562258
const formatOptions = this.getHostFormatOptions();
22572259
const preferences = this.getHostPreferences();
2258-
const changes = flattenCombineOutputResult(
2259-
combineProjectOutputFromEveryProject(
2260-
this.projectService,
2261-
project => project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences),
2262-
(a, b) => a.fileName === b.fileName
2263-
)
2264-
);
2265-
return simplifiedResult ? changes.map(c => this.mapTextChangeToCodeEdit(c)) : changes;
2260+
2261+
2262+
const seenFiles = new Set<string>();
2263+
const textChanges: FileTextChanges[] = [];
2264+
// TODO (https://github.com/microsoft/TypeScript/issues/47839)
2265+
// This appears to have been intended to search all projects but, in practice, it seems to only search
2266+
// those that are downstream from already-loaded projects.
2267+
this.projectService.loadAncestorProjectTree();
2268+
this.projectService.forEachEnabledProject(project => {
2269+
const projectTextChanges = project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences);
2270+
const projectFiles: string[] = [];
2271+
for (const textChange of projectTextChanges) {
2272+
if (!seenFiles.has(textChange.fileName)) {
2273+
textChanges.push(textChange);
2274+
projectFiles.push(textChange.fileName);
2275+
}
2276+
}
2277+
for (const file of projectFiles) {
2278+
seenFiles.add(file);
2279+
}
2280+
});
2281+
2282+
return simplifiedResult ? textChanges.map(c => this.mapTextChangeToCodeEdit(c)) : textChanges;
22662283
}
22672284

22682285
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): readonly protocol.CodeFixAction[] | readonly CodeFixAction[] | undefined {

src/testRunner/unittests/tsserver/declarationFileMaps.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ namespace ts.projectSystem {
286286
const session = makeSampleProjects();
287287
const response = executeSessionRequest<protocol.NavtoRequest, protocol.NavtoResponse>(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" });
288288
assert.deepEqual<readonly protocol.NavtoItem[] | undefined>(response, [
289+
// Keep the .d.ts file since the .ts file no longer exists
290+
// (otherwise it would be treated as not in the project)
289291
{
290292
...protocolFileSpanFromSubstring({
291293
file: bDts,
@@ -308,20 +310,9 @@ namespace ts.projectSystem {
308310
kind: ScriptElementKind.functionElement,
309311
kindModifiers: "export",
310312
},
311-
{
312-
...protocolFileSpanFromSubstring({
313-
file: aTs,
314-
text: "export function fnA() {}"
315-
}),
316-
name: "fnA",
317-
matchKind: "prefix",
318-
isCaseSensitive: true,
319-
kind: ScriptElementKind.functionElement,
320-
kindModifiers: "export",
321-
},
322313
]);
323314

324-
verifyATsConfigOriginalProject(session);
315+
verifySingleInferredProject(session);
325316
});
326317

327318
it("navigateToAll -- when neither file nor project is specified", () => {

0 commit comments

Comments
 (0)