From ccaf48de8f7f55785645296ffd0073569d9a5071 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 18 Nov 2020 14:07:30 -0800 Subject: [PATCH] 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 `` and `
`. PR Close #40032 --- packages/language-service/ivy/completions.ts | 11 +- packages/language-service/ivy/definitions.ts | 7 +- .../language-service/ivy/language_service.ts | 7 +- packages/language-service/ivy/references.ts | 11 +- .../language-service/ivy/template_target.ts | 93 ++++++- .../ivy/test/legacy/template_target_spec.ts | 226 ++++++++++++------ 6 files changed, 266 insertions(+), 89 deletions(-) diff --git a/packages/language-service/ivy/completions.ts b/packages/language-service/ivy/completions.ts index 62299420aa..a06493bd2f 100644 --- a/packages/language-service/ivy/completions.ts +++ b/packages/language-service/ivy/completions.ts @@ -38,7 +38,7 @@ export class CompletionBuilder { 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 { this: PropertyExpressionCompletionBuilder, options: ts.GetCompletionsAtPositionOptions| undefined): ts.WithMetadata|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 { 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 { */ 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; } diff --git a/packages/language-service/ivy/definitions.ts b/packages/language-service/ivy/definitions.ts index ca8861e1d7..e6f6667386 100644 --- a/packages/language-service/ivy/definitions.ts +++ b/packages/language-service/ivy/definitions.ts @@ -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}; } } diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 04deedfda1..6e01c8ea04 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -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( diff --git a/packages/language-service/ivy/references.ts b/packages/language-service/ivy/references.ts index 661a77c2d7..605cd7b767 100644 --- a/packages/language-service/ivy/references.ts +++ b/packages/language-service/ivy/references.ts @@ -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)) { diff --git a/packages/language-service/ivy/template_target.ts b/packages/language-service/ivy/template_target.ts index 4a18374a3b..4209cd00c7 100644 --- a/packages/language-service/ivy/template_target.ts +++ b/packages/language-service/ivy/template_target.ts @@ -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}; } /** 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 a2ac29fa81..9fe85fcbb0 100644 --- a/packages/language-service/ivy/test/legacy/template_target_spec.ts +++ b/packages/language-service/ivy/test/legacy/template_target_spec.ts @@ -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(`
`); 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(`
`); 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>
`); 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(``); 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(`
`); 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(`
`); 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(``); 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(``); 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(``); 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(``); 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(`
`); 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(`
`); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(``); 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(''); 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(''); 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(`
`); 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(``); 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(``); 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(`{sw¦itch, plural, other {text}}">`); 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( `{expr, plural, other { {ne¦sted, plural, =1 { {{nestedInterpolation}} }} }}">`); 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(`{expr, plural, other { {{ i¦nterpolation }} }}">`); 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( `{expr, plural, other { {nested, plural, =1 { {{n¦estedInterpolation}} }} }}">`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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 // 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(``); 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¦ }} `); 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(`
`); 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(`
`); 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(`
`); + 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(`
`); + expect(errors).toBe(null); + const {nodeInContext} = getTargetAtPosition(nodes, position)!; + expect(nodeInContext.kind).toBe(TargetNodeKind.ElementInBodyContext); + expect(nodeInContext.node).toBeInstanceOf(t.Element); + }); });