diff --git a/packages/language-service/ivy/template_target.ts b/packages/language-service/ivy/template_target.ts index 09650a2df3..f4d760c639 100644 --- a/packages/language-service/ivy/template_target.ts +++ b/packages/language-service/ivy/template_target.ts @@ -49,8 +49,8 @@ export interface TemplateTarget { export type TargetContext = SingleNodeTarget|MultiNodeTarget; /** Contexts which logically target only a single node in the template AST. */ -export type SingleNodeTarget = RawExpression|RawTemplateNode|ElementInBodyContext| - ElementInTagContext|AttributeInKeyContext|AttributeInValueContext; +export type SingleNodeTarget = RawExpression|MethodCallExpressionInArgContext|RawTemplateNode| + ElementInBodyContext|ElementInTagContext|AttributeInKeyContext|AttributeInValueContext; /** * Contexts which logically target multiple nodes in the template AST, which cannot be @@ -65,6 +65,7 @@ export type MultiNodeTarget = TwoWayBindingContext; */ export enum TargetNodeKind { RawExpression, + MethodCallExpressionInArgContext, RawTemplateNode, ElementInTagContext, ElementInBodyContext, @@ -79,6 +80,21 @@ export enum TargetNodeKind { export interface RawExpression { kind: TargetNodeKind.RawExpression; node: e.AST; + parents: e.AST[]; +} + +/** + * An `e.MethodCall` or `e.SafeMethodCall` expression with the cursor in a position where an + * argument could appear. + * + * This is returned when the only matching node is the method call expression, but the cursor is + * within the method call parentheses. For example, in the expression `foo(|)` there is no argument + * expression that the cursor could be targeting, but the cursor is in a position where one could + * appear. + */ +export interface MethodCallExpressionInArgContext { + kind: TargetNodeKind.MethodCallExpressionInArgContext; + node: e.MethodCall|e.SafeMethodCall; } /** @@ -158,10 +174,21 @@ export function getTargetAtPosition(template: t.Node[], position: number): Templ // Given the candidate node, determine the full targeted context. let nodeInContext: TargetContext; - if (candidate instanceof e.AST) { + if ((candidate instanceof e.MethodCall || candidate instanceof e.SafeMethodCall) && + isWithin(position, candidate.argumentSpan)) { + nodeInContext = { + kind: TargetNodeKind.MethodCallExpressionInArgContext, + node: candidate, + }; + } else if (candidate instanceof e.AST) { + const parents = path.filter((value: e.AST|t.Node): value is e.AST => value instanceof e.AST); + // Remove the current node from the parents list. + parents.pop(); + nodeInContext = { kind: TargetNodeKind.RawExpression, node: candidate, + parents, }; } else if (candidate instanceof t.Element) { // Elements have two contexts: the tag context (position is within the element tag) or the diff --git a/packages/language-service/ivy/test/legacy/template_target_spec.ts b/packages/language-service/ivy/test/legacy/template_target_spec.ts index 01623e9d7c..3099c851a3 100644 --- a/packages/language-service/ivy/test/legacy/template_target_spec.ts +++ b/packages/language-service/ivy/test/legacy/template_target_spec.ts @@ -549,6 +549,15 @@ describe('getTargetAtPosition for expression AST', () => { expect(node).toBeInstanceOf(e.SafeMethodCall); }); + it('should identify when in the argument position in a no-arg method call', () => { + const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`); + expect(errors).toBe(null); + const {context} = getTargetAtPosition(nodes, position)!; + expect(context.kind).toEqual(TargetNodeKind.MethodCallExpressionInArgContext); + const {node} = context as SingleNodeTarget; + expect(node).toBeInstanceOf(e.MethodCall); + }); + it('should locate literal primitive in interpolation', () => { const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`); expect(errors).toBe(null);