refactor(language-service): add context to template target system (#40032)

This commit extends the template targeting system, which determines the node
being referenced given a template position, to return additional context if
needed about the particular aspect of the node to which the position refers.
For example, a position pointing to an element node may be pointing either
to its tag name or to somewhere in the node body. This is the difference
between `<div|>` and `<div foo | bar>`.

PR Close #40032
This commit is contained in:
Alex Rickabaugh 2020-11-18 14:07:30 -08:00
parent 524d581dfd
commit ccaf48de8f
6 changed files with 266 additions and 89 deletions

View File

@ -38,7 +38,7 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
private readonly component: ts.ClassDeclaration, private readonly node: N,
private readonly nodeParent: TmplAstNode|AST|null,
private readonly context: TmplAstTemplate|null) {}
private readonly template: TmplAstTemplate|null) {}
/**
* Analogue for `ts.LanguageService.getCompletionsAtPosition`.
@ -185,7 +185,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
this: PropertyExpressionCompletionBuilder,
options: ts.GetCompletionsAtPositionOptions|
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component);
const completions =
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
if (completions === null) {
return undefined;
}
@ -248,7 +249,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
this: PropertyExpressionCompletionBuilder, entryName: string,
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component);
const completions =
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
if (completions === null) {
return undefined;
}
@ -288,7 +290,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
*/
private getGlobalPropertyExpressionCompletionSymbol(
this: PropertyExpressionCompletionBuilder, entryName: string): ts.Symbol|undefined {
const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component);
const completions =
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
if (completions === null) {
return undefined;
}

View File

@ -226,13 +226,14 @@ export class DefinitionBuilder {
if (target === null) {
return undefined;
}
const {node, parent} = target;
const {nodeInContext, parent} = target;
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
const symbol =
this.compiler.getTemplateTypeChecker().getSymbolOfNode(nodeInContext.node, component);
if (symbol === null) {
return undefined;
}
return {node, parent, symbol};
return {node: nodeInContext.node, parent, symbol};
}
}

View File

