2020-08-12 19:13:02 -04:00
|
|
|
/**
|
|
|
|
* @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 * 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
|
|
|
|
|
2020-11-19 16:31:34 -05:00
|
|
|
import {isTemplateNode, isTemplateNodeWithKeyAndValue, isWithin} from './utils';
|
2020-09-28 14:26:07 -04:00
|
|
|
|
2020-08-12 19:13:02 -04:00
|
|
|
/**
|
2020-10-13 13:28:15 -04:00
|
|
|
* Contextual information for a target position within the template.
|
|
|
|
*/
|
|
|
|
export interface TemplateTarget {
|
|
|
|
/**
|
|
|
|
* Target position within the template.
|
|
|
|
*/
|
|
|
|
position: number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The template node (or AST expression) closest to the search position.
|
|
|
|
*/
|
2020-11-18 17:07:30 -05:00
|
|
|
nodeInContext: TargetNode;
|
2020-10-13 13:28:15 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The `t.Template` which contains the found node or expression (or `null` if in the root
|
|
|
|
* template).
|
|
|
|
*/
|
2020-11-18 17:07:30 -05:00
|
|
|
template: t.Template|null;
|
2020-10-13 13:28:15 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The immediate parent node of the targeted node.
|
|
|
|
*/
|
|
|
|
parent: t.Node|e.AST|null;
|
|
|
|
}
|
|
|
|
|
2020-11-18 17:07:30 -05:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
/**
|
|
|
|
* Return the template AST node or expression AST node that most accurately
|
2020-08-12 19:13:02 -04:00
|
|
|
* represents the node at the specified cursor `position`.
|
2020-10-12 15:48:56 -04:00
|
|
|
*
|
2020-10-13 13:28:15 -04:00
|
|
|
* @param template AST tree of the template
|
|
|
|
* @param position target cursor position
|
2020-08-12 19:13:02 -04:00
|
|
|
*/
|
2020-10-13 13:28:15 -04:00
|
|
|
export function getTargetAtPosition(template: t.Node[], position: number): TemplateTarget|null {
|
|
|
|
const path = TemplateTargetVisitor.visitTemplate(template, position);
|
|
|
|
if (path.length === 0) {
|
|
|
|
return null;
|
2020-09-25 01:11:57 -04:00
|
|
|
}
|
2020-10-13 13:28:15 -04:00
|
|
|
|
|
|
|
const candidate = path[path.length - 1];
|
2020-09-25 01:11:57 -04:00
|
|
|
if (isTemplateNodeWithKeyAndValue(candidate)) {
|
|
|
|
const {keySpan, valueSpan} = candidate;
|
2020-09-30 11:54:04 -04:00
|
|
|
const isWithinKeyValue =
|
|
|
|
isWithin(position, keySpan) || (valueSpan && isWithin(position, valueSpan));
|
|
|
|
if (!isWithinKeyValue) {
|
|
|
|
// If cursor is within source span but not within key span or value span,
|
|
|
|
// do not return the node.
|
2020-10-13 13:28:15 -04:00
|
|
|
return null;
|
2020-09-25 01:11:57 -04:00
|
|
|
}
|
|
|
|
}
|
2020-10-12 15:48:56 -04:00
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
// Walk up the result nodes to find the nearest `t.Template` which contains the targeted node.
|
|
|
|
let context: t.Template|null = null;
|
|
|
|
for (let i = path.length - 2; i >= 0; i--) {
|
|
|
|
const node = path[i];
|
|
|
|
if (node instanceof t.Template) {
|
|
|
|
context = node;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let parent: t.Node|e.AST|null = null;
|
|
|
|
if (path.length >= 2) {
|
|
|
|
parent = path[path.length - 2];
|
2020-10-12 15:48:56 -04:00
|
|
|
}
|
2020-10-13 13:28:15 -04:00
|
|
|
|
2020-11-18 17:07:30 -05:00
|
|
|
// 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};
|
2020-08-12 19:13:02 -04:00
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
/**
|
|
|
|
* Visitor which, given a position and a template, identifies the node within the template at that
|
|
|
|
* position, as well as records the path of increasingly nested nodes that were traversed to reach
|
|
|
|
* that position.
|
|
|
|
*/
|
|
|
|
class TemplateTargetVisitor implements t.Visitor {
|
2020-08-12 19:13:02 -04:00
|
|
|
// 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<t.Node|e.AST> = [];
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
static visitTemplate(template: t.Node[], position: number): Array<t.Node|e.AST> {
|
|
|
|
const visitor = new TemplateTargetVisitor(position);
|
|
|
|
visitor.visitAll(template);
|
|
|
|
return visitor.path;
|
|
|
|
}
|
|
|
|
|
2020-08-12 19:13:02 -04:00
|
|
|
// Position must be absolute in the source file.
|
2020-10-13 13:28:15 -04:00
|
|
|
private constructor(private readonly position: number) {}
|
2020-08-12 19:13:02 -04:00
|
|
|
|
|
|
|
visit(node: t.Node) {
|
2020-12-09 12:39:33 -05:00
|
|
|
const last: t.Node|e.AST|undefined = this.path[this.path.length - 1];
|
|
|
|
if (last && isTemplateNodeWithKeyAndValue(last) && isWithin(this.position, last.keySpan)) {
|
|
|
|
// We've already identified that we are within a `keySpan` of a node.
|
|
|
|
// We should stop processing nodes at this point to prevent matching
|
|
|
|
// any other nodes. This can happen when the end span of a different node
|
|
|
|
// touches the start of the keySpan for the candidate node. Because
|
|
|
|
// our `isWithin` logic is inclusive on both ends, we can match both nodes.
|
|
|
|
return;
|
|
|
|
}
|
2020-08-12 19:13:02 -04:00
|
|
|
const {start, end} = getSpanIncludingEndTag(node);
|
2020-09-23 11:36:38 -04:00
|
|
|
if (isWithin(this.position, {start, end})) {
|
2020-08-12 19:13:02 -04:00
|
|
|
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);
|
2020-12-09 12:31:17 -05:00
|
|
|
const last: t.Node|e.AST|undefined = this.path[this.path.length - 1];
|
|
|
|
// If we get here and have not found a candidate node on the element itself, proceed with
|
|
|
|
// looking for a more specific node on the element children.
|
|
|
|
if (last === element) {
|
|
|
|
this.visitAll(element.children);
|
|
|
|
}
|
2020-08-12 19:13:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2020-12-09 12:31:17 -05:00
|
|
|
const last: t.Node|e.AST|undefined = this.path[this.path.length - 1];
|
|
|
|
// If we get here and have not found a candidate node on the template itself, proceed with
|
|
|
|
// looking for a more specific node on the template children.
|
|
|
|
if (last === template) {
|
|
|
|
this.visitAll(template.children);
|
|
|
|
}
|
2020-08-12 19:13:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2020-09-25 01:11:57 -04:00
|
|
|
const visitor = new ExpressionVisitor(this.position);
|
|
|
|
visitor.visit(attribute.value, this.path);
|
2020-08-12 19:13:02 -04:00
|
|
|
}
|
|
|
|
|
2020-09-25 18:34:03 -04:00
|
|
|
visitBoundEvent(event: t.BoundEvent) {
|
|
|
|
const isTwoWayBinding =
|
|
|
|
this.path.some(n => n instanceof t.BoundAttribute && event.name === n.name + 'Change');
|
|
|
|
if (isTwoWayBinding) {
|
|
|
|
// 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.
|
|
|
|
this.path.pop(); // remove bound event from the AST path
|
|
|
|
return;
|
|
|
|
}
|
2020-08-12 19:13:02 -04:00
|
|
|
const visitor = new ExpressionVisitor(this.position);
|
2020-09-25 18:34:03 -04:00
|
|
|
visitor.visit(event.handler, this.path);
|
2020-08-12 19:13:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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<t.Node|e.AST>) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
// The third condition is to account for the implicit receiver, which should
|
|
|
|
// not be visited.
|
2020-09-23 11:36:38 -04:00
|
|
|
if (isWithin(this.position, node.sourceSpan) && !(node instanceof e.ImplicitReceiver)) {
|
2020-08-12 19:13:02 -04:00
|
|
|
path.push(node);
|
|
|
|
node.visit(this, path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
// <my-component></my-comp¦onent> 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;
|
|
|
|
}
|