@@ -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+ / ^ (?: (?: (?: ( b i n d - ) | ( l e t - ) | ( r e f - | # ) | ( o n - ) | ( b i n d o n - ) | ( @ ) ) ( .* ) ) | \[ \( ( [ ^ \) ] * ) \) \] | \[ ( [ ^ \] ] * ) \] | \( ( [ ^ \) ] * ) \) ) $ / ;
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+
4875function 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
192219function 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
200268function 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}
0 commit comments