refactor(language-service): specifically identify empty argument positions (#41581)

This commit changes `getTemplateAtTarget` to be able to identify when a
cursor position is specifically within the argument span of a `MethodCall`
or `SafeMethodCall` with no arguments. If the call had arguments, one of the
argument expressions would be returned instead, but in a call with no
arguments the tightest node _is_ the `MethodCall`. Adding the additional
argument context will allow for functionality that relies on tracking
argument positions, like `getSignatureHelpItems`.

PR Close #41581
This commit is contained in:
Alex Rickabaugh 2021-04-12 11:07:43 -04:00 committed by Zach Arend
parent e1a2930893
commit d85e74e05c
2 changed files with 39 additions and 3 deletions

View File

@ -49,8 +49,8 @@ export interface TemplateTarget {
export type TargetContext = SingleNodeTarget|MultiNodeTarget; export type TargetContext = SingleNodeTarget|MultiNodeTarget;
/** Contexts which logically target only a single node in the template AST. */ /** Contexts which logically target only a single node in the template AST. */
export type SingleNodeTarget = RawExpression|RawTemplateNode|ElementInBodyContext| export type SingleNodeTarget = RawExpression|MethodCallExpressionInArgContext|RawTemplateNode|
ElementInTagContext|AttributeInKeyContext|AttributeInValueContext; ElementInBodyContext|ElementInTagContext|AttributeInKeyContext|AttributeInValueContext;
/** /**
* Contexts which logically target multiple nodes in the template AST, which cannot be * Contexts which logically target multiple nodes in the template AST, which cannot be
@ -65,6 +65,7 @@ export type MultiNodeTarget = TwoWayBindingContext;
*/ */
export enum TargetNodeKind { export enum TargetNodeKind {
RawExpression, RawExpression,
MethodCallExpressionInArgContext,
RawTemplateNode, RawTemplateNode,
ElementInTagContext, ElementInTagContext,
ElementInBodyContext, ElementInBodyContext,
@ -79,6 +80,21 @@ export enum TargetNodeKind {
export interface RawExpression { export interface RawExpression {
kind: TargetNodeKind.RawExpression; kind: TargetNodeKind.RawExpression;
node: e.AST; 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. // Given the candidate node, determine the full targeted context.
let nodeInContext: TargetContext; 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 = { nodeInContext = {
kind: TargetNodeKind.RawExpression, kind: TargetNodeKind.RawExpression,
node: candidate, node: candidate,
parents,
}; };
} else if (candidate instanceof t.Element) { } else if (candidate instanceof t.Element) {
// Elements have two contexts: the tag context (position is within the element tag) or the // Elements have two contexts: the tag context (position is within the element tag) or the

View File

@ -549,6 +549,15 @@ describe('getTargetAtPosition for expression AST', () => {
expect(node).toBeInstanceOf(e.SafeMethodCall); 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', () => { it('should locate literal primitive in interpolation', () => {
const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`); const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`);
expect(errors).toBe(null); expect(errors).toBe(null);