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

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
import {AstResult} from './common'; import {AstResult} from './common';
@ -169,19 +169,15 @@ export function getTemplateCompletions(
result = attributeCompletionsForElement(templateInfo, ast.name); result = attributeCompletionsForElement(templateInfo, ast.name);
} }
}, },
visitAttribute(ast) { visitAttribute(ast: Attribute) {
const bindParts = ast.name.match(BIND_NAME_REGEXP); // An attribute consists of two parts, LHS="RHS".
const isReference = bindParts && bindParts[ATTR.KW_REF_IDX] !== undefined; // Determine if completions are requested for LHS or RHS
if (!isReference && if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
(!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan)))) { // RHS completion
// We are in the name of an attribute. Show attribute completions. result = attributeValueCompletions(templateInfo, path);
} else {
// LHS completion
result = attributeCompletions(templateInfo, path); 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) { visitText(ast) {
@ -300,44 +296,52 @@ function attributeCompletionsForElement(
return results; return results;
} }
function attributeValueCompletions( /**
info: AstResult, position: number, attr: Attribute): ng.CompletionEntry[] { * Provide completions to the RHS of an attribute, which is of the form
const path = findTemplateAstAt(info.templateAst, position); * LHS="RHS". The template path is computed from the specified `info` whereas
if (!path.tail) { * the context is determined from the specified `htmlPath`.
return []; * @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 // In order to provide accurate attribute value completion, we need to know
// node may be missing from the TemplateAst if the compiler fails to parse it fully. In this case, // what the LHS is, and construct the proper AST if it is missing.
// manually append an `AttrAst` node to the path. const htmlAttr = htmlPath.tail as Attribute;
if (!(path.tail instanceof AttrAst) && !(path.tail instanceof BoundElementPropertyAst) && const bindParts = htmlAttr.name.match(BIND_NAME_REGEXP);
!(path.tail instanceof BoundEventAst)) { if (bindParts && bindParts[ATTR.KW_REF_IDX] !== undefined) {
// The sourceSpan of an AttrAst is the valueSpan of the HTML Attribute. let refAst: ReferenceAst|undefined;
path.push(new AttrAst(attr.name, attr.value, attr.valueSpan !)); 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; return visitor.results;
} }