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);
result = attributeCompletions(templateInfo, path);
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
if (isReference) {
result = referenceAttributeValueCompletions(templateInfo, templatePosition, ast);
} else { } else {
result = attributeValueCompletions(templateInfo, templatePosition, ast); // LHS completion
} result = attributeCompletions(templateInfo, path);
} }
}, },
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
// 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, function attributeValueCompletions(info: AstResult, htmlPath: HtmlAstPath): ng.CompletionEntry[] {
// manually append an `AttrAst` node to the path. // Find the corresponding Template AST path.
if (!(path.tail instanceof AttrAst) && !(path.tail instanceof BoundElementPropertyAst) && const templatePath = findTemplateAstAt(info.templateAst, htmlPath.position);
!(path.tail instanceof BoundEventAst)) { const visitor = new ExpressionVisitor(info, htmlPath.position, () => {
// The sourceSpan of an AttrAst is the valueSpan of the HTML Attribute.
path.push(new AttrAst(attr.name, attr.value, attr.valueSpan !));
}
const dinfo = diagnosticInfoFromTemplateInfo(info); const dinfo = diagnosticInfoFromTemplateInfo(info);
const visitor = return getExpressionScope(dinfo, templatePath, false);
new ExpressionVisitor(info, position, () => getExpressionScope(dinfo, path, false)); });
path.tail.visit(visitor, null); if (templatePath.tail instanceof AttrAst ||
templatePath.tail instanceof BoundElementPropertyAst ||
templatePath.tail instanceof BoundEventAst) {
templatePath.tail.visit(visitor, null);
return visitor.results; return visitor.results;
} }
// In order to provide accurate attribute value completion, we need to know
function referenceAttributeValueCompletions( // what the LHS is, and construct the proper AST if it is missing.
info: AstResult, position: number, attr: Attribute): ng.CompletionEntry[] { const htmlAttr = htmlPath.tail as Attribute;
const path = findTemplateAstAt(info.templateAst, position); const bindParts = htmlAttr.name.match(BIND_NAME_REGEXP);
if (!path.tail) { if (bindParts && bindParts[ATTR.KW_REF_IDX] !== undefined) {
return []; 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) {
// When the template parser does not find a directive with matching "exportAs", refAst = new ReferenceAst(htmlAttr.name, null !, htmlAttr.value, htmlAttr.valueSpan !);
// the ReferenceAst will be ignored. elemAst = templatePath.tail;
if (!(path.tail instanceof ReferenceAst)) { }
// The sourceSpan of an ReferenceAst is the valueSpan of the HTML Attribute. if (refAst && elemAst) {
path.push(new ReferenceAst(attr.name, null !, attr.value, attr.valueSpan !)); 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, path.parentOf(path.tail));
return visitor.results; return visitor.results;
} }