diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index 79ea098da065d..845dba5d97db7 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -419,11 +419,9 @@ class ExpressionVisitor extends NullTemplateVisitor { } visitAttr(ast: AttrAst) { - // The attribute value is a template expression but the expression AST - // was not produced when the TemplateAst was produced so do that here. + // First, verify the attribute consists of some binding we can give completions for. const {templateBindings} = this.info.expressionParser.parseTemplateBindings( ast.name, ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset); - // Find where the cursor is relative to the start of the attribute value. const valueRelativePosition = this.position - ast.sourceSpan.start.offset; // Find the template binding that contains the position @@ -436,12 +434,11 @@ class ExpressionVisitor extends NullTemplateVisitor { if (ast.name.startsWith('*')) { this.microSyntaxInAttributeValue(ast, binding); } else { - // If the position is in the expression or after the key or there is no key, - // return the expression completions - const span = new ParseSpan(0, ast.value.length); - const offset = ast.sourceSpan.start.offset; - const receiver = new ImplicitReceiver(span, span.toAbsolute(offset)); - const expressionAst = new PropertyRead(span, span.toAbsolute(offset), receiver, ''); + // If the position is in the expression or after the key or there is no key, return the + // expression completions. + // The expression must be reparsed to get a valid AST rather than only template bindings. + const expressionAst = this.info.expressionParser.parseBinding( + ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset); this.addAttributeValuesToCompletions(expressionAst); } } @@ -538,10 +535,8 @@ class ExpressionVisitor extends NullTemplateVisitor { const KW_OF = ' of '; const ofLocation = attr.value.indexOf(KW_OF); if (ofLocation > 0 && valueRelativePosition >= ofLocation + KW_OF.length) { - const span = new ParseSpan(0, attr.value.length); - const offset = attr.sourceSpan.start.offset; - const receiver = new ImplicitReceiver(span, span.toAbsolute(offset)); - const expressionAst = new PropertyRead(span, span.toAbsolute(offset), receiver, ''); + const expressionAst = this.info.expressionParser.parseBinding( + attr.value, attr.sourceSpan.toString(), attr.sourceSpan.start.offset); this.addAttributeValuesToCompletions(expressionAst); } } diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index e94977dd9f094..631137677b6ef 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -308,6 +308,13 @@ describe('completions', () => { expectContain(completions, CompletionKind.VARIABLE, ['x']); }); + it('should include expression completions', () => { + mockHost.override(TEST_TEMPLATE, `
`); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'expr-property-read'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['name']); + }); + it('should include variable in the let scope in interpolation', () => { mockHost.override(TEST_TEMPLATE, `
@@ -358,6 +365,13 @@ describe('completions', () => { expectContain(completions, CompletionKind.PROPERTY, ['test']); }); + it('should be able to complete property read', () => { + mockHost.override(TEST_TEMPLATE, `

`); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'property-read'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); + }); + it('should be able to complete an event', () => { const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'event-binding-model'); const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); @@ -459,6 +473,16 @@ describe('completions', () => { expectContain(completions, CompletionKind.PROPERTY, ['name', 'testEvent']); }); + it('should get reference property completions in a data binding', () => { + mockHost.override(TEST_TEMPLATE, ` + +
+ `); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'property-read'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['name', 'testEvent']); + }); + // TODO: Enable when we have a flag that indicates the project targets the DOM // it('should reference the element if no component', () => { // const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'test-comp-after-div');