diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index f507ce9184..752b8acd6b 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -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; }