Skip to content

Commit 402e2e6

Browse files
atscottAndrewKushnir
authored andcommitted
refactor(language-service): de-duplicate rename and reference results (angular#40454)
The initial implementation assumed that the consuming editors would de-duplicate rename locations. In fact, vscode treats overlapping rename locations as distinct and errors when trying to preview the renames. This commit updates the language service to de-duplicate exact file+span matches before returning rename and reference locations. While vscode _does_ de-duplicate reference results, it still makes sense to de-duplicate them on our side when possible to make tests more understandable. If a template has 3 instances of a variable, it makes sense to get get 3 reference results rather than 4+ with some duplicates. PR Close angular#40454
1 parent 5e95153 commit 402e2e6

File tree

2 files changed

+40
-20
lines changed

2 files changed

+40
-20
lines changed

packages/language-service/ivy/references.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class ReferencesAndRenameBuilder {
166166
return undefined;
167167
}
168168

169-
const entries: ts.RenameLocation[] = [];
169+
const entries: Map<string, ts.RenameLocation> = new Map();
170170
for (const location of locations) {
171171
// TODO(atscott): Determine if a file is a shim file in a more robust way and make the API
172172
// available in an appropriate location.
@@ -177,17 +177,17 @@ export class ReferencesAndRenameBuilder {
177177
if (entry === null) {
178178
return undefined;
179179
}
180-
entries.push(entry);
180+
entries.set(createLocationKey(entry), entry);
181181
} else {
182182
// Ensure we only allow renaming a TS result with matching text
183183
const refNode = this.getTsNodeAtPosition(location.fileName, location.textSpan.start);
184184
if (refNode === null || refNode.getText() !== originalNodeText) {
185185
return undefined;
186186
}
187-
entries.push(location);
187+
entries.set(createLocationKey(location), location);
188188
}
189189
}
190-
return entries;
190+
return Array.from(entries.values());
191191
}
192192

193193
getReferencesAtPosition(filePath: string, position: number): ts.ReferenceEntry[]|undefined {
@@ -344,18 +344,18 @@ export class ReferencesAndRenameBuilder {
344344
return undefined;
345345
}
346346

347-
const entries: ts.ReferenceEntry[] = [];
347+
const entries: Map<string, ts.ReferenceEntry> = new Map();
348348
for (const ref of refs) {
349349
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(ref.fileName))) {
350350
const entry = this.convertToTemplateDocumentSpan(ref, this.ttc);
351351
if (entry !== null) {
352-
entries.push(entry);
352+
entries.set(createLocationKey(entry), entry);
353353
}
354354
} else {
355-
entries.push(ref);
355+
entries.set(createLocationKey(ref), ref);
356356
}
357357
}
358-
return entries;
358+
return Array.from(entries.values());
359359
}
360360

361361
private convertToTemplateDocumentSpan<T extends ts.DocumentSpan>(
@@ -432,3 +432,13 @@ function getRenameTextAndSpanAtPosition(node: TmplAstNode|AST, position: number)
432432

433433
return null;
434434
}
435+
436+
437+
/**
438+
* Creates a "key" for a rename/reference location by concatenating file name, span start, and span
439+
* length. This allows us to de-duplicate template results when an item may appear several times
440+
* in the TCB but map back to the same template location.
441+
*/
442+
function createLocationKey(ds: ts.DocumentSpan) {
443+
return ds.fileName + ds.textSpan.start + ds.textSpan.length;
444+
}

packages/language-service/ivy/test/references_spec.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -554,33 +554,43 @@ describe('find references and rename locations', () => {
554554
let cursor: number;
555555
beforeEach(() => {
556556
const cursorInfo = extractCursorInfo(`
557+
<div *ngFor="let hero of heroes">
558+
<span *ngIf="hero">
559+
{{her¦o}}
560+
</span>
561+
</div>
562+
`);
563+
cursor = cursorInfo.cursor;
564+
const appFile = {
565+
name: _('/app.ts'),
566+
contents: `
557567
import {Component} from '@angular/core';
558568
559-
@Component({template: '<div *ngFor="let hero of heroes">{{her¦o}}</div>'})
569+
@Component({templateUrl: './template.ng.html'})
560570
export class AppCmp {
561571
heroes: string[] = [];
562-
}`);
563-
cursor = cursorInfo.cursor;
564-
const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
565-
env = createModuleWithDeclarations([appFile]);
572+
}`
573+
};
574+
const templateFile = {name: _('/template.ng.html'), contents: cursorInfo.text};
575+
env = createModuleWithDeclarations([appFile], [templateFile]);
566576
});
567577

568578
it('should find references', () => {
569-
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
570-
expect(refs.length).toBe(2);
571-
assertFileNames(refs, ['app.ts']);
579+
const refs = getReferencesAtPosition(_('/template.ng.html'), cursor)!;
580+
expect(refs.length).toBe(3);
581+
assertFileNames(refs, ['template.ng.html']);
572582
assertTextSpans(refs, ['hero']);
573583

574-
const originalRefs = env.ngLS.getReferencesAtPosition(_('/app.ts'), cursor)!;
584+
const originalRefs = env.ngLS.getReferencesAtPosition(_('/template.ng.html'), cursor)!;
575585
// Get the declaration by finding the reference that appears first in the template
576586
originalRefs.sort((a, b) => a.textSpan.start - b.textSpan.start);
577587
expect(originalRefs[0].isDefinition).toBe(true);
578588
});
579589

580590
it('should find rename locations', () => {
581-
const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
582-
expect(renameLocations.length).toBe(2);
583-
assertFileNames(renameLocations, ['app.ts']);
591+
const renameLocations = getRenameLocationsAtPosition(_('/template.ng.html'), cursor)!;
592+
expect(renameLocations.length).toBe(3);
593+
assertFileNames(renameLocations, ['template.ng.html']);
584594
assertTextSpans(renameLocations, ['hero']);
585595
});
586596
});

0 commit comments

Comments
 (0)