Skip to content

Commit eb5c20c

Browse files
ivanwonderatscott
authored andcommitted
fix(language-service): break the hover/definitions for two-way binding (#34564)
PR Close #34564
1 parent fe19327 commit eb5c20c

File tree

3 files changed

+74
-5
lines changed

3 files changed

+74
-5
lines changed

packages/language-service/src/locate_symbol.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, CompileTypeSummary, CssSelector, DirectiveAst, ElementAst, SelectorMatcher, TemplateAstPath, tokenReference} from '@angular/compiler';
9+
import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, CompileTypeSummary, CssSelector, DirectiveAst, ElementAst, RecursiveTemplateAstVisitor, SelectorMatcher, TemplateAst, TemplateAstPath, templateVisitAll, tokenReference} from '@angular/compiler';
1010

1111
import {AstResult} from './common';
1212
import {getExpressionScope} from './expression_diagnostics';
@@ -148,14 +148,31 @@ function findAttribute(info: AstResult, position: number): Attribute|undefined {
148148
return path.first(Attribute);
149149
}
150150

151+
function findParentOfDirectivePropertyAst(ast: TemplateAst[], binding: BoundDirectivePropertyAst) {
152+
let res: DirectiveAst|undefined;
153+
const visitor = new class extends RecursiveTemplateAstVisitor {
154+
visitDirective(ast: DirectiveAst) {
155+
const result = this.visitChildren(ast, visit => { visit(ast.inputs); });
156+
return result;
157+
}
158+
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any) {
159+
if (ast === binding) {
160+
res = context;
161+
}
162+
}
163+
};
164+
templateVisitAll(visitor, ast);
165+
return res;
166+
}
167+
151168
function findInputBinding(
152169
info: AstResult, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol|undefined {
153-
const directive = path.parentOf(path.tail);
154-
if (directive instanceof DirectiveAst) {
155-
const invertedInput = invertMap(directive.directive.inputs);
170+
const directiveAst = findParentOfDirectivePropertyAst(info.templateAst, binding);
171+
if (directiveAst) {
172+
const invertedInput = invertMap(directiveAst.directive.inputs);
156173
const fieldName = invertedInput[binding.templateName];
157174
if (fieldName) {
158-
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
175+
const classSymbol = info.template.query.getTypeSymbol(directiveAst.directive.type.reference);
159176
if (classSymbol) {
160177
return classSymbol.members().get(fieldName);
161178
}

packages/language-service/test/definitions_spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,42 @@ describe('definitions', () => {
291291
// Not asserting the textSpan of definition because it's external file
292292
});
293293

294+
it('should be able to find a two-way binding', () => {
295+
const fileName = mockHost.addCode(`
296+
@Component({
297+
template: '<test-comp string-model ~{start-my}[(«model»)]="test"~{end-my}></test-comp>'
298+
})
299+
export class MyComponent {
300+
test = "";
301+
}`);
302+
// Get the marker for «model» in the code added above.
303+
const marker = mockHost.getReferenceMarkerFor(fileName, 'model');
304+
305+
const result = ngService.getDefinitionAt(fileName, marker.start);
306+
expect(result).toBeDefined();
307+
const {textSpan, definitions} = result !;
308+
309+
// Get the marker for bounded text in the code added above
310+
const boundedText = mockHost.getLocationMarkerFor(fileName, 'my');
311+
expect(textSpan).toEqual(boundedText);
312+
313+
// There should be exactly 1 definition
314+
expect(definitions).toBeDefined();
315+
expect(definitions !.length).toBe(1);
316+
const def = definitions ![0];
317+
318+
const refFileName = '/app/parsing-cases.ts';
319+
expect(def.fileName).toBe(refFileName);
320+
expect(def.name).toBe('model');
321+
expect(def.kind).toBe('property');
322+
const content = mockHost.readFile(refFileName) !;
323+
const ref = `@Input() model: string = 'model';`;
324+
expect(def.textSpan).toEqual({
325+
start: content.indexOf(ref),
326+
length: ref.length,
327+
});
328+
});
329+
294330
it('should be able to find a template from a url', () => {
295331
const fileName = mockHost.addCode(`
296332
@Component({

packages/language-service/test/hover_spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,22 @@ describe('hover', () => {
162162
expect(toText(displayParts)).toBe('(property) NgIf<T>.ngIf: T');
163163
});
164164

165+
it('should be able to find a reference to a two-way binding', () => {
166+
const fileName = mockHost.addCode(`
167+
@Component({
168+
template: '<test-comp string-model «[(ᐱmodelᐱ)]="test"»></test-comp>'
169+
})
170+
export class MyComponent {
171+
test = "";
172+
}`);
173+
const marker = mockHost.getDefinitionMarkerFor(fileName, 'model');
174+
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
175+
expect(quickInfo).toBeTruthy();
176+
const {textSpan, displayParts} = quickInfo !;
177+
expect(textSpan).toEqual(marker);
178+
expect(toText(displayParts)).toBe('(property) StringModel.model: string');
179+
});
180+
165181
it('should be able to ignore a reference declaration', () => {
166182
const fileName = mockHost.addCode(`
167183
@Component({

0 commit comments

Comments
 (0)