diff --git a/packages/language-service/ivy/hybrid_visitor.ts b/packages/language-service/ivy/hybrid_visitor.ts
index 2f94eea3b4..1dec2486ad 100644
--- a/packages/language-service/ivy/hybrid_visitor.ts
+++ b/packages/language-service/ivy/hybrid_visitor.ts
@@ -19,7 +19,19 @@ import * as t from '@angular/compiler/src/render3/r3_ast'; // t for temp
export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AST|undefined {
const visitor = new R3Visitor(position);
visitor.visitAll(ast);
- return visitor.path[visitor.path.length - 1];
+ const candidate = visitor.path[visitor.path.length - 1];
+ if (!candidate) {
+ return;
+ }
+ if (isTemplateNodeWithKeyAndValue(candidate)) {
+ const {keySpan, valueSpan} = candidate;
+ // If cursor is within source span but not within key span or value span,
+ // do not return the node.
+ if (!isWithin(position, keySpan) && (valueSpan && !isWithin(position, valueSpan))) {
+ return;
+ }
+ }
+ return candidate;
}
class R3Visitor implements t.Visitor {
@@ -31,11 +43,6 @@ class R3Visitor implements t.Visitor {
constructor(private readonly position: number) {}
visit(node: t.Node) {
- if (node instanceof t.BoundAttribute) {
- node.visit(this);
- return;
- }
-
const {start, end} = getSpanIncludingEndTag(node);
if (isWithin(this.position, {start, end})) {
const length = end - start;
@@ -100,13 +107,8 @@ class R3Visitor implements t.Visitor {
}
visitBoundAttribute(attribute: t.BoundAttribute) {
- if (isWithin(this.position, attribute.keySpan)) {
- this.path.push(attribute);
- } else if (attribute.valueSpan && isWithin(this.position, attribute.valueSpan)) {
- this.path.push(attribute);
- const visitor = new ExpressionVisitor(this.position);
- visitor.visit(attribute.value, this.path);
- }
+ const visitor = new ExpressionVisitor(this.position);
+ visitor.visit(attribute.value, this.path);
}
visitBoundEvent(attribute: t.BoundEvent) {
@@ -166,6 +168,15 @@ export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
return node.sourceSpan instanceof ParseSourceSpan;
}
+interface NodeWithKeyAndValue extends t.Node {
+ keySpan: ParseSourceSpan;
+ valueSpan?: ParseSourceSpan;
+}
+
+export function isTemplateNodeWithKeyAndValue(node: t.Node|e.AST): node is NodeWithKeyAndValue {
+ return isTemplateNode(node) && node.hasOwnProperty('keySpan');
+}
+
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
return node instanceof e.AST;
}
diff --git a/packages/language-service/ivy/test/hybrid_visitor_spec.ts b/packages/language-service/ivy/test/hybrid_visitor_spec.ts
index 2bc2a1778e..36504d389b 100644
--- a/packages/language-service/ivy/test/hybrid_visitor_spec.ts
+++ b/packages/language-service/ivy/test/hybrid_visitor_spec.ts
@@ -97,6 +97,13 @@ describe('findNodeAtPosition for template AST', () => {
expect(node).toBeInstanceOf(e.PropertyRead);
});
+ it('should not locate bound attribute if cursor is between key and value', () => {
+ const {errors, nodes, position} = parse(``);
+ expect(errors).toBeNull();
+ const node = findNodeAtPosition(nodes, position);
+ expect(node).toBeUndefined();
+ });
+
it('should locate bound event key', () => {
const {errors, nodes, position} = parse(``);
expect(errors).toBe(null);