fix(language-service): Proper completions for properties and events (#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 https://github.com/angular/vscode-ng-language-service/issues/398 PR closes https://github.com/angular/vscode-ng-language-service/issues/474 PR Close #34445
This commit is contained in:
		
							parent
							
								
									9d1175e2b2
								
							
						
					
					
						commit
						a04f7c0d5f
					
				| @ -45,6 +45,33 @@ 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, | ||||
| } | ||||
| 
 | ||||
| function isIdentifierPart(code: number) { | ||||
|   // Identifiers consist of alphanumeric characters, '_', or '$'.
 | ||||
|   return isAsciiLetter(code) || isDigit(code) || code == $$ || code == $_; | ||||
| @ -139,7 +166,7 @@ export function getTemplateCompletions( | ||||
|             } else if (templatePosition < startTagSpan.end) { | ||||
|               // We are in the attribute section of the element (but not in an attribute).
 | ||||
|               // Return the attribute completions.
 | ||||
|               result = attributeCompletions(templateInfo, path); | ||||
|               result = attributeCompletionsForElement(templateInfo, ast.name); | ||||
|             } | ||||
|           }, | ||||
|           visitAttribute(ast) { | ||||
| @ -190,13 +217,54 @@ export function getTemplateCompletions( | ||||
| } | ||||
| 
 | ||||
| function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] { | ||||
|   const item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail); | ||||
|   if (item instanceof Element) { | ||||
|     return attributeCompletionsForElement(info, item.name); | ||||
|   } | ||||
|   const attr = path.tail; | ||||
|   const elem = path.parentOf(attr); | ||||
|   if (!(attr instanceof Attribute) || !(elem instanceof Element)) { | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // TODO: Consider parsing the attrinute name to a proper AST instead of
 | ||||
|   // 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); | ||||
|   // TemplateRef starts with '*'. See https://angular.io/api/core/TemplateRef
 | ||||
|   const isTemplateRef = attr.name.startsWith('*'); | ||||
|   const isBinding = bindParts !== null || isTemplateRef; | ||||
| 
 | ||||
|   if (!isBinding) { | ||||
|     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); | ||||
|   } | ||||
|   return results.map(name => { | ||||
|     return { | ||||
|       name, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function attributeCompletionsForElement( | ||||
|     info: AstResult, elementName: string): ng.CompletionEntry[] { | ||||
|   const results: ng.CompletionEntry[] = []; | ||||
| @ -212,26 +280,15 @@ function attributeCompletionsForElement( | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add html properties
 | ||||
|   for (const name of propertyNames(elementName)) { | ||||
|     results.push({ | ||||
|       name: `[${name}]`, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Add html events
 | ||||
|   for (const name of eventNames(elementName)) { | ||||
|     results.push({ | ||||
|       name: `(${name})`, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Add Angular attributes
 | ||||
|   results.push(...angularAttributes(info, elementName)); | ||||
|   const ngAttrs = angularAttributes(info, elementName); | ||||
|   for (const name of ngAttrs.others) { | ||||
|     results.push({ | ||||
|       name, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   return results; | ||||
| } | ||||
| @ -484,26 +541,56 @@ function getSourceText(template: ng.TemplateSource, span: ng.Span): string { | ||||
|   return template.source.substring(span.start, span.end); | ||||
| } | ||||
| 
 | ||||
| function angularAttributes(info: AstResult, elementName: string): ng.CompletionEntry[] { | ||||
| interface AngularAttributes { | ||||
|   /** | ||||
|    * Attributes that support the * syntax. See https://angular.io/api/core/TemplateRef
 | ||||
|    */ | ||||
|   templateRefs: Set<string>; | ||||
|   /** | ||||
|    * Attributes with the @Input annotation. | ||||
|    */ | ||||
|   inputs: Set<string>; | ||||
|   /** | ||||
|    * Attributes with the @Output annotation. | ||||
|    */ | ||||
|   outputs: Set<string>; | ||||
|   /** | ||||
|    * Attributes that support the [()] or bindon- syntax. | ||||
|    */ | ||||
|   bananas: Set<string>; | ||||
|   /** | ||||
|    * General attributes that match the specified element. | ||||
|    */ | ||||
|   others: Set<string>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Return all Angular-specific attributes for the element with `elementName`. | ||||
|  * @param info | ||||
|  * @param elementName | ||||
|  */ | ||||
| function angularAttributes(info: AstResult, elementName: string): AngularAttributes { | ||||
|   const {selectors, map: selectorMap} = getSelectors(info); | ||||
|   const templateRefs = new Set<string>(); | ||||
|   const inputs = new Set<string>(); | ||||
|   const outputs = new Set<string>(); | ||||
|   const bananas = new Set<string>(); | ||||
|   const others = new Set<string>(); | ||||
|   for (const selector of selectors) { | ||||
|     if (selector.element && selector.element !== elementName) { | ||||
|       continue; | ||||
|     } | ||||
|     const summary = selectorMap.get(selector) !; | ||||
|     for (const attr of selector.attrs) { | ||||
|       if (attr) { | ||||
|         if (hasTemplateReference(summary.type)) { | ||||
|     const isTemplateRef = hasTemplateReference(summary.type); | ||||
|     // attributes are listed in (attribute, value) pairs
 | ||||
|     for (let i = 0; i < selector.attrs.length; i += 2) { | ||||
|       const attr = selector.attrs[i]; | ||||
|       if (isTemplateRef) { | ||||
|         templateRefs.add(attr); | ||||
|       } else { | ||||
|         others.add(attr); | ||||
|       } | ||||
|     } | ||||
|     } | ||||
|     for (const input of Object.values(summary.inputs)) { | ||||
|       inputs.add(input); | ||||
|     } | ||||
| @ -511,44 +598,12 @@ function angularAttributes(info: AstResult, elementName: string): ng.CompletionE | ||||
|       outputs.add(output); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const results: ng.CompletionEntry[] = []; | ||||
|   for (const name of templateRefs) { | ||||
|     results.push({ | ||||
|       name: `*${name}`, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|   } | ||||
|   for (const name of inputs) { | ||||
|     results.push({ | ||||
|       name: `[${name}]`, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|     // Add banana-in-a-box syntax
 | ||||
|     // https://angular.io/guide/template-syntax#two-way-binding-
 | ||||
|     if (outputs.has(`${name}Change`)) { | ||||
|       results.push({ | ||||
|         name: `[(${name})]`, | ||||
|         kind: ng.CompletionKind.ATTRIBUTE, | ||||
|         sortText: name, | ||||
|       }); | ||||
|       bananas.add(name); | ||||
|     } | ||||
|   } | ||||
|   for (const name of outputs) { | ||||
|     results.push({ | ||||
|       name: `(${name})`, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|   } | ||||
|   for (const name of others) { | ||||
|     results.push({ | ||||
|       name, | ||||
|       kind: ng.CompletionKind.ATTRIBUTE, | ||||
|       sortText: name, | ||||
|     }); | ||||
|   } | ||||
|   return results; | ||||
|   return {templateRefs, inputs, outputs, bananas, others}; | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, identifierName, templateVisitAll, visitAll} from '@angular/compiler'; | ||||
| import {AstPath, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, Identifiers, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, identifierName, templateVisitAll, visitAll} from '@angular/compiler'; | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {AstResult, SelectorInfo} from './common'; | ||||
| @ -57,10 +57,8 @@ export function isNarrower(spanA: Span, spanB: Span): boolean { | ||||
| } | ||||
| 
 | ||||
| export function hasTemplateReference(type: CompileTypeMetadata): boolean { | ||||
|   if (type.diDeps) { | ||||
|     for (let diDep of type.diDeps) { | ||||
|       if (diDep.token && diDep.token.identifier && | ||||
|           identifierName(diDep.token !.identifier !) === 'TemplateRef') | ||||
|   for (const diDep of type.diDeps) { | ||||
|     if (diDep.token && identifierName(diDep.token.identifier) === Identifiers.TemplateRef.name) { | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -80,14 +80,15 @@ describe('completions', () => { | ||||
|     ]); | ||||
|   }); | ||||
| 
 | ||||
|   it('should be able to find common angular attributes', () => { | ||||
|   it('should be able to find common Angular attributes', () => { | ||||
|     const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'div-attributes'); | ||||
|     const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start); | ||||
|     expectContain(completions, CompletionKind.ATTRIBUTE, [ | ||||
|       '(click)', | ||||
|       '[ngClass]', | ||||
|       '*ngIf', | ||||
|       '*ngFor', | ||||
|       'ngClass', | ||||
|       'ngForm', | ||||
|       'ngModel', | ||||
|       'string-model', | ||||
|       'number-model', | ||||
|     ]); | ||||
|   }); | ||||
| 
 | ||||
| @ -117,46 +118,21 @@ describe('completions', () => { | ||||
|     expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|   }); | ||||
| 
 | ||||
|   describe('property completions for members of an indexed type', () => { | ||||
|     it('should work with numeric index signatures (arrays)', () => { | ||||
|       mockHost.override(TEST_TEMPLATE, `{{ heroes[0].~{heroes-number-index}}}`); | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-number-index'); | ||||
|   it('should suggest template refereces', () => { | ||||
|     mockHost.override(TEST_TEMPLATE, `<div *~{cursor}></div>`); | ||||
|     const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|     const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|       expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|     expectContain(completions, CompletionKind.ATTRIBUTE, [ | ||||
|       'ngFor', | ||||
|       'ngForOf', | ||||
|       'ngIf', | ||||
|       'ngSwitchCase', | ||||
|       'ngSwitchDefault', | ||||
|       'ngPluralCase', | ||||
|     ]); | ||||
|   }); | ||||
| 
 | ||||
|     it('should work with numeric index signatures (tuple arrays)', () => { | ||||
|       mockHost.override(TEST_TEMPLATE, `{{ tupleArray[1].~{tuple-array-number-index}}}`); | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'tuple-array-number-index'); | ||||
|       const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|       expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|     }); | ||||
| 
 | ||||
|     describe('with string index signatures', () => { | ||||
|       it('should work with index notation', () => { | ||||
|         mockHost.override(TEST_TEMPLATE, `{{ heroesByName['Jacky'].~{heroes-string-index}}}`); | ||||
|         const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index'); | ||||
|         const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|         expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|       }); | ||||
| 
 | ||||
|       it('should work with dot notation', () => { | ||||
|         mockHost.override(TEST_TEMPLATE, `{{ heroesByName.jacky.~{heroes-string-index}}}`); | ||||
|         const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index'); | ||||
|         const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|         expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|       }); | ||||
| 
 | ||||
|       it('should work with dot notation if stringIndexType is a primitive type', () => { | ||||
|         mockHost.override(TEST_TEMPLATE, `{{ primitiveIndexType.test.~{string-primitive-type}}}`); | ||||
|         const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'string-primitive-type'); | ||||
|         const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|         expectContain(completions, CompletionKind.METHOD, ['substring']); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   it('should be able to return attribute names with an incompete attribute', () => { | ||||
|   it('should be able to return attribute names with an incomplete attribute', () => { | ||||
|     const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'no-value-attribute'); | ||||
|     const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); | ||||
|     expectContain(completions, CompletionKind.HTML_ATTRIBUTE, ['id', 'class', 'dir', 'lang']); | ||||
| @ -275,14 +251,16 @@ describe('completions', () => { | ||||
|       expect(entries).not.toContain(jasmine.objectContaining({name: 'onmouseup'})); | ||||
|     }); | ||||
| 
 | ||||
|     it('should be able to find common angular attributes', () => { | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'div-attributes'); | ||||
|     it('should be able to find common Angular attributes', () => { | ||||
|       mockHost.override(TEST_TEMPLATE, `<div ~{cursor}></div>`); | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|       expectContain(completions, CompletionKind.ATTRIBUTE, [ | ||||
|         '(click)', | ||||
|         '[ngClass]', | ||||
|         '*ngIf', | ||||
|         '*ngFor', | ||||
|         'ngClass', | ||||
|         'ngForm', | ||||
|         'ngModel', | ||||
|         'string-model', | ||||
|         'number-model', | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| @ -366,14 +344,10 @@ describe('completions', () => { | ||||
|     }); | ||||
| 
 | ||||
|     it('should be able to complete a the LHS of a two-way binding', () => { | ||||
|       const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'two-way-binding-input'); | ||||
|       const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); | ||||
|       expectContain(completions, CompletionKind.ATTRIBUTE, [ | ||||
|         'ngModel', | ||||
|         '[ngModel]', | ||||
|         '(ngModelChange)', | ||||
|         '[(ngModel)]', | ||||
|       ]); | ||||
|       mockHost.override(TEST_TEMPLATE, `<div [(~{cursor})]></div>`); | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|       expectContain(completions, CompletionKind.ATTRIBUTE, ['ngModel']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should be able to complete a the RHS of a two-way binding', () => { | ||||
| @ -382,14 +356,46 @@ describe('completions', () => { | ||||
|       expectContain(completions, CompletionKind.PROPERTY, ['test']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with input and output', () => { | ||||
|       const m1 = mockHost.getLocationMarkerFor(PARSING_CASES, 'string-marker'); | ||||
|       const c1 = ngLS.getCompletionsAt(PARSING_CASES, m1.start); | ||||
|       expectContain(c1, CompletionKind.ATTRIBUTE, ['[model]', '(modelChange)', '[(model)]']); | ||||
|     it('should suggest property binding for input', () => { | ||||
|       // Property binding via []
 | ||||
|       mockHost.override(TEST_TEMPLATE, `<div number-model [~{cursor}]></div>`); | ||||
|       const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const c1 = ngLS.getCompletionsAt(TEST_TEMPLATE, m1.start); | ||||
|       expectContain(c1, CompletionKind.ATTRIBUTE, ['inputAlias']); | ||||
| 
 | ||||
|       const m2 = mockHost.getLocationMarkerFor(PARSING_CASES, 'number-marker'); | ||||
|       const c2 = ngLS.getCompletionsAt(PARSING_CASES, m2.start); | ||||
|       expectContain(c2, CompletionKind.ATTRIBUTE, ['[inputAlias]', '(outputAlias)']); | ||||
|       // Property binding via bind-
 | ||||
|       mockHost.override(TEST_TEMPLATE, `<div number-model bind-~{cursor}></div>`); | ||||
|       const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const c2 = ngLS.getCompletionsAt(TEST_TEMPLATE, m2.start); | ||||
|       expectContain(c2, CompletionKind.ATTRIBUTE, ['inputAlias']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should suggest event binding for output', () => { | ||||
|       // Event binding via ()
 | ||||
|       mockHost.override(TEST_TEMPLATE, `<div number-model (~{cursor})></div>`); | ||||
|       const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const c1 = ngLS.getCompletionsAt(TEST_TEMPLATE, m1.start); | ||||
|       expectContain(c1, CompletionKind.ATTRIBUTE, ['outputAlias']); | ||||
| 
 | ||||
|       // Event binding via on-
 | ||||
|       mockHost.override(TEST_TEMPLATE, `<div number-mode on-~{cursor}></div>`); | ||||
|       const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const c2 = ngLS.getCompletionsAt(TEST_TEMPLATE, m2.start); | ||||
|       expectContain(c2, CompletionKind.ATTRIBUTE, ['outputAlias']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should suggest two-way binding for input and output', () => { | ||||
|       // Banana-in-a-box via [()]
 | ||||
|       mockHost.override(TEST_TEMPLATE, `<div string-model [(~{cursor})]></div>`); | ||||
|       const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const c1 = ngLS.getCompletionsAt(TEST_TEMPLATE, m1.start); | ||||
|       expectContain(c1, CompletionKind.ATTRIBUTE, ['model']); | ||||
| 
 | ||||
|       // Banana-in-a-box via bindon-
 | ||||
|       mockHost.override(TEST_TEMPLATE, `<div string-model bindon-~{cursor}></div>`); | ||||
|       const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); | ||||
|       const c2 = ngLS.getCompletionsAt(TEST_TEMPLATE, m2.start); | ||||
|       expectContain(c2, CompletionKind.ATTRIBUTE, ['model']); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -543,7 +549,7 @@ describe('completions', () => { | ||||
|         @Component({ | ||||
|           selector: 'foo-component', | ||||
|           template: \` | ||||
|             <div cl~{click}></div> | ||||
|             <div (cl~{click})></div> | ||||
|           \`,
 | ||||
|         }) | ||||
|         export class FooComponent {} | ||||
| @ -551,9 +557,9 @@ describe('completions', () => { | ||||
|       const location = mockHost.getLocationMarkerFor(fileName, 'click'); | ||||
|       const completions = ngLS.getCompletionsAt(fileName, location.start) !; | ||||
|       expect(completions).toBeDefined(); | ||||
|       const completion = completions.entries.find(entry => entry.name === '(click)') !; | ||||
|       const completion = completions.entries.find(entry => entry.name === 'click') !; | ||||
|       expect(completion).toBeDefined(); | ||||
|       expect(completion.kind).toBe('attribute'); | ||||
|       expect(completion.kind).toBe(CompletionKind.ATTRIBUTE); | ||||
|       expect(completion.replacementSpan).toEqual({start: location.start - 2, length: 2}); | ||||
|     }); | ||||
| 
 | ||||
| @ -602,7 +608,7 @@ describe('completions', () => { | ||||
|         @Component({ | ||||
|           selector: 'foo-component', | ||||
|           template: \` | ||||
|             <input ngMod~{model} /> | ||||
|             <input [(ngMod~{model})] /> | ||||
|           \`,
 | ||||
|         }) | ||||
|         export class FooComponent {} | ||||
| @ -610,12 +616,51 @@ describe('completions', () => { | ||||
|       const location = mockHost.getLocationMarkerFor(fileName, 'model'); | ||||
|       const completions = ngLS.getCompletionsAt(fileName, location.start) !; | ||||
|       expect(completions).toBeDefined(); | ||||
|       const completion = completions.entries.find(entry => entry.name === '[(ngModel)]') !; | ||||
|       const completion = completions.entries.find(entry => entry.name === 'ngModel') !; | ||||
|       expect(completion).toBeDefined(); | ||||
|       expect(completion.kind).toBe('attribute'); | ||||
|       expect(completion.kind).toBe(CompletionKind.ATTRIBUTE); | ||||
|       expect(completion.replacementSpan).toEqual({start: location.start - 5, length: 5}); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('property completions for members of an indexed type', () => { | ||||
|     it('should work with numeric index signatures (arrays)', () => { | ||||
|       mockHost.override(TEST_TEMPLATE, `{{ heroes[0].~{heroes-number-index}}}`); | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-number-index'); | ||||
|       const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|       expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with numeric index signatures (tuple arrays)', () => { | ||||
|       mockHost.override(TEST_TEMPLATE, `{{ tupleArray[1].~{tuple-array-number-index}}}`); | ||||
|       const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'tuple-array-number-index'); | ||||
|       const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|       expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|     }); | ||||
| 
 | ||||
|     describe('with string index signatures', () => { | ||||
|       it('should work with index notation', () => { | ||||
|         mockHost.override(TEST_TEMPLATE, `{{ heroesByName['Jacky'].~{heroes-string-index}}}`); | ||||
|         const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index'); | ||||
|         const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|         expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|       }); | ||||
| 
 | ||||
|       it('should work with dot notation', () => { | ||||
|         mockHost.override(TEST_TEMPLATE, `{{ heroesByName.jacky.~{heroes-string-index}}}`); | ||||
|         const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index'); | ||||
|         const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|         expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); | ||||
|       }); | ||||
| 
 | ||||
|       it('should work with dot notation if stringIndexType is a primitive type', () => { | ||||
|         mockHost.override(TEST_TEMPLATE, `{{ primitiveIndexType.test.~{string-primitive-type}}}`); | ||||
|         const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'string-primitive-type'); | ||||
|         const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); | ||||
|         expectContain(completions, CompletionKind.METHOD, ['substring']); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| function expectContain( | ||||
|  | ||||
| @ -36,7 +36,6 @@ import * as ParsingCases from './parsing-cases'; | ||||
|     ParsingCases.CaseUnknown, | ||||
|     ParsingCases.EmptyInterpolation, | ||||
|     ParsingCases.EventBinding, | ||||
|     ParsingCases.FooComponent, | ||||
|     ParsingCases.ForLetIEqual, | ||||
|     ParsingCases.ForOfLetEmpty, | ||||
|     ParsingCases.ForUsingComponent, | ||||
|  | ||||
| @ -93,18 +93,6 @@ export class NumberModel { | ||||
|   @Output('outputAlias') modelChange: EventEmitter<number> = new EventEmitter(); | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'foo-component', | ||||
|   template: ` | ||||
|     <div string-model ~{string-marker}="text"></div> | ||||
|     <div number-model ~{number-marker}="value"></div> | ||||
|   `,
 | ||||
| }) | ||||
| export class FooComponent { | ||||
|   text: string = 'some text'; | ||||
|   value: number = 42; | ||||
| } | ||||
| 
 | ||||
| interface Person { | ||||
|   name: string; | ||||
|   age: number; | ||||
|  | ||||
| @ -4,7 +4,4 @@ | ||||
| </h1> | ||||
| ~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2> | ||||
| <div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div> | ||||
| <div ~{div-attributes}> | ||||
|   <label>name: </label> | ||||
| </div> | ||||
| &~{entity-amp}amp; | ||||
|  | ||||
| @ -94,7 +94,7 @@ describe('TypeScriptServiceHost', () => { | ||||
|     const tsLS = ts.createLanguageService(tsLSHost); | ||||
|     const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS); | ||||
|     const templates = ngLSHost.getTemplates('/app/parsing-cases.ts'); | ||||
|     expect(templates.length).toBe(17); | ||||
|     expect(templates.length).toBe(16); | ||||
|   }); | ||||
| 
 | ||||
|   it('should be able to find external template', () => { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user