refactor(language-service): provide service for attribute binding type (#36301)
This commit refactors the process for determining the type of an Angular attribute to be use a function that takes an attribute name and returns the Angular attribute kind and name, rather than requiring the user to query match the attribute name with the regex and query the matching array. This refactor prepares for a future change that will improve the experience of completing attributes in `()`, `[]`, or `[()]` contexts. PR Close #36301
This commit is contained in:
parent
2dd6f25647
commit
2c7d366c82
69
packages/language-service/src/binding_utils.ts
Normal file
69
packages/language-service/src/binding_utils.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches an Angular attribute to a binding type. See `ATTR` for more details.
|
||||||
|
*
|
||||||
|
* This is adapted from packages/compiler/src/render3/r3_template_transform.ts
|
||||||
|
* to allow empty binding names and match template attributes.
|
||||||
|
*/
|
||||||
|
const BIND_NAME_REGEXP =
|
||||||
|
/^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@)|(\*))(.*))|\[\(([^\)]*)\)\]|\[([^\]]*)\]|\(([^\)]*)\))$/;
|
||||||
|
/**
|
||||||
|
* Represents possible Angular attribute bindings, as indices on a match of `BIND_NAME_REGEXP`.
|
||||||
|
*/
|
||||||
|
export enum ATTR {
|
||||||
|
/** "bind-" */
|
||||||
|
KW_BIND = 1,
|
||||||
|
/** "let-" */
|
||||||
|
KW_LET = 2,
|
||||||
|
/** "ref-/#" */
|
||||||
|
KW_REF = 3,
|
||||||
|
/** "on-" */
|
||||||
|
KW_ON = 4,
|
||||||
|
/** "bindon-" */
|
||||||
|
KW_BINDON = 5,
|
||||||
|
/** "@" */
|
||||||
|
KW_AT = 6,
|
||||||
|
/**
|
||||||
|
* "*"
|
||||||
|
* Microsyntax template starts with '*'. See https://angular.io/api/core/TemplateRef
|
||||||
|
*/
|
||||||
|
KW_MICROSYNTAX = 7,
|
||||||
|
/** The identifier after "bind-", "let-", "ref-/#", "on-", "bindon-", "@", or "*" */
|
||||||
|
IDENT_KW = 8,
|
||||||
|
/** Identifier inside [()] */
|
||||||
|
IDENT_BANANA_BOX = 9,
|
||||||
|
/** Identifier inside [] */
|
||||||
|
IDENT_PROPERTY = 10,
|
||||||
|
/** Identifier inside () */
|
||||||
|
IDENT_EVENT = 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BindingDescriptor {
|
||||||
|
kind: ATTR;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns a descriptor for a given Angular attribute, or undefined if the attribute is
|
||||||
|
* not an Angular attribute.
|
||||||
|
*/
|
||||||
|
export function getBindingDescriptor(attribute: string): BindingDescriptor|undefined {
|
||||||
|
const bindParts = attribute.match(BIND_NAME_REGEXP);
|
||||||
|
if (!bindParts) return;
|
||||||
|
// The first match element is skipped because it matches the entire attribute text, including the
|
||||||
|
// binding part.
|
||||||
|
const kind = bindParts.findIndex((val, i) => i > 0 && val !== undefined);
|
||||||
|
if (!(kind in ATTR)) {
|
||||||
|
throw TypeError(`"${kind}" is not a valid Angular binding kind for "${attribute}"`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
name: bindParts[ATTR.IDENT_KW],
|
||||||
|
};
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding} from '@angular/compiler';
|
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding} from '@angular/compiler';
|
||||||
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
||||||
|
|
||||||
|
import {ATTR, getBindingDescriptor} from './binding_utils';
|
||||||
import {AstResult} from './common';
|
import {AstResult} from './common';
|
||||||
import {getExpressionScope} from './expression_diagnostics';
|
import {getExpressionScope} from './expression_diagnostics';
|
||||||
import {getExpressionCompletions} from './expressions';
|
import {getExpressionCompletions} from './expressions';
|
||||||
@ -45,35 +46,6 @@ const ANGULAR_ELEMENTS: ReadonlyArray<ng.CompletionEntry> = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// This is adapted from packages/compiler/src/render3/r3_template_transform.ts
|
|
||||||
// to allow empty binding names.
|
|
||||||
const BIND_NAME_REGEXP =
|
|
||||||
/^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.*))|\[\(([^\)]*)\)\]|\[([^\]]*)\]|\(([^\)]*)\))$/;
|
|
||||||
enum ATTR {
|
|
||||||
// Group 1 = "bind-"
|
|
||||||
KW_BIND_IDX = 1,
|
|
||||||
// Group 2 = "let-"
|
|
||||||
KW_LET_IDX = 2,
|
|
||||||
// Group 3 = "ref-/#"
|
|
||||||
KW_REF_IDX = 3,
|
|
||||||
// Group 4 = "on-"
|
|
||||||
KW_ON_IDX = 4,
|
|
||||||
// Group 5 = "bindon-"
|
|
||||||
KW_BINDON_IDX = 5,
|
|
||||||
// Group 6 = "@"
|
|
||||||
KW_AT_IDX = 6,
|
|
||||||
// Group 7 = the identifier after "bind-", "let-", "ref-/#", "on-", "bindon-" or "@"
|
|
||||||
IDENT_KW_IDX = 7,
|
|
||||||
// Group 8 = identifier inside [()]
|
|
||||||
IDENT_BANANA_BOX_IDX = 8,
|
|
||||||
// Group 9 = identifier inside []
|
|
||||||
IDENT_PROPERTY_IDX = 9,
|
|
||||||
// Group 10 = identifier inside ()
|
|
||||||
IDENT_EVENT_IDX = 10,
|
|
||||||
}
|
|
||||||
// Microsyntax template starts with '*'. See https://angular.io/api/core/TemplateRef
|
|
||||||
const TEMPLATE_ATTR_PREFIX = '*';
|
|
||||||
|
|
||||||
function isIdentifierPart(code: number) {
|
function isIdentifierPart(code: number) {
|
||||||
// Identifiers consist of alphanumeric characters, '_', or '$'.
|
// Identifiers consist of alphanumeric characters, '_', or '$'.
|
||||||
return isAsciiLetter(code) || isDigit(code) || code == $$ || code == $_;
|
return isAsciiLetter(code) || isDigit(code) || code == $$ || code == $_;
|
||||||
@ -233,34 +205,39 @@ function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.Compl
|
|||||||
// matching using regex. This is because the regexp would incorrectly identify
|
// matching using regex. This is because the regexp would incorrectly identify
|
||||||
// bind parts for cases like [()|]
|
// bind parts for cases like [()|]
|
||||||
// ^ cursor is here
|
// ^ cursor is here
|
||||||
const bindParts = attr.name.match(BIND_NAME_REGEXP);
|
const binding = getBindingDescriptor(attr.name);
|
||||||
const isTemplateRef = attr.name.startsWith(TEMPLATE_ATTR_PREFIX);
|
if (!binding) {
|
||||||
const isBinding = bindParts !== null || isTemplateRef;
|
// This is a normal HTML attribute, not an Angular attribute.
|
||||||
|
|
||||||
if (!isBinding) {
|
|
||||||
return attributeCompletionsForElement(info, elem.name);
|
return attributeCompletionsForElement(info, elem.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
const ngAttrs = angularAttributes(info, elem.name);
|
const ngAttrs = angularAttributes(info, elem.name);
|
||||||
if (!bindParts) {
|
switch (binding.kind) {
|
||||||
// If bindParts is null then this must be a TemplateRef.
|
case ATTR.KW_MICROSYNTAX:
|
||||||
results.push(...ngAttrs.templateRefs);
|
// template reference attribute: *attrName
|
||||||
} else if (
|
results.push(...ngAttrs.templateRefs);
|
||||||
bindParts[ATTR.KW_BIND_IDX] !== undefined ||
|
break;
|
||||||
bindParts[ATTR.IDENT_PROPERTY_IDX] !== undefined) {
|
|
||||||
// property binding via bind- or []
|
case ATTR.KW_BIND:
|
||||||
results.push(...propertyNames(elem.name), ...ngAttrs.inputs);
|
case ATTR.IDENT_PROPERTY:
|
||||||
} else if (
|
// property binding via bind- or []
|
||||||
bindParts[ATTR.KW_ON_IDX] !== undefined || bindParts[ATTR.IDENT_EVENT_IDX] !== undefined) {
|
results.push(...propertyNames(elem.name), ...ngAttrs.inputs);
|
||||||
// event binding via on- or ()
|
break;
|
||||||
results.push(...eventNames(elem.name), ...ngAttrs.outputs);
|
|
||||||
} else if (
|
case ATTR.KW_ON:
|
||||||
bindParts[ATTR.KW_BINDON_IDX] !== undefined ||
|
case ATTR.IDENT_EVENT:
|
||||||
bindParts[ATTR.IDENT_BANANA_BOX_IDX] !== undefined) {
|
// event binding via on- or ()
|
||||||
// banana-in-a-box binding via bindon- or [()]
|
results.push(...eventNames(elem.name), ...ngAttrs.outputs);
|
||||||
results.push(...ngAttrs.bananas);
|
break;
|
||||||
|
|
||||||
|
case ATTR.KW_BINDON:
|
||||||
|
case ATTR.IDENT_BANANA_BOX:
|
||||||
|
// banana-in-a-box binding via bindon- or [()]
|
||||||
|
results.push(...ngAttrs.bananas);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.map(name => {
|
return results.map(name => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -321,8 +298,8 @@ function attributeValueCompletions(info: AstResult, htmlPath: HtmlAstPath): ng.C
|
|||||||
// In order to provide accurate attribute value completion, we need to know
|
// 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.
|
// what the LHS is, and construct the proper AST if it is missing.
|
||||||
const htmlAttr = htmlPath.tail as Attribute;
|
const htmlAttr = htmlPath.tail as Attribute;
|
||||||
const bindParts = htmlAttr.name.match(BIND_NAME_REGEXP);
|
const binding = getBindingDescriptor(htmlAttr.name);
|
||||||
if (bindParts && bindParts[ATTR.KW_REF_IDX] !== undefined) {
|
if (binding && binding.kind === ATTR.KW_REF) {
|
||||||
let refAst: ReferenceAst|undefined;
|
let refAst: ReferenceAst|undefined;
|
||||||
let elemAst: ElementAst|undefined;
|
let elemAst: ElementAst|undefined;
|
||||||
if (templatePath.tail instanceof ReferenceAst) {
|
if (templatePath.tail instanceof ReferenceAst) {
|
||||||
@ -456,11 +433,12 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
visitAttr(ast: AttrAst) {
|
visitAttr(ast: AttrAst) {
|
||||||
if (ast.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
const binding = getBindingDescriptor(ast.name);
|
||||||
|
if (binding && binding.kind === ATTR.KW_MICROSYNTAX) {
|
||||||
// This a template binding given by micro syntax expression.
|
// This a template binding given by micro syntax expression.
|
||||||
// First, verify the attribute consists of some binding we can give completions for.
|
// First, verify the attribute consists of some binding we can give completions for.
|
||||||
// The sourceSpan of AttrAst points to the RHS of the attribute
|
// The sourceSpan of AttrAst points to the RHS of the attribute
|
||||||
const templateKey = ast.name.substring(TEMPLATE_ATTR_PREFIX.length);
|
const templateKey = binding.name;
|
||||||
const templateValue = ast.sourceSpan.toString();
|
const templateValue = ast.sourceSpan.toString();
|
||||||
const templateUrl = ast.sourceSpan.start.file.url;
|
const templateUrl = ast.sourceSpan.start.file.url;
|
||||||
// TODO(kyliau): We are unable to determine the absolute offset of the key
|
// TODO(kyliau): We are unable to determine the absolute offset of the key
|
||||||
@ -470,13 +448,13 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||||||
const {templateBindings} = this.info.expressionParser.parseTemplateBindings(
|
const {templateBindings} = this.info.expressionParser.parseTemplateBindings(
|
||||||
templateKey, templateValue, templateUrl, absKeyOffset, absValueOffset);
|
templateKey, templateValue, templateUrl, absKeyOffset, absValueOffset);
|
||||||
// Find the template binding that contains the position.
|
// Find the template binding that contains the position.
|
||||||
const binding = templateBindings.find(b => inSpan(this.position, b.sourceSpan));
|
const templateBinding = templateBindings.find(b => inSpan(this.position, b.sourceSpan));
|
||||||
|
|
||||||
if (!binding) {
|
if (!templateBinding) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.microSyntaxInAttributeValue(ast, binding);
|
this.microSyntaxInAttributeValue(ast, templateBinding);
|
||||||
} else {
|
} else {
|
||||||
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user