refactor(language-service): create attr for missing attr, bound tmpl (#34743)
Currently the language service constructs an `AttrAst` anytime it is missing from a `TemplateAst` path. However, this should only be done when the path does not contain an "attribute-like" AST, which can includes bound properties or bound events. This commit also refactors `visitAttr` to parse bindings only for microsyntax expressions and does some other minor cleanup to make linters happy. This is some cleanup to help the language service eventually use `BoundDirectivePropertyAst`s for providing completions for template bindings rather than performing the manual parsing currently done. PR Close #34743
This commit is contained in:
parent
7d401853b5
commit
15b4173a76
|
@ -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, ImplicitReceiver, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ParseSpan, PropertyRead, ReferenceAst, TagContentType, TemplateBinding, Text, getHtmlTagDefinition} from '@angular/compiler';
|
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 {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
||||||
|
|
||||||
import {AstResult} from './common';
|
import {AstResult} from './common';
|
||||||
|
@ -208,9 +208,9 @@ export function getTemplateCompletions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visitComment(ast) {},
|
visitComment() {},
|
||||||
visitExpansion(ast) {},
|
visitExpansion() {},
|
||||||
visitExpansionCase(ast) {}
|
visitExpansionCase() {}
|
||||||
},
|
},
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
@ -306,10 +306,11 @@ function attributeValueCompletions(
|
||||||
if (!path.tail) {
|
if (!path.tail) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// HtmlAst contains the `Attribute` node, however the corresponding `AttrAst`
|
// HtmlAst contains the `Attribute` node, however a corresponding attribute AST
|
||||||
// node is missing from the TemplateAst. In this case, we have to manually
|
// node may be missing from the TemplateAst if the compiler fails to parse it fully. In this case,
|
||||||
// append the `AttrAst` node to the path.
|
// manually append an `AttrAst` node to the path.
|
||||||
if (!(path.tail instanceof AttrAst)) {
|
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.
|
// The sourceSpan of an AttrAst is the valueSpan of the HTML Attribute.
|
||||||
path.push(new AttrAst(attr.name, attr.value, attr.valueSpan !));
|
path.push(new AttrAst(attr.name, attr.value, attr.valueSpan !));
|
||||||
}
|
}
|
||||||
|
@ -441,36 +442,34 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
||||||
|
|
||||||
visitEvent(ast: BoundEventAst): void { this.processExpressionCompletions(ast.handler); }
|
visitEvent(ast: BoundEventAst): void { this.processExpressionCompletions(ast.handler); }
|
||||||
|
|
||||||
visitElement(ast: ElementAst): void {
|
visitElement(): void {
|
||||||
// no-op for now
|
// no-op for now
|
||||||
}
|
}
|
||||||
|
|
||||||
visitAttr(ast: AttrAst) {
|
visitAttr(ast: AttrAst) {
|
||||||
// First, verify the attribute consists of some binding we can give completions for.
|
|
||||||
const {templateBindings} = this.info.expressionParser.parseTemplateBindings(
|
|
||||||
ast.name, ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset);
|
|
||||||
// Find where the cursor is relative to the start of the attribute value.
|
|
||||||
const valueRelativePosition = this.position - ast.sourceSpan.start.offset;
|
|
||||||
// Find the template binding that contains the position
|
|
||||||
const binding = templateBindings.find(b => inSpan(valueRelativePosition, b.span));
|
|
||||||
|
|
||||||
if (!binding) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ast.name.startsWith('*')) {
|
if (ast.name.startsWith('*')) {
|
||||||
|
// This a template binding given by micro syntax expression.
|
||||||
|
// First, verify the attribute consists of some binding we can give completions for.
|
||||||
|
const {templateBindings} = this.info.expressionParser.parseTemplateBindings(
|
||||||
|
ast.name, ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset);
|
||||||
|
// Find where the cursor is relative to the start of the attribute value.
|
||||||
|
const valueRelativePosition = this.position - ast.sourceSpan.start.offset;
|
||||||
|
// Find the template binding that contains the position.
|
||||||
|
const binding = templateBindings.find(b => inSpan(valueRelativePosition, b.span));
|
||||||
|
|
||||||
|
if (!binding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.microSyntaxInAttributeValue(ast, binding);
|
this.microSyntaxInAttributeValue(ast, binding);
|
||||||
} else {
|
} else {
|
||||||
// If the position is in the expression or after the key or there is no key, return the
|
|
||||||
// expression completions.
|
|
||||||
// The expression must be reparsed to get a valid AST rather than only template bindings.
|
|
||||||
const expressionAst = this.info.expressionParser.parseBinding(
|
const expressionAst = this.info.expressionParser.parseBinding(
|
||||||
ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset);
|
ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset);
|
||||||
this.processExpressionCompletions(expressionAst);
|
this.processExpressionCompletions(expressionAst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visitReference(ast: ReferenceAst, context: ElementAst) {
|
visitReference(_ast: ReferenceAst, context: ElementAst) {
|
||||||
context.directives.forEach(dir => {
|
context.directives.forEach(dir => {
|
||||||
const {exportAs} = dir.directive;
|
const {exportAs} = dir.directive;
|
||||||
if (exportAs) {
|
if (exportAs) {
|
||||||
|
|
Loading…
Reference in New Issue