fix(language-service): Support 'find references' for two-way bindings (#40185)

Rather than expecting that a position in a template only targets a
single node, this commit simply adjusts the approach to account for two way
bindings. Specifically, we attempt to get references for each targeted
node and then return the combination of all results, or `undefined` if
none of the target nodes had references.

PR Close #40185
This commit is contained in:
Andrew Scott 2020-12-17 14:43:53 -08:00 committed by atscott
parent a9d8c228d9
commit ebb7ac5979
2 changed files with 113 additions and 62 deletions

View File

@ -39,39 +39,46 @@ export class ReferenceBuilder {
return undefined; return undefined;
} }
const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ? const nodes = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] : positionDetails.context.nodes :
positionDetails.context.node; [positionDetails.context.node];
const references: ts.ReferenceEntry[] = [];
for (const node of nodes) {
// Get the information about the TCB at the template position. // Get the information about the TCB at the template position.
const symbol = this.ttc.getSymbolOfNode(node, component); const symbol = this.ttc.getSymbolOfNode(node, component);
if (symbol === null) { if (symbol === null) {
return undefined; continue;
} }
switch (symbol.kind) { switch (symbol.kind) {
case SymbolKind.Directive: case SymbolKind.Directive:
case SymbolKind.Template: case SymbolKind.Template:
// References to elements, templates, and directives will be through template references // References to elements, templates, and directives will be through template references
// (#ref). They shouldn't be used directly for a Language Service reference request. // (#ref). They shouldn't be used directly for a Language Service reference request.
return undefined; break;
case SymbolKind.Element: { case SymbolKind.Element: {
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives); const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
return this.getReferencesForDirectives(matches); references.push(...this.getReferencesForDirectives(matches) ?? []);
break;
} }
case SymbolKind.DomBinding: { case SymbolKind.DomBinding: {
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't // Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
// have a shim location. This means we can't match dom bindings to their lib.dom reference, // have a shim location. This means we can't match dom bindings to their lib.dom
// but we can still see if they match to a directive. // reference, but we can still see if they match to a directive.
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) { if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
return undefined; break;
} }
const directives = getDirectiveMatchesForAttribute( const directives = getDirectiveMatchesForAttribute(
node.name, symbol.host.templateNode, symbol.host.directives); node.name, symbol.host.templateNode, symbol.host.directives);
return this.getReferencesForDirectives(directives); references.push(...this.getReferencesForDirectives(directives) ?? []);
break;
} }
case SymbolKind.Reference: { case SymbolKind.Reference: {
const {shimPath, positionInShimFile} = symbol.referenceVarLocation; const {shimPath, positionInShimFile} = symbol.referenceVarLocation;
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile); references.push(
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
break;
} }
case SymbolKind.Variable: { case SymbolKind.Variable: {
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation; const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
@ -80,32 +87,45 @@ export class ReferenceBuilder {
if ((node instanceof TmplAstVariable)) { if ((node instanceof TmplAstVariable)) {
if (node.valueSpan !== undefined && isWithin(position, node.valueSpan)) { if (node.valueSpan !== undefined && isWithin(position, node.valueSpan)) {
// In the valueSpan of the variable, we want to get the reference of the initializer. // In the valueSpan of the variable, we want to get the reference of the initializer.
return this.getReferencesAtTypescriptPosition(shimPath, initializerPosition); references.push(
...this.getReferencesAtTypescriptPosition(shimPath, initializerPosition) ?? []);
} else if (isWithin(position, node.keySpan)) { } else if (isWithin(position, node.keySpan)) {
// In the keySpan of the variable, we want to get the reference of the local variable. // In the keySpan of the variable, we want to get the reference of the local variable.
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition); references.push(
...this.getReferencesAtTypescriptPosition(shimPath, localVarPosition) ?? []);
}
} else { } else {
return undefined;
}
}
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the variable // If the templateNode is not the `TmplAstVariable`, it must be a usage of the variable
// somewhere in the template. // somewhere in the template.
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition); references.push(
...this.getReferencesAtTypescriptPosition(shimPath, localVarPosition) ?? []);
}
break;
} }
case SymbolKind.Input: case SymbolKind.Input:
case SymbolKind.Output: { case SymbolKind.Output: {
// TODO(atscott): Determine how to handle when the binding maps to several inputs/outputs // TODO(atscott): Determine how to handle when the binding maps to several inputs/outputs
const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation; const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation;
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile); references.push(
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
break;
} }
case SymbolKind.Pipe: case SymbolKind.Pipe:
case SymbolKind.Expression: { case SymbolKind.Expression: {
const {shimPath, positionInShimFile} = symbol.shimLocation; const {shimPath, positionInShimFile} = symbol.shimLocation;
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile); references.push(
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
break;
} }
} }
} }
if (references.length === 0) {
return undefined;
}
return references;
}
private getReferencesForDirectives(directives: Set<DirectiveSymbol>): private getReferencesForDirectives(directives: Set<DirectiveSymbol>):
ts.ReferenceEntry[]|undefined { ts.ReferenceEntry[]|undefined {

View File

@ -666,6 +666,37 @@ describe('find references', () => {
}); });
}); });
it('should get references to both input and output for two-way binding', () => {
const dirFile = {
name: _('/dir.ts'),
contents: `
import {Directive, Input, Output} from '@angular/core';
@Directive({selector: '[string-model]'})
export class StringModel {
@Input() model!: any;
@Output() modelChange!: any;
}`
};
const {text, cursor} = extractCursorInfo(`
import {Component} from '@angular/core';
@Component({template: '<div string-model [(mod¦el)]="title"></div>'})
export class AppCmp {
title = 'title';
}`);
const appFile = {name: _('/app.ts'), contents: text};
env = createModuleWithDeclarations([appFile, dirFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
// Note that this includes the 'model` twice from the template. As with other potential
// duplicates (like if another plugin returns the same span), we expect the LS clients to filter
// these out themselves.
expect(refs.length).toEqual(4);
assertFileNames(refs, ['dir.ts', 'app.ts']);
assertTextSpans(refs, ['model', 'modelChange']);
});
describe('directives', () => { describe('directives', () => {
it('works for directive classes', () => { it('works for directive classes', () => {
const {text, cursor} = extractCursorInfo(` const {text, cursor} = extractCursorInfo(`