@ -105,7 +105,8 @@ export class LanguageService {
return undefined;
}
const results =
new QuickInfoBuilder(this.tsLS, compiler, templateInfo.component, positionDetails.node)
new QuickInfoBuilder(
this.tsLS, compiler, templateInfo.component, positionDetails.nodeInContext.node)
.get();
this.compilerFactory.registerLastKnownProgram();
return results;
@ -131,8 +132,8 @@ export class LanguageService {
return null;
}
return new CompletionBuilder(
this.tsLS, compiler, templateInfo.component, positionDetails.node, positionDetails.parent,
positionDetails.context);
this.tsLS, compiler, templateInfo.component, positionDetails.nodeInContext.node,
positionDetails.parent, positionDetails.template);
}
getCompletionsAtPosition(

View File

@ -37,8 +37,10 @@ export class ReferenceBuilder {
return undefined;
}
const node = positionDetails.nodeInContext.node;
// Get the information about the TCB at the template position.
const symbol = this.ttc.getSymbolOfNode(positionDetails.node, component);
const symbol = this.ttc.getSymbolOfNode(node, component);
if (symbol === null) {
return undefined;
}
@ -56,12 +58,11 @@ export class ReferenceBuilder {
// 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.
if (!(positionDetails.node instanceof TmplAstTextAttribute) &&
!(positionDetails.node instanceof TmplAstBoundAttribute)) {
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
return undefined;
}
const directives = getDirectiveMatchesForAttribute(
positionDetails.node.name, symbol.host.templateNode, symbol.host.directives);
node.name, symbol.host.templateNode, symbol.host.directives);
return this.getReferencesForDirectives(directives);
}
case SymbolKind.Reference: {
@ -71,7 +72,7 @@ export class ReferenceBuilder {
case SymbolKind.Variable: {
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
const localVarPosition = symbol.localVarLocation.positionInShimFile;
const templateNode = positionDetails.node;
const templateNode = positionDetails.nodeInContext.node;
if ((templateNode instanceof TmplAstVariable)) {
if (templateNode.valueSpan !== undefined && isWithin(position, templateNode.valueSpan)) {

View File

@ -23,13 +23,13 @@ export interface TemplateTarget {
/**
* The template node (or AST expression) closest to the search position.
*/
node: t.Node|e.AST;
nodeInContext: TargetNode;
/**
* The `t.Template` which contains the found node or expression (or `null` if in the root
* template).
*/
context: t.Template|null;
template: t.Template|null;
/**
* The immediate parent node of the targeted node.
@ -37,6 +37,60 @@ export interface TemplateTarget {
parent: t.Node|e.AST|null;
}
/**
* A node targeted at a given position in the template, including potential contextual information
* about the specific aspect of the node being referenced.
*
* Some nodes have multiple interior contexts. For example, `t.Element` nodes have both a tag name
* as well as a body, and a given position definitively points to one or the other. `TargetNode`
* captures the node itself, as well as this additional contextual disambiguation.
*/
export type TargetNode = RawExpression|RawTemplateNode|ElementInBodyContext|ElementInTagContext;
/**
* Differentiates the various kinds of `TargetNode`s.
*/
export enum TargetNodeKind {
RawExpression,
RawTemplateNode,
ElementInTagContext,
ElementInBodyContext,
}
/**
* An `e.AST` expression that's targeted at a given position, with no additional context.
*/
export interface RawExpression {
kind: TargetNodeKind.RawExpression;
node: e.AST;
}
/**
* A `t.Node` template node that's targeted at a given position, with no additional context.
*/
export interface RawTemplateNode {
kind: TargetNodeKind.RawTemplateNode;
node: t.Node;
}
/**
* A `t.Element` (or `t.Template`) element node that's targeted, where the given position is within
* the tag name.
*/
export interface ElementInTagContext {
kind: TargetNodeKind.ElementInTagContext;
node: t.Element|t.Template;
}
/**
* A `t.Element` (or `t.Template`) element node that's targeted, where the given position is within
* the element body.
*/
export interface ElementInBodyContext {
kind: TargetNodeKind.ElementInBodyContext;
node: t.Element|t.Template;
}
/**
* Return the template AST node or expression AST node that most accurately
* represents the node at the specified cursor `position`.
@ -77,7 +131,40 @@ export function getTargetAtPosition(template: t.Node[], position: number): Templ
parent = path[path.length - 2];
}
return {position, node: candidate, context, parent};
// Given the candidate node, determine the full targeted context.
let nodeInContext: TargetNode;
if (candidate instanceof e.AST) {
nodeInContext = {
kind: TargetNodeKind.RawExpression,
node: candidate,
};
} else if (candidate instanceof t.Element) {
// Elements have two contexts: the tag context (position is within the element tag) or the
// element body context (position is outside of the tag name, but still in the element).
// Calculate the end of the element tag name. Any position beyond this is in the element body.
const tagEndPos =
candidate.sourceSpan.start.offset + 1 /* '<' element open */ + candidate.name.length;
if (position > tagEndPos) {
// Position is within the element body
nodeInContext = {
kind: TargetNodeKind.ElementInBodyContext,
node: candidate,
};
} else {
nodeInContext = {
kind: TargetNodeKind.ElementInTagContext,
node: candidate,
};
}
} else {
nodeInContext = {
kind: TargetNodeKind.RawTemplateNode,
node: candidate,
};
}
return {position, nodeInContext, template: context, parent};
}
/**

View File

@ -10,7 +10,7 @@ import {ParseError, parseTemplate} from '@angular/compiler';
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
import {getTargetAtPosition} from '../../template_target';
import {getTargetAtPosition, TargetNodeKind} from '../../template_target';
import {isExpressionNode, isTemplateNode} from '../../utils';
interface ParseResult {
@ -36,7 +36,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate element in opening tag', () => {
const {errors, nodes, position} = parse(`<di¦v></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
@ -44,7 +45,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate element in closing tag', () => {
const {errors, nodes, position} = parse(`<div></di¦v>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
@ -52,7 +54,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate element when cursor is at the beginning', () => {
const {errors, nodes, position} = parse(`<¦div></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
@ -60,7 +63,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate element when cursor is at the end', () => {
const {errors, nodes, position} = parse(`<div¦></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
@ -68,7 +72,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate attribute key', () => {
const {errors, nodes, position} = parse(`<div cla¦ss="foo"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
@ -76,7 +81,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate attribute value', () => {
const {errors, nodes, position} = parse(`<div class="fo¦o"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(node).toBeInstanceOf(t.TextAttribute);
@ -85,7 +91,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate bound attribute key', () => {
const {errors, nodes, position} = parse(`<test-cmp [fo¦o]="bar"></test-cmp>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
});
@ -93,7 +100,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate bound attribute value', () => {
const {errors, nodes, position} = parse(`<test-cmp [foo]="b¦ar"></test-cmp>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
@ -108,7 +116,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate bound event key', () => {
const {errors, nodes, position} = parse(`<test-cmp (fo¦o)="bar()"></test-cmp>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundEvent);
});
@ -116,7 +125,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate bound event value', () => {
const {errors, nodes, position} = parse(`<test-cmp (foo)="b¦ar()"></test-cmp>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.MethodCall);
});
@ -124,7 +134,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate element children', () => {
const {errors, nodes, position} = parse(`<div><sp¦an></span></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
expect((node as t.Element).name).toBe('span');
@ -133,7 +144,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate element reference', () => {
const {errors, nodes, position} = parse(`<div #my¦div></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
});
@ -141,7 +153,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template text attribute', () => {
const {errors, nodes, position} = parse(`<ng-template ng¦If></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
@ -149,7 +162,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template bound attribute key', () => {
const {errors, nodes, position} = parse(`<ng-template [ng¦If]="foo"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
});
@ -157,7 +171,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template bound attribute value', () => {
const {errors, nodes, position} = parse(`<ng-template [ngIf]="f¦oo"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
@ -165,7 +180,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template bound attribute key in two-way binding', () => {
const {errors, nodes, position} = parse(`<ng-template [(f¦oo)]="bar"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('foo');
@ -174,7 +190,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template bound attribute value in two-way binding', () => {
const {errors, nodes, position} = parse(`<ng-template [(foo)]="b¦ar"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('bar');
@ -183,7 +200,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template bound event key', () => {
const {errors, nodes, position} = parse(`<ng-template (cl¦ick)="foo()"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundEvent);
});
@ -191,14 +209,16 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template bound event value', () => {
const {errors, nodes, position} = parse(`<ng-template (click)="f¦oo()"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(node).toBeInstanceOf(e.MethodCall);
});
it('should locate template attribute key', () => {
const {errors, nodes, position} = parse(`<ng-template i¦d="foo"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
@ -206,7 +226,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template attribute value', () => {
const {errors, nodes, position} = parse(`<ng-template id="f¦oo"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(node).toBeInstanceOf(t.TextAttribute);
@ -215,7 +236,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template reference key via the # notation', () => {
const {errors, nodes, position} = parse(`<ng-template #f¦oo></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).name).toBe('foo');
@ -224,7 +246,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template reference key via the ref- notation', () => {
const {errors, nodes, position} = parse(`<ng-template ref-fo¦o></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).name).toBe('foo');
@ -233,7 +256,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template reference value via the # notation', () => {
const {errors, nodes, position} = parse(`<ng-template #foo="export¦As"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).value).toBe('exportAs');
@ -243,7 +267,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template reference value via the ref- notation', () => {
const {errors, nodes, position} = parse(`<ng-template ref-foo="export¦As"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
expect((node as t.Reference).value).toBe('exportAs');
@ -253,7 +278,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template variable key', () => {
const {errors, nodes, position} = parse(`<ng-template let-f¦oo="bar"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
});
@ -261,7 +287,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template variable value', () => {
const {errors, nodes, position} = parse(`<ng-template let-foo="b¦ar"></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
});
@ -269,7 +296,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate template children', () => {
const {errors, nodes, position} = parse(`<ng-template><d¦iv></div></ng-template>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
});
@ -277,7 +305,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate ng-content', () => {
const {errors, nodes, position} = parse(`<ng-co¦ntent></ng-content>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Content);
});
@ -285,7 +314,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate ng-content attribute key', () => {
const {errors, nodes, position} = parse('<ng-content cla¦ss="red"></ng-content>');
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
});
@ -293,7 +323,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate ng-content attribute value', () => {
const {errors, nodes, position} = parse('<ng-content class="r¦ed"></ng-content>');
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
@ -302,7 +333,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should not locate implicit receiver', () => {
const {errors, nodes, position} = parse(`<div [foo]="¦bar"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
@ -310,7 +342,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate bound attribute key in two-way binding', () => {
const {errors, nodes, position} = parse(`<cmp [(f¦oo)]="bar"></cmp>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('foo');
@ -319,7 +352,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate bound attribute value in two-way binding', () => {
const {errors, nodes, position} = parse(`<cmp [(foo)]="b¦ar"></cmp>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('bar');
@ -328,7 +362,8 @@ describe('getTargetAtPosition for template AST', () => {
it('should locate switch value in ICUs', () => {
const {errors, nodes, position} = parse(`<span i18n>{sw¦itch, plural, other {text}}"></span>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('switch');
@ -338,7 +373,8 @@ describe('getTargetAtPosition for template AST', () => {
const {errors, nodes, position} = parse(
`<span i18n>{expr, plural, other { {ne¦sted, plural, =1 { {{nestedInterpolation}} }} }}"></span>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('nested');
@ -348,7 +384,8 @@ describe('getTargetAtPosition for template AST', () => {
const {errors, nodes, position} =
parse(`<span i18n>{expr, plural, other { {{ i¦nterpolation }} }}"></span>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('interpolation');
@ -358,7 +395,8 @@ describe('getTargetAtPosition for template AST', () => {
const {errors, nodes, position} = parse(
`<span i18n>{expr, plural, other { {nested, plural, =1 { {{n¦estedInterpolation}} }} }}"></span>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('nestedInterpolation');
@ -369,7 +407,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should not locate implicit receiver', () => {
const {errors, nodes, position} = parse(`{{ ¦title }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('title');
@ -378,7 +417,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate property read', () => {
const {errors, nodes, position} = parse(`{{ ti¦tle }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('title');
@ -387,7 +427,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate safe property read', () => {
const {errors, nodes, position} = parse(`{{ foo?¦.bar }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.SafePropertyRead);
expect((node as e.SafePropertyRead).name).toBe('bar');
@ -396,7 +437,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate keyed read', () => {
const {errors, nodes, position} = parse(`{{ foo['bar']¦ }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.KeyedRead);
});
@ -404,7 +446,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate property write', () => {
const {errors, nodes, position} = parse(`<div (foo)="b¦ar=$event"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyWrite);
});
@ -412,7 +455,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate keyed write', () => {
const {errors, nodes, position} = parse(`<div (foo)="bar['baz']¦=$event"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.KeyedWrite);
});
@ -420,7 +464,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate binary', () => {
const {errors, nodes, position} = parse(`{{ 1 +¦ 2 }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Binary);
});
@ -428,7 +473,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate binding pipe with an identifier', () => {
const {errors, nodes, position} = parse(`{{ title | p¦ }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.BindingPipe);
});
@ -439,7 +485,8 @@ describe('getTargetAtPosition for expression AST', () => {
expect(errors![0].toString())
.toContain(
'Unexpected end of input, expected identifier or keyword at the end of the expression');
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
// TODO: We want this to be a BindingPipe.
expect(node).toBeInstanceOf(e.Interpolation);
@ -451,7 +498,7 @@ describe('getTargetAtPosition for expression AST', () => {
// parser throws an error. This case is important for autocomplete.
// const {errors, nodes, position} = parse(`{{ title | ¦ }}`);
// expect(errors).toBe(null);
// const {node} = findNodeAtPosition(nodes, position)!;
// const {nodeInContext} = findNodeAtPosition(nodes, position)!;
// expect(isExpressionNode(node!)).toBe(true);
// expect(node).toBeInstanceOf(e.BindingPipe);
});
@ -460,7 +507,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate method call', () => {
const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.MethodCall);
});
@ -468,7 +516,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate safe method call', () => {
const {errors, nodes, position} = parse(`{{ title?.toString(¦) }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.SafeMethodCall);
});
@ -476,7 +525,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate literal primitive in interpolation', () => {
const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralPrimitive);
expect((node as e.LiteralPrimitive).value).toBe('t');
@ -485,7 +535,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate literal primitive in binding', () => {
const {errors, nodes, position} = parse(`<div [id]="'t¦'"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralPrimitive);
expect((node as e.LiteralPrimitive).value).toBe('t');
@ -494,7 +545,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate empty expression', () => {
const {errors, nodes, position} = parse(`<div [id]="¦"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.EmptyExpr);
});
@ -502,7 +554,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate literal array', () => {
const {errors, nodes, position} = parse(`{{ [1, 2,¦ 3] }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralArray);
});
@ -510,7 +563,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate literal map', () => {
const {errors, nodes, position} = parse(`{{ { hello:¦ "world" } }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralMap);
});
@ -518,7 +572,8 @@ describe('getTargetAtPosition for expression AST', () => {
it('should locate conditional', () => {
const {errors, nodes, position} = parse(`{{ cond ?¦ true : false }}`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Conditional);
});
@ -528,7 +583,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template key', () => {
const {errors, nodes, position} = parse(`<div *ng¦If="foo"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
});
@ -536,7 +592,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template value', () => {
const {errors, nodes, position} = parse(`<div *ngIf="f¦oo"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
@ -546,7 +603,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
// ngFor is a text attribute because the desugared form is
// <ng-template ngFor let-item [ngForOf]="items">
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBeTrue();
expect(node).toBeInstanceOf(t.TextAttribute);
expect((node as t.TextAttribute).name).toBe('ngFor');
@ -562,7 +620,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate let variable', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let i¦tem of items"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
expect((node as t.Variable).name).toBe('item');
@ -571,7 +630,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate bound attribute key', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item o¦f items"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('ngForOf');
@ -580,8 +640,9 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate bound attribute key when cursor is at the start', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item ¦of items"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
expect(isTemplateNode(node!)).toBe(true);
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const node = nodeInContext.node;
expect(isTemplateNode(node)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('ngForOf');
});
@ -590,7 +651,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
const {errors, nodes, position} =
parse(`<div *ngFor="let item of items; trac¦kBy: trackByFn"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('ngForTrackBy');
@ -603,7 +665,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
const {errors, nodes, position} =
parse(`<div *ngFor="let item o¦f items; trackBy: trackByFn"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
expect((node as t.BoundAttribute).name).toBe('ngForOf');
@ -612,7 +675,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate bound attribute value', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of it¦ems"></div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
expect((node as e.PropertyRead).name).toBe('items');
@ -621,7 +685,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template children', () => {
const {errors, nodes, position} = parse(`<di¦v *ngIf></div>`);
expect(errors).toBe(null);
const {node, context} = getTargetAtPosition(nodes, position)!;
const {nodeInContext, template: context} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
expect((node as t.Element).name).toBe('div');
@ -634,7 +699,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
{{ i¦ }}
</div>`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});
@ -642,7 +708,8 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate LHS of variable declaration', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i¦=index">`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
// TODO: Currently there is no way to distinguish LHS from RHS
@ -652,10 +719,27 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate RHS of variable declaration', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i=in¦dex">`);
expect(errors).toBe(null);
const {node} = getTargetAtPosition(nodes, position)!;
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
const {node} = nodeInContext;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
// TODO: Currently there is no way to distinguish LHS from RHS
expect((node as t.Variable).value).toBe('index');
});
it('should locate an element in its tag context', () => {
const {errors, nodes, position} = parse(`<div¦ attr></div>`);
expect(errors).toBe(null);
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
expect(nodeInContext.kind).toBe(TargetNodeKind.ElementInTagContext);
expect(nodeInContext.node).toBeInstanceOf(t.Element);
});
it('should locate an element in its body context', () => {
const {errors, nodes, position} = parse(`<div ¦ attr></div>`);
expect(errors).toBe(null);
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
expect(nodeInContext.kind).toBe(TargetNodeKind.ElementInBodyContext);
expect(nodeInContext.node).toBeInstanceOf(t.Element);
});
});