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
|
@ -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 {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
||||
|
||||
import {ATTR, getBindingDescriptor} from './binding_utils';
|
||||
import {AstResult} from './common';
|
||||
import {getExpressionScope} from './expression_diagnostics';
|
||||
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) {
|
||||
// Identifiers consist of alphanumeric characters, '_', or '$'.
|
||||
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
|
||||
// bind parts for cases like [()|]
|
||||
// ^ cursor is here
|
||||
const bindParts = attr.name.match(BIND_NAME_REGEXP);
|
||||
const isTemplateRef = attr.name.startsWith(TEMPLATE_ATTR_PREFIX);
|
||||
const isBinding = bindParts !== null || isTemplateRef;
|
||||
|
||||
if (!isBinding) {
|
||||
const binding = getBindingDescriptor(attr.name);
|
||||
if (!binding) {
|
||||
// This is a normal HTML attribute, not an Angular attribute.
|
||||
return attributeCompletionsForElement(info, elem.name);
|
||||
}
|
||||
|
||||
const results: string[] = [];
|
||||
const ngAttrs = angularAttributes(info, elem.name);
|
||||
if (!bindParts) {
|
||||
// If bindParts is null then this must be a TemplateRef.
|
||||
results.push(...ngAttrs.templateRefs);
|
||||
} else if (
|
||||
bindParts[ATTR.KW_BIND_IDX] !== undefined ||
|
||||
bindParts[ATTR.IDENT_PROPERTY_IDX] !== undefined) {
|
||||
// property binding via bind- or []
|
||||
results.push(...propertyNames(elem.name), ...ngAttrs.inputs);
|
||||
} else if (
|
||||
bindParts[ATTR.KW_ON_IDX] !== undefined || bindParts[ATTR.IDENT_EVENT_IDX] !== undefined) {
|
||||
// event binding via on- or ()
|
||||
results.push(...eventNames(elem.name), ...ngAttrs.outputs);
|
||||
} else if (
|
||||
bindParts[ATTR.KW_BINDON_IDX] !== undefined ||
|
||||
bindParts[ATTR.IDENT_BANANA_BOX_IDX] !== undefined) {
|
||||
// banana-in-a-box binding via bindon- or [()]
|
||||
results.push(...ngAttrs.bananas);
|
||||
switch (binding.kind) {
|
||||
case ATTR.KW_MICROSYNTAX:
|
||||
// template reference attribute: *attrName
|
||||
results.push(...ngAttrs.templateRefs);
|
||||
break;
|
||||
|
||||
case ATTR.KW_BIND:
|
||||
case ATTR.IDENT_PROPERTY:
|
||||
// property binding via bind- or []
|
||||
results.push(...propertyNames(elem.name), ...ngAttrs.inputs);
|
||||
break;
|
||||
|
||||
case ATTR.KW_ON:
|
||||
case ATTR.IDENT_EVENT:
|
||||
// event binding via on- or ()
|
||||
results.push(...eventNames(elem.name), ...ngAttrs.outputs);
|
||||
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 {
|
||||
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
|
||||
// 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) {
|
||||
const binding = getBindingDescriptor(htmlAttr.name);
|
||||
if (binding && binding.kind === ATTR.KW_REF) {
|
||||
let refAst: ReferenceAst|undefined;
|
||||
let elemAst: ElementAst|undefined;
|
||||
if (templatePath.tail instanceof ReferenceAst) {
|
||||
|
@ -456,11 +433,12 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||
}
|
||||
|
||||
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.
|
||||
// First, verify the attribute consists of some binding we can give completions for.
|
||||
// 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 templateUrl = ast.sourceSpan.start.file.url;
|
||||
// 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(
|
||||
templateKey, templateValue, templateUrl, absKeyOffset, absValueOffset);
|
||||
// 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;
|
||||
}
|
||||
|
||||
this.microSyntaxInAttributeValue(ast, binding);
|
||||
this.microSyntaxInAttributeValue(ast, templateBinding);
|
||||
} else {
|
||||
const expressionAst = this.info.expressionParser.parseBinding(
|
||||
ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset);
|
||||
|
|
Loading…
Reference in New Issue