Skip to content

Commit

Permalink
fix(language-service): Support 'find references' for two-way bindings (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
atscott committed Jan 7, 2021
1 parent a9d8c22 commit ebb7ac5
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 59 deletions.
138 changes: 79 additions & 59 deletions packages/language-service/ivy/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,72 +39,92 @@ export class ReferenceBuilder {
return undefined;
}

const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] :
positionDetails.context.node;

// Get the information about the TCB at the template position.
const symbol = this.ttc.getSymbolOfNode(node, component);
if (symbol === null) {
return undefined;
}
switch (symbol.kind) {
case SymbolKind.Directive:
case SymbolKind.Template:
// References to elements, templates, and directives will be through template references
// (#ref). They shouldn't be used directly for a Language Service reference request.
return undefined;
case SymbolKind.Element: {
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
return this.getReferencesForDirectives(matches);
const nodes = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes :
[positionDetails.context.node];

const references: ts.ReferenceEntry[] = [];
for (const node of nodes) {
// Get the information about the TCB at the template position.
const symbol = this.ttc.getSymbolOfNode(node, component);
if (symbol === null) {
continue;
}
case SymbolKind.DomBinding: {
// 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,
// but we can still see if they match to a directive.
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
return undefined;

switch (symbol.kind) {
case SymbolKind.Directive:
case SymbolKind.Template:
// References to elements, templates, and directives will be through template references
// (#ref). They shouldn't be used directly for a Language Service reference request.
break;
case SymbolKind.Element: {
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
references.push(...this.getReferencesForDirectives(matches) ?? []);
break;
}
const directives = getDirectiveMatchesForAttribute(
node.name, symbol.host.templateNode, symbol.host.directives);
return this.getReferencesForDirectives(directives);
}
case SymbolKind.Reference: {
const {shimPath, positionInShimFile} = symbol.referenceVarLocation;
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
}
case SymbolKind.Variable: {
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
const localVarPosition = symbol.localVarLocation.positionInShimFile;

if ((node instanceof TmplAstVariable)) {
if (node.valueSpan !== undefined && isWithin(position, node.valueSpan)) {
// In the valueSpan of the variable, we want to get the reference of the initializer.
return this.getReferencesAtTypescriptPosition(shimPath, initializerPosition);
} else if (isWithin(position, node.keySpan)) {
// In the keySpan of the variable, we want to get the reference of the local variable.
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition);
} else {
return undefined;
case SymbolKind.DomBinding: {
// 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, but we can still see if they match to a directive.
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
break;
}
const directives = getDirectiveMatchesForAttribute(
node.name, symbol.host.templateNode, symbol.host.directives);
references.push(...this.getReferencesForDirectives(directives) ?? []);
break;
}
case SymbolKind.Reference: {
const {shimPath, positionInShimFile} = symbol.referenceVarLocation;
references.push(
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
break;
}
case SymbolKind.Variable: {
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
const localVarPosition = symbol.localVarLocation.positionInShimFile;

if ((node instanceof TmplAstVariable)) {
if (node.valueSpan !== undefined && isWithin(position, node.valueSpan)) {
// In the valueSpan of the variable, we want to get the reference of the initializer.
references.push(
...this.getReferencesAtTypescriptPosition(shimPath, initializerPosition) ?? []);
} else if (isWithin(position, node.keySpan)) {
// In the keySpan of the variable, we want to get the reference of the local variable.
references.push(
...this.getReferencesAtTypescriptPosition(shimPath, localVarPosition) ?? []);
}
} else {
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the variable
// somewhere in the template.
references.push(
...this.getReferencesAtTypescriptPosition(shimPath, localVarPosition) ?? []);
}

// If the templateNode is not the `TmplAstVariable`, it must be a usage of the variable
// somewhere in the template.
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition);
}
case SymbolKind.Input:
case SymbolKind.Output: {
// TODO(atscott): Determine how to handle when the binding maps to several inputs/outputs
const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation;
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
}
case SymbolKind.Pipe:
case SymbolKind.Expression: {
const {shimPath, positionInShimFile} = symbol.shimLocation;
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
break;
}
case SymbolKind.Input:
case SymbolKind.Output: {
// TODO(atscott): Determine how to handle when the binding maps to several inputs/outputs
const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation;
references.push(
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
break;
}
case SymbolKind.Pipe:
case SymbolKind.Expression: {
const {shimPath, positionInShimFile} = symbol.shimLocation;
references.push(
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
break;
}
}
}
if (references.length === 0) {
return undefined;
}

return references;
}

private getReferencesForDirectives(directives: Set<DirectiveSymbol>):
Expand Down
31 changes: 31 additions & 0 deletions packages/language-service/ivy/test/references_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
it('works for directive classes', () => {
const {text, cursor} = extractCursorInfo(`
Expand Down

0 comments on commit ebb7ac5

Please sign in to comment.