Skip to content

Commit 973f797

Browse files
atscottalxhub
authored andcommitted
feat(language-service): enable get references for directive and component from template (#40054)
This commit adds the ability to find references for a directive or component from within a component template. That is, you can find component references from the element tag `<my-c|omp></my-comp>` (where `|` is the cursor position) as well as find references for directives that match a given attribute `<div d|ir></div>`. PR Close #40054
1 parent 1bf1b68 commit 973f797

File tree

3 files changed

+134
-11
lines changed

3 files changed

+134
-11
lines changed
12 KB
Binary file not shown.

packages/language-service/ivy/references.ts

+40-11
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {TmplAstVariable} from '@angular/compiler';
8+
import {TmplAstBoundAttribute, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
99
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
1010
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
11-
import {SymbolKind, TemplateTypeChecker, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
11+
import {DirectiveSymbol, SymbolKind, TemplateTypeChecker, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1212
import * as ts from 'typescript';
1313

1414
import {getTargetAtPosition} from './template_target';
15-
import {getTemplateInfoAtPosition, isWithin, TemplateInfo, toTextSpan} from './utils';
15+
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isWithin, TemplateInfo, toTextSpan} from './utils';
1616

1717
export class ReferenceBuilder {
1818
private readonly ttc = this.compiler.getTemplateTypeChecker();
@@ -43,19 +43,27 @@ export class ReferenceBuilder {
4343
return undefined;
4444
}
4545
switch (symbol.kind) {
46-
case SymbolKind.Element:
4746
case SymbolKind.Directive:
4847
case SymbolKind.Template:
49-
case SymbolKind.DomBinding:
5048
// References to elements, templates, and directives will be through template references
5149
// (#ref). They shouldn't be used directly for a Language Service reference request.
52-
//
53-
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
54-
// have a shim location and so we cannot find references for them.
55-
//
56-
// TODO(atscott): Consider finding references for elements that are components as well as
57-
// when the position is on an element attribute that directly maps to a directive.
5850
return undefined;
51+
case SymbolKind.Element: {
52+
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
53+
return this.getReferencesForDirectives(matches);
54+
}
55+
case SymbolKind.DomBinding: {
56+
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
57+
// have a shim location. This means we can't match dom bindings to their lib.dom reference,
58+
// but we can still see if they match to a directive.
59+
if (!(positionDetails.node instanceof TmplAstTextAttribute) &&
60+
!(positionDetails.node instanceof TmplAstBoundAttribute)) {
61+
return undefined;
62+
}
63+
const directives = getDirectiveMatchesForAttribute(
64+
positionDetails.node.name, symbol.host.templateNode, symbol.host.directives);
65+
return this.getReferencesForDirectives(directives);
66+
}
5967
case SymbolKind.Reference: {
6068
const {shimPath, positionInShimFile} = symbol.referenceVarLocation;
6169
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
@@ -94,6 +102,27 @@ export class ReferenceBuilder {
94102
}
95103
}
96104

105+
private getReferencesForDirectives(directives: Set<DirectiveSymbol>):
106+
ts.ReferenceEntry[]|undefined {
107+
const allDirectiveRefs: ts.ReferenceEntry[] = [];
108+
for (const dir of directives.values()) {
109+
const dirClass = dir.tsSymbol.valueDeclaration;
110+
if (dirClass === undefined || !ts.isClassDeclaration(dirClass) ||
111+
dirClass.name === undefined) {
112+
continue;
113+
}
114+
115+
const dirFile = dirClass.getSourceFile().fileName;
116+
const dirPosition = dirClass.name.getStart();
117+
const directiveRefs = this.getReferencesAtTypescriptPosition(dirFile, dirPosition);
118+
if (directiveRefs !== undefined) {
119+
allDirectiveRefs.push(...directiveRefs);
120+
}
121+
}
122+
123+
return allDirectiveRefs.length > 0 ? allDirectiveRefs : undefined;
124+
}
125+
97126
private getReferencesAtTypescriptPosition(fileName: string, position: number):
98127
ts.ReferenceEntry[]|undefined {
99128
const refs = this.tsLS.getReferencesAtPosition(fileName, position);

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

+94
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,100 @@ describe('find references', () => {
679679
assertTextSpans(refs, ['<div dir>', 'Dir']);
680680
assertFileNames(refs, ['app.ts', 'dir.ts']);
681681
});
682+
683+
it('gets references to all matching directives when cursor is on an attribute', () => {
684+
const dirFile = `
685+
import {Directive} from '@angular/core';
686+
687+
@Directive({selector: '[dir]'})
688+
export class Dir {}`;
689+
const dirFile2 = `
690+
import {Directive} from '@angular/core';
691+
692+
@Directive({selector: '[dir]'})
693+
export class Dir2 {}`;
694+
const {text, cursor} = extractCursorInfo(`
695+
import {Component, NgModule} from '@angular/core';
696+
import {Dir} from './dir';
697+
import {Dir2} from './dir2';
698+
699+
@Component({template: '<div di¦r></div>'})
700+
export class AppCmp {
701+
}
702+
703+
@NgModule({declarations: [AppCmp, Dir, Dir2]})
704+
export class AppModule {}
705+
`);
706+
env = LanguageServiceTestEnvironment.setup([
707+
{name: _('/app.ts'), contents: text, isRoot: true},
708+
{name: _('/dir.ts'), contents: dirFile},
709+
{name: _('/dir2.ts'), contents: dirFile2},
710+
]);
711+
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
712+
expect(refs.length).toBe(8);
713+
assertTextSpans(refs, ['<div dir>', 'Dir', 'Dir2']);
714+
assertFileNames(refs, ['app.ts', 'dir.ts', 'dir2.ts']);
715+
});
716+
});
717+
718+
describe('components', () => {
719+
it('works for component classes', () => {
720+
const {text, cursor} = extractCursorInfo(`
721+
import {Component} from '@angular/core';
722+
723+
@Component({selector: 'my-comp', template: ''})
724+
export class MyCo¦mp {}`);
725+
const appFile = `
726+
import {Component, NgModule} from '@angular/core';
727+
import {MyComp} from './comp';
728+
729+
@Component({template: '<my-comp></my-comp>'})
730+
export class AppCmp {
731+
}
732+
733+
@NgModule({declarations: [AppCmp, MyComp]})
734+
export class AppModule {}
735+
`;
736+
env = LanguageServiceTestEnvironment.setup([
737+
{name: _('/app.ts'), contents: appFile, isRoot: true},
738+
{name: _('/comp.ts'), contents: text},
739+
]);
740+
const refs = getReferencesAtPosition(_('/comp.ts'), cursor)!;
741+
// 4 references are: class declaration, template usage, app import and use in declarations
742+
// list.
743+
expect(refs.length).toBe(4);
744+
assertTextSpans(refs, ['<my-comp>', 'MyComp']);
745+
assertFileNames(refs, ['app.ts', 'comp.ts']);
746+
});
747+
748+
it('gets works when cursor is on element tag', () => {
749+
const compFile = `
750+
import {Component} from '@angular/core';
751+
752+
@Component({selector: 'my-comp', template: ''})
753+
export class MyComp {}`;
754+
const {text, cursor} = extractCursorInfo(`
755+
import {Component, NgModule} from '@angular/core';
756+
import {MyComp} from './comp';
757+
758+
@Component({template: '<my-c¦omp></my-comp>'})
759+
export class AppCmp {
760+
}
761+
762+
@NgModule({declarations: [AppCmp, MyComp]})
763+
export class AppModule {}
764+
`);
765+
env = LanguageServiceTestEnvironment.setup([
766+
{name: _('/app.ts'), contents: text, isRoot: true},
767+
{name: _('/comp.ts'), contents: compFile},
768+
]);
769+
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
770+
// 4 references are: class declaration, template usage, app import and use in declarations
771+
// list.
772+
expect(refs.length).toBe(4);
773+
assertTextSpans(refs, ['<my-comp>', 'MyComp']);
774+
assertFileNames(refs, ['app.ts', 'comp.ts']);
775+
});
682776
});
683777

684778
function getReferencesAtPosition(fileName: string, position: number) {

0 commit comments

Comments
 (0)