refactor(language-service): Construct proper template AST from HTML ast (#34764)

The Template AST that corresponds to a given HTML AST is not always
complete, and often has to be reconstructed. This commit refactors the
code to make it easier to adapt to multiple cases.

PR Close #34764
This commit is contained in:
Keen Yee Liau 2020-01-09 18:16:26 -08:00 committed by atscott
parent 23cbfa791c
commit 84c659e246
1 changed files with 53 additions and 49 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ReferenceAst, TagContentType, TemplateBinding, Text, getHtmlTagDefinition} from '@angular/compiler';
import {AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ReferenceAst, TagContentType, TemplateBinding, Text, getHtmlTagDefinition} from '@angular/compiler';
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
import {AstResult} from './common';
@ -169,19 +169,15 @@ export function getTemplateCompletions(
result = attributeCompletionsForElement(templateInfo, ast.name);
}
},
visitAttribute(ast) {
const bindParts = ast.name.match(BIND_NAME_REGEXP);
const isReference = bindParts && bindParts[ATTR.KW_REF_IDX] !== undefined;
if (!isReference &&
(!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan)))) {
// We are in the name of an attribute. Show attribute completions.
visitAttribute(ast: Attribute) {
// An attribute consists of two parts, LHS="RHS".
// Determine if completions are requested for LHS or RHS
if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
// RHS completion
result = attributeValueCompletions(templateInfo, path);
} else {
// LHS completion
result = attributeCompletions(templateInfo, path);
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
if (isReference) {
result = referenceAttributeValueCompletions(templateInfo, templatePosition, ast);
} else {
result = attributeValueCompletions(templateInfo, templatePosition, ast);
}
}
},
visitText(ast) {
@ -300,44 +296,52 @@ function attributeCompletionsForElement(
return results;
}
function attributeValueCompletions(
info: AstResult, position: number, attr: Attribute): ng.CompletionEntry[] {
const path = findTemplateAstAt(info.templateAst, position);
if (!path.tail) {
return [];
/**
* Provide completions to the RHS of an attribute, which is of the form
* LHS="RHS". The template path is computed from the specified `info` whereas
* the context is determined from the specified `htmlPath`.
* @param info Object that contains the template AST
* @param htmlPath Path to the HTML node
*/
function attributeValueCompletions(info: AstResult, htmlPath: HtmlAstPath): ng.CompletionEntry[] {
// Find the corresponding Template AST path.
const templatePath = findTemplateAstAt(info.templateAst, htmlPath.position);
const visitor = new ExpressionVisitor(info, htmlPath.position, () => {
const dinfo = diagnosticInfoFromTemplateInfo(info);
return getExpressionScope(dinfo, templatePath, false);
});
if (templatePath.tail instanceof AttrAst ||
templatePath.tail instanceof BoundElementPropertyAst ||
templatePath.tail instanceof BoundEventAst) {
templatePath.tail.visit(visitor, null);
return visitor.results;
}
// HtmlAst contains the `Attribute` node, however a corresponding attribute AST
// node may be missing from the TemplateAst if the compiler fails to parse it fully. In this case,
// manually append an `AttrAst` node to the path.
if (!(path.tail instanceof AttrAst) && !(path.tail instanceof BoundElementPropertyAst) &&
!(path.tail instanceof BoundEventAst)) {
// The sourceSpan of an AttrAst is the valueSpan of the HTML Attribute.
path.push(new AttrAst(attr.name, attr.value, attr.valueSpan !));
// In order to provide accurate attribute value completion, we need to know
// what the LHS is, and construct the proper AST if it is missing.
const htmlAttr = htmlPath.tail as Attribute;
const bindParts = htmlAttr.name.match(BIND_NAME_REGEXP);
if (bindParts && bindParts[ATTR.KW_REF_IDX] !== undefined) {
let refAst: ReferenceAst|undefined;
let elemAst: ElementAst|undefined;
if (templatePath.tail instanceof ReferenceAst) {
refAst = templatePath.tail;
const parent = templatePath.parentOf(refAst);
if (parent instanceof ElementAst) {
elemAst = parent;
}
} else if (templatePath.tail instanceof ElementAst) {
refAst = new ReferenceAst(htmlAttr.name, null !, htmlAttr.value, htmlAttr.valueSpan !);
elemAst = templatePath.tail;
}
if (refAst && elemAst) {
refAst.visit(visitor, elemAst);
}
} else {
// HtmlAst contains the `Attribute` node, however the corresponding `AttrAst`
// node is missing from the TemplateAst.
const attrAst = new AttrAst(htmlAttr.name, htmlAttr.value, htmlAttr.valueSpan !);
attrAst.visit(visitor, null);
}
const dinfo = diagnosticInfoFromTemplateInfo(info);
const visitor =
new ExpressionVisitor(info, position, () => getExpressionScope(dinfo, path, false));
path.tail.visit(visitor, null);
return visitor.results;
}
function referenceAttributeValueCompletions(
info: AstResult, position: number, attr: Attribute): ng.CompletionEntry[] {
const path = findTemplateAstAt(info.templateAst, position);
if (!path.tail) {
return [];
}
// When the template parser does not find a directive with matching "exportAs",
// the ReferenceAst will be ignored.
if (!(path.tail instanceof ReferenceAst)) {
// The sourceSpan of an ReferenceAst is the valueSpan of the HTML Attribute.
path.push(new ReferenceAst(attr.name, null !, attr.value, attr.valueSpan !));
}
const dinfo = diagnosticInfoFromTemplateInfo(info);
const visitor =
new ExpressionVisitor(info, position, () => getExpressionScope(dinfo, path, false));
path.tail.visit(visitor, path.parentOf(path.tail));
return visitor.results;
}