diff --git a/packages/language-service/ivy/BUILD.bazel b/packages/language-service/ivy/BUILD.bazel index 3519eedd136..e56ebed70bd 100644 --- a/packages/language-service/ivy/BUILD.bazel +++ b/packages/language-service/ivy/BUILD.bazel @@ -6,6 +6,7 @@ ts_library( name = "ivy", srcs = glob(["*.ts"]), deps = [ + "//packages/compiler", "//packages/compiler-cli", "//packages/compiler-cli/src/ngtsc/core", "//packages/compiler-cli/src/ngtsc/core:api", diff --git a/packages/language-service/ivy/hybrid_visitor.ts b/packages/language-service/ivy/hybrid_visitor.ts new file mode 100644 index 00000000000..234649533b5 --- /dev/null +++ b/packages/language-service/ivy/hybrid_visitor.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ParseSourceSpan} 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 + +/** + * Return the template AST node or expression AST node that most accurately + * represents the node at the specified cursor `position`. + * @param ast AST tree + * @param position cursor position + */ +export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AST|undefined { + const visitor = new R3Visitor(position); + visitor.visitAll(ast); + return visitor.path[visitor.path.length - 1]; +} + +class R3Visitor implements t.Visitor { + // We need to keep a path instead of the last node because we might need more + // context for the last node, for example what is the parent node? + readonly path: Array = []; + + // Position must be absolute in the source file. + constructor(private readonly position: number) {} + + visit(node: t.Node) { + const {start, end} = getSpanIncludingEndTag(node); + // Note both start and end are inclusive because we want to match conditions + // like ¦start and end¦ where ¦ is the cursor. + if (start <= this.position && this.position <= end) { + const length = end - start; + const last: t.Node|e.AST|undefined = this.path[this.path.length - 1]; + if (last) { + const {start, end} = isTemplateNode(last) ? getSpanIncludingEndTag(last) : last.sourceSpan; + const lastLength = end - start; + if (length > lastLength) { + // The current node has a span that is larger than the last node found + // so we do not descend into it. This typically means we have found + // a candidate in one of the root nodes so we do not need to visit + // other root nodes. + return; + } + } + if (node instanceof t.BoundEvent && + this.path.find((n => n instanceof t.BoundAttribute && node.name === n.name + 'Change'))) { + // For two-way binding aka banana-in-a-box, there are two matches: + // BoundAttribute and BoundEvent. Both have the same spans. We choose to + // return BoundAttribute because it matches the identifier name verbatim. + // TODO: For operations like go to definition, ideally we want to return + // both. + return; + } + this.path.push(node); + node.visit(this); + } + } + + visitElement(element: t.Element) { + this.visitAll(element.attributes); + this.visitAll(element.inputs); + this.visitAll(element.outputs); + this.visitAll(element.references); + this.visitAll(element.children); + } + + visitTemplate(template: t.Template) { + this.visitAll(template.attributes); + this.visitAll(template.inputs); + this.visitAll(template.outputs); + this.visitAll(template.templateAttrs); + this.visitAll(template.references); + this.visitAll(template.variables); + this.visitAll(template.children); + } + + visitContent(content: t.Content) { + t.visitAll(this, content.attributes); + } + + visitVariable(variable: t.Variable) { + // Variable has no template nodes or expression nodes. + } + + visitReference(reference: t.Reference) { + // Reference has no template nodes or expression nodes. + } + + visitTextAttribute(attribute: t.TextAttribute) { + // Text attribute has no template nodes or expression nodes. + } + + visitBoundAttribute(attribute: t.BoundAttribute) { + const visitor = new ExpressionVisitor(this.position); + visitor.visit(attribute.value, this.path); + } + + visitBoundEvent(attribute: t.BoundEvent) { + const visitor = new ExpressionVisitor(this.position); + visitor.visit(attribute.handler, this.path); + } + + visitText(text: t.Text) { + // Text has no template nodes or expression nodes. + } + + visitBoundText(text: t.BoundText) { + const visitor = new ExpressionVisitor(this.position); + visitor.visit(text.value, this.path); + } + + visitIcu(icu: t.Icu) { + for (const boundText of Object.values(icu.vars)) { + this.visit(boundText); + } + for (const boundTextOrText of Object.values(icu.placeholders)) { + this.visit(boundTextOrText); + } + } + + visitAll(nodes: t.Node[]) { + for (const node of nodes) { + this.visit(node); + } + } +} + +class ExpressionVisitor extends e.RecursiveAstVisitor { + // Position must be absolute in the source file. + constructor(private readonly position: number) { + super(); + } + + visit(node: e.AST, path: Array) { + if (node instanceof e.ASTWithSource) { + // In order to reduce noise, do not include `ASTWithSource` in the path. + // For the purpose of source spans, there is no difference between + // `ASTWithSource` and and underlying node that it wraps. + node = node.ast; + } + const {start, end} = node.sourceSpan; + // The third condition is to account for the implicit receiver, which should + // not be visited. + if (start <= this.position && this.position <= end && !(node instanceof e.ImplicitReceiver)) { + path.push(node); + node.visit(this, path); + } + } +} + +export function isTemplateNode(node: t.Node|e.AST): node is t.Node { + // Template node implements the Node interface so we cannot use instanceof. + return node.sourceSpan instanceof ParseSourceSpan; +} + +export function isExpressionNode(node: t.Node|e.AST): node is e.AST { + return node instanceof e.AST; +} + +function getSpanIncludingEndTag(ast: t.Node) { + const result = { + start: ast.sourceSpan.start.offset, + end: ast.sourceSpan.end.offset, + }; + // For Element and Template node, sourceSpan.end is the end of the opening + // tag. For the purpose of language service, we need to actually recognize + // the end of the closing tag. Otherwise, for situation like + // where the cursor is in the closing tag + // we will not be able to return any information. + if ((ast instanceof t.Element || ast instanceof t.Template) && ast.endSourceSpan) { + result.end = ast.endSourceSpan.end.offset; + } + return result; +} diff --git a/packages/language-service/ivy/test/BUILD.bazel b/packages/language-service/ivy/test/BUILD.bazel index 7d539e9e6b1..aaa729f2666 100644 --- a/packages/language-service/ivy/test/BUILD.bazel +++ b/packages/language-service/ivy/test/BUILD.bazel @@ -5,6 +5,7 @@ ts_library( testonly = True, srcs = glob(["*.ts"]), deps = [ + "//packages/compiler", "//packages/language-service/ivy", "@npm//typescript", ], diff --git a/packages/language-service/ivy/test/hybrid_visitor_spec.ts b/packages/language-service/ivy/test/hybrid_visitor_spec.ts new file mode 100644 index 00000000000..30eb4daf733 --- /dev/null +++ b/packages/language-service/ivy/test/hybrid_visitor_spec.ts @@ -0,0 +1,568 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +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 {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_visitor'; + +interface ParseResult { + nodes: t.Node[]; + errors?: ParseError[]; + position: number; +} + +function parse(template: string): ParseResult { + const position = template.indexOf('¦'); + if (position < 0) { + throw new Error(`Template "${template}" does not contain the cursor`); + } + template = template.replace('¦', ''); + const templateUrl = '/foo'; + return { + ...parseTemplate(template, templateUrl), + position, + }; +} + +describe('findNodeAtPosition for template AST', () => { + it('should locate element in opening tag', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + }); + + it('should locate element in closing tag', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + }); + + it('should locate element when cursor is at the beginning', () => { + const {errors, nodes, position} = parse(`<¦div>
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + }); + + it('should locate element when cursor is at the end', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + }); + + it('should locate attribute key', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should locate attribute value', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + // TODO: Note that we do not have the ability to detect the RHS (yet) + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should locate bound attribute key', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundAttribute); + }); + + it('should locate bound attribute value', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + }); + + it('should locate bound event key', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundEvent); + }); + + it('should locate bound event value', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.MethodCall); + }); + + it('should locate element children', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + expect((node as t.Element).name).toBe('span'); + }); + + it('should locate element reference', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Reference); + }); + + it('should locate template text attribute', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should locate template bound attribute key', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundAttribute); + }); + + it('should locate template bound attribute value', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + }); + + it('should locate template bound attribute key in two-way binding', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundAttribute); + expect((node as t.BoundAttribute).name).toBe('foo'); + }); + + it('should locate template bound attribute value in two-way binding', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + expect((node as e.PropertyRead).name).toBe('bar'); + }); + + it('should locate template bound event key', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundEvent); + }); + + it('should locate template bound event value', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(node).toBeInstanceOf(e.MethodCall); + }); + + it('should locate template attribute key', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should locate template attribute value', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + // TODO: Note that we do not have the ability to detect the RHS (yet) + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should locate template reference key via the # notation', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Reference); + expect((node as t.Reference).name).toBe('foo'); + }); + + it('should locate template reference key via the ref- notation', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Reference); + expect((node as t.Reference).name).toBe('foo'); + }); + + it('should locate template reference value via the # notation', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Reference); + expect((node as t.Reference).value).toBe('exportAs'); + // TODO: Note that we do not have the ability to distinguish LHS and RHS + }); + + it('should locate template reference value via the ref- notation', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Reference); + expect((node as t.Reference).value).toBe('exportAs'); + // TODO: Note that we do not have the ability to distinguish LHS and RHS + }); + + it('should locate template variable key', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Variable); + }); + + it('should locate template variable value', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Variable); + }); + + it('should locate template children', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + }); + + it('should locate ng-content', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Content); + }); + + it('should locate ng-content attribute key', () => { + const {errors, nodes, position} = parse(''); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should locate ng-content attribute value', () => { + const {errors, nodes, position} = parse(''); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + // TODO: Note that we do not have the ability to detect the RHS (yet) + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.TextAttribute); + }); + + it('should not locate implicit receiver', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + }); + + it('should locate bound attribute key in two-way binding', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundAttribute); + expect((node as t.BoundAttribute).name).toBe('foo'); + }); + + it('should locate bound attribute value in two-way binding', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + expect((node as e.PropertyRead).name).toBe('bar'); + }); +}); + +describe('findNodeAtPosition for expression AST', () => { + it('should not locate implicit receiver', () => { + const {errors, nodes, position} = parse(`{{ ¦title }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + expect((node as e.PropertyRead).name).toBe('title'); + }); + + it('should locate property read', () => { + const {errors, nodes, position} = parse(`{{ ti¦tle }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + expect((node as e.PropertyRead).name).toBe('title'); + }); + + it('should locate safe property read', () => { + const {errors, nodes, position} = parse(`{{ foo?¦.bar }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.SafePropertyRead); + expect((node as e.SafePropertyRead).name).toBe('bar'); + }); + + it('should locate keyed read', () => { + const {errors, nodes, position} = parse(`{{ foo['bar']¦ }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.KeyedRead); + }); + + it('should locate property write', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyWrite); + }); + + it('should locate keyed write', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.KeyedWrite); + }); + + it('should locate binary', () => { + const {errors, nodes, position} = parse(`{{ 1 +¦ 2 }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.Binary); + }); + + it('should locate binding pipe with an identifier', () => { + const {errors, nodes, position} = parse(`{{ title | p¦ }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.BindingPipe); + }); + + it('should locate binding pipe without identifier', + () => { + // TODO: We are not able to locate pipe if identifier is missing because the + // parser throws an error. This case is important for autocomplete. + // const {errors, nodes, position} = parse(`{{ title | ¦ }}`); + // expect(errors).toBeUndefined(); + // const node = findNodeAtPosition(nodes, position); + // expect(isExpressionNode(node!)).toBe(true); + // expect(node).toBeInstanceOf(e.BindingPipe); + }); + + it('should locate method call', () => { + const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.MethodCall); + }); + + it('should locate safe method call', () => { + const {errors, nodes, position} = parse(`{{ title?.toString(¦) }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.SafeMethodCall); + }); + + it('should locate literal primitive in interpolation', () => { + const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.LiteralPrimitive); + expect((node as e.LiteralPrimitive).value).toBe('t'); + }); + + it('should locate literal primitive in binding', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.LiteralPrimitive); + expect((node as e.LiteralPrimitive).value).toBe('t'); + }); + + it('should locate empty expression', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.EmptyExpr); + }); + + it('should locate literal array', () => { + const {errors, nodes, position} = parse(`{{ [1, 2,¦ 3] }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.LiteralArray); + }); + + it('should locate literal map', () => { + const {errors, nodes, position} = parse(`{{ { hello:¦ "world" } }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.LiteralMap); + }); + + it('should locate conditional', () => { + const {errors, nodes, position} = parse(`{{ cond ?¦ true : false }}`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.Conditional); + }); +}); + +describe('findNodeAtPosition for microsyntax expression', () => { + it('should locate template key', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundAttribute); + }); + + it('should locate template value', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + }); + + it('should locate text attribute', () => { + const {errors, nodes, position} = parse(`
`); + // ngFor is a text attribute because the desugared form is + // + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + // TODO: this is currently wrong because it should point to ngFor text + // attribute instead of ngForOf bound attribute + }); + + it('should locate not let keyword', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + // TODO: this is currently wrong because node is currently pointing to + // "item". In this case, it should return undefined. + }); + + it('should locate let variable', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Variable); + expect((node as t.Variable).name).toBe('item'); + }); + + it('should locate bound attribute key', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.BoundAttribute); + expect((node as t.BoundAttribute).name).toBe('ngForOf'); + }); + + it('should locate bound attribute value', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + expect((node as e.PropertyRead).name).toBe('items'); + }); + + it('should locate template children', () => { + const {errors, nodes, position} = parse(``); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isTemplateNode(node!)).toBe(true); + expect(node).toBeInstanceOf(t.Element); + expect((node as t.Element).name).toBe('div'); + }); + + it('should locate property read of variable declared within template', () => { + const {errors, nodes, position} = parse(` +
+ {{ i¦ }} +
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + expect(isExpressionNode(node!)).toBe(true); + expect(node).toBeInstanceOf(e.PropertyRead); + }); + + it('should locate LHS of variable declaration', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + 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).name).toBe('i'); + }); + + it('should locate RHS of variable declaration', () => { + const {errors, nodes, position} = parse(`
`); + expect(errors).toBeUndefined(); + const node = findNodeAtPosition(nodes, position); + 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'); + }); +});