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:
parent
a9d8c228d9
commit
ebb7ac5979
|
@ -39,39 +39,46 @@ export class ReferenceBuilder {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
|
||||
positionDetails.context.nodes[0] :
|
||||
positionDetails.context.node;
|
||||
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) {
|
||||
return undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
break;
|
||||
case SymbolKind.Element: {
|
||||
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
|
||||
return this.getReferencesForDirectives(matches);
|
||||
references.push(...this.getReferencesForDirectives(matches) ?? []);
|
||||
break;
|
||||
}
|
||||
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.
|
||||
// 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;
|
||||
break;
|
||||
}
|
||||
const directives = getDirectiveMatchesForAttribute(
|
||||
node.name, symbol.host.templateNode, symbol.host.directives);
|
||||
return this.getReferencesForDirectives(directives);
|
||||
references.push(...this.getReferencesForDirectives(directives) ?? []);
|
||||
break;
|
||||
}
|
||||
case SymbolKind.Reference: {
|
||||
const {shimPath, positionInShimFile} = symbol.referenceVarLocation;
|
||||
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
||||
references.push(
|
||||
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
|
||||
break;
|
||||
}
|
||||
case SymbolKind.Variable: {
|
||||
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
|
||||
|
@ -80,32 +87,45 @@ export class ReferenceBuilder {
|
|||
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);
|
||||
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.
|
||||
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition);
|
||||
references.push(
|
||||
...this.getReferencesAtTypescriptPosition(shimPath, localVarPosition) ?? []);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the variable
|
||||
// somewhere in the template.
|
||||
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition);
|
||||
references.push(
|
||||
...this.getReferencesAtTypescriptPosition(shimPath, localVarPosition) ?? []);
|
||||
}
|
||||
|
||||
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;
|
||||
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
||||
references.push(
|
||||
...this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile) ?? []);
|
||||
break;
|
||||
}
|
||||
case SymbolKind.Pipe:
|
||||
case SymbolKind.Expression: {
|
||||
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>):
|
||||
ts.ReferenceEntry[]|undefined {
|
||||
|
|
|
@ -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(`
|
||||
|
|
Loading…
Reference in New Issue