Skip to content

Commit a04f7c0

Browse files
kyliaukara
authored andcommitted
fix(language-service): Proper completions for properties and events (angular#34445)
This commit fixes autocompletions for properties and events bindings. The language service will no longer provide bindings like (click) or [id]. Instead, it'll infer the context based on the brackets and provide suggestions without any brackets. This fix also adds support for alternative binding syntax such as `bind-`, `on-`, and `bindon`. PR closes angular/vscode-ng-language-service#398 PR closes angular/vscode-ng-language-service#474 PR Close angular#34445
1 parent 9d1175e commit a04f7c0

File tree

7 files changed

+237
-155
lines changed

7 files changed

+237
-155
lines changed

packages/language-service/src/completions.ts

Lines changed: 117 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,33 @@ const ANGULAR_ELEMENTS: ReadonlyArray<ng.CompletionEntry> = [
4545
},
4646
];
4747

48+
// This is adapted from packages/compiler/src/render3/r3_template_transform.ts
49+
// to allow empty binding names.
50+
const BIND_NAME_REGEXP =
51+
/^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.*))|\[\(([^\)]*)\)\]|\[([^\]]*)\]|\(([^\)]*)\))$/;
52+
enum ATTR {
53+
// Group 1 = "bind-"
54+
KW_BIND_IDX = 1,
55+
// Group 2 = "let-"
56+
KW_LET_IDX = 2,
57+
// Group 3 = "ref-/#"
58+
KW_REF_IDX = 3,
59+
// Group 4 = "on-"
60+
KW_ON_IDX = 4,
61+
// Group 5 = "bindon-"
62+
KW_BINDON_IDX = 5,
63+
// Group 6 = "@"
64+
KW_AT_IDX = 6,
65+
// Group 7 = the identifier after "bind-", "let-", "ref-/#", "on-", "bindon-" or "@"
66+
IDENT_KW_IDX = 7,
67+
// Group 8 = identifier inside [()]
68+
IDENT_BANANA_BOX_IDX = 8,
69+
// Group 9 = identifier inside []
70+
IDENT_PROPERTY_IDX = 9,
71+
// Group 10 = identifier inside ()
72+
IDENT_EVENT_IDX = 10,
73+
}
74+
4875
function isIdentifierPart(code: number) {
4976
// Identifiers consist of alphanumeric characters, '_', or '$'.
5077
return isAsciiLetter(code) || isDigit(code) || code == $$ || code == $_;
@@ -139,7 +166,7 @@ export function getTemplateCompletions(
139166
} else if (templatePosition < startTagSpan.end) {
140167
// We are in the attribute section of the element (but not in an attribute).
141168
// Return the attribute completions.
142-
result = attributeCompletions(templateInfo, path);
169+
result = attributeCompletionsForElement(templateInfo, ast.name);
143170
}
144171
},
145172
visitAttribute(ast) {
@@ -190,11 +217,52 @@ export function getTemplateCompletions(
190217
}
191218

192219
function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
193-
const item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
194-
if (item instanceof Element) {
195-
return attributeCompletionsForElement(info, item.name);
220+
const attr = path.tail;
221+
const elem = path.parentOf(attr);
222+
if (!(attr instanceof Attribute) || !(elem instanceof Element)) {
223+
return [];
196224
}
197-
return [];
225+
226+
// TODO: Consider parsing the attrinute name to a proper AST instead of
227+
// matching using regex. This is because the regexp would incorrectly identify
228+
// bind parts for cases like [()|]
229+
// ^ cursor is here
230+
const bindParts = attr.name.match(BIND_NAME_REGEXP);
231+
// TemplateRef starts with '*'. See https://angular.io/api/core/TemplateRef
232+
const isTemplateRef = attr.name.startsWith('*');
233+
const isBinding = bindParts !== null || isTemplateRef;
234+
235+
if (!isBinding) {
236+
return attributeCompletionsForElement(info, elem.name);
237+
}
238+
239+
const results: string[] = [];
240+
const ngAttrs = angularAttributes(info, elem.name);
241+
if (!bindParts) {
242+
// If bindParts is null then this must be a TemplateRef.
243+
results.push(...ngAttrs.templateRefs);
244+
} else if (
245+
bindParts[ATTR.KW_BIND_IDX] !== undefined ||
246+
bindParts[ATTR.IDENT_PROPERTY_IDX] !== undefined) {
247+
// property binding via bind- or []
248+
results.push(...propertyNames(elem.name), ...ngAttrs.inputs);
249+
} else if (
250+
bindParts[ATTR.KW_ON_IDX] !== undefined || bindParts[ATTR.IDENT_EVENT_IDX] !== undefined) {
251+
// event binding via on- or ()
252+
results.push(...eventNames(elem.name), ...ngAttrs.outputs);
253+
} else if (
254+
bindParts[ATTR.KW_BINDON_IDX] !== undefined ||
255+
bindParts[ATTR.IDENT_BANANA_BOX_IDX] !== undefined) {
256+
// banana-in-a-box binding via bindon- or [()]
257+
results.push(...ngAttrs.bananas);
258+
}
259+
return results.map(name => {
260+
return {
261+
name,
262+
kind: ng.CompletionKind.ATTRIBUTE,
263+
sortText: name,
264+
};
265+
});
198266
}
199267

200268
function attributeCompletionsForElement(
@@ -212,27 +280,16 @@ function attributeCompletionsForElement(
212280
}
213281
}
214282

215-
// Add html properties
216-
for (const name of propertyNames(elementName)) {
217-
results.push({
218-
name: `[${name}]`,
219-
kind: ng.CompletionKind.ATTRIBUTE,
220-
sortText: name,
221-
});
222-
}
223-
224-
// Add html events
225-
for (const name of eventNames(elementName)) {
283+
// Add Angular attributes
284+
const ngAttrs = angularAttributes(info, elementName);
285+
for (const name of ngAttrs.others) {
226286
results.push({
227-
name: `(${name})`,
287+
name,
228288
kind: ng.CompletionKind.ATTRIBUTE,
229289
sortText: name,
230290
});
231291
}
232292

233-
// Add Angular attributes
234-
results.push(...angularAttributes(info, elementName));
235-
236293
return results;
237294
}
238295

@@ -484,24 +541,54 @@ function getSourceText(template: ng.TemplateSource, span: ng.Span): string {
484541
return template.source.substring(span.start, span.end);
485542
}
486543

487-
function angularAttributes(info: AstResult, elementName: string): ng.CompletionEntry[] {
544+
interface AngularAttributes {
545+
/**
546+
* Attributes that support the * syntax. See https://angular.io/api/core/TemplateRef
547+
*/
548+
templateRefs: Set<string>;
549+
/**
550+
* Attributes with the @Input annotation.
551+
*/
552+
inputs: Set<string>;
553+
/**
554+
* Attributes with the @Output annotation.
555+
*/
556+
outputs: Set<string>;
557+
/**
558+
* Attributes that support the [()] or bindon- syntax.
559+
*/
560+
bananas: Set<string>;
561+
/**
562+
* General attributes that match the specified element.
563+
*/
564+
others: Set<string>;
565+
}
566+
567+
/**
568+
* Return all Angular-specific attributes for the element with `elementName`.
569+
* @param info
570+
* @param elementName
571+
*/
572+
function angularAttributes(info: AstResult, elementName: string): AngularAttributes {
488573
const {selectors, map: selectorMap} = getSelectors(info);
489574
const templateRefs = new Set<string>();
490575
const inputs = new Set<string>();
491576
const outputs = new Set<string>();
577+
const bananas = new Set<string>();
492578
const others = new Set<string>();
493579
for (const selector of selectors) {
494580
if (selector.element && selector.element !== elementName) {
495581
continue;
496582
}
497583
const summary = selectorMap.get(selector) !;
498-
for (const attr of selector.attrs) {
499-
if (attr) {
500-
if (hasTemplateReference(summary.type)) {
501-
templateRefs.add(attr);
502-
} else {
503-
others.add(attr);
504-
}
584+
const isTemplateRef = hasTemplateReference(summary.type);
585+
// attributes are listed in (attribute, value) pairs
586+
for (let i = 0; i < selector.attrs.length; i += 2) {
587+
const attr = selector.attrs[i];
588+
if (isTemplateRef) {
589+
templateRefs.add(attr);
590+
} else {
591+
others.add(attr);
505592
}
506593
}
507594
for (const input of Object.values(summary.inputs)) {
@@ -511,44 +598,12 @@ function angularAttributes(info: AstResult, elementName: string): ng.CompletionE
511598
outputs.add(output);
512599
}
513600
}
514-
515-
const results: ng.CompletionEntry[] = [];
516-
for (const name of templateRefs) {
517-
results.push({
518-
name: `*${name}`,
519-
kind: ng.CompletionKind.ATTRIBUTE,
520-
sortText: name,
521-
});
522-
}
523601
for (const name of inputs) {
524-
results.push({
525-
name: `[${name}]`,
526-
kind: ng.CompletionKind.ATTRIBUTE,
527-
sortText: name,
528-
});
529602
// Add banana-in-a-box syntax
530603
// https://angular.io/guide/template-syntax#two-way-binding-
531604
if (outputs.has(`${name}Change`)) {
532-
results.push({
533-
name: `[(${name})]`,
534-
kind: ng.CompletionKind.ATTRIBUTE,
535-
sortText: name,
536-
});
605+
bananas.add(name);
537606
}
538607
}
539-
for (const name of outputs) {
540-
results.push({
541-
name: `(${name})`,
542-
kind: ng.CompletionKind.ATTRIBUTE,
543-
sortText: name,
544-
});
545-
}
546-
for (const name of others) {
547-
results.push({
548-
name,
549-
kind: ng.CompletionKind.ATTRIBUTE,
550-
sortText: name,
551-
});
552-
}
553-
return results;
608+
return {templateRefs, inputs, outputs, bananas, others};
554609
}

packages/language-service/src/utils.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, identifierName, templateVisitAll, visitAll} from '@angular/compiler';
9+
import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, Identifiers, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, identifierName, templateVisitAll, visitAll} from '@angular/compiler';
1010
import * as ts from 'typescript';
1111

1212
import {AstResult, SelectorInfo} from './common';
@@ -57,11 +57,9 @@ export function isNarrower(spanA: Span, spanB: Span): boolean {
5757
}
5858

5959
export function hasTemplateReference(type: CompileTypeMetadata): boolean {
60-
if (type.diDeps) {
61-
for (let diDep of type.diDeps) {
62-
if (diDep.token && diDep.token.identifier &&
63-
identifierName(diDep.token !.identifier !) === 'TemplateRef')
64-
return true;
60+
for (const diDep of type.diDeps) {
61+
if (diDep.token && identifierName(diDep.token.identifier) === Identifiers.TemplateRef.name) {
62+
return true;
6563
}
6664
}
6765
return false;

0 commit comments

Comments
 (0)