diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index 622083edb1..79ea098da0 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -511,7 +511,7 @@ class ExpressionVisitor extends NullTemplateVisitor { if (binding.keyIsVar) { const equalLocation = attr.value.indexOf('='); - if (equalLocation >= 0 && valueRelativePosition >= equalLocation) { + if (equalLocation > 0 && valueRelativePosition > equalLocation) { // We are after the '=' in a let clause. The valid values here are the members of the // template reference's type parameter. const directiveMetadata = selectorInfo.map.get(selector); @@ -531,6 +531,19 @@ class ExpressionVisitor extends NullTemplateVisitor { this.addAttributeValuesToCompletions(binding.expression.ast); return; } + + // If the expression is incomplete, for example *ngFor="let x of |" + // binding.expression is null. We could still try to provide suggestions + // by looking for symbols that are in scope. + 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, ''); + this.addAttributeValuesToCompletions(expressionAst); + } } } diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index 3bce45f1b2..e94977dd9f 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -118,7 +118,7 @@ describe('completions', () => { expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); }); - it('should suggest template refereces', () => { + it('should suggest template references', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); @@ -275,8 +275,9 @@ describe('completions', () => { describe('with a *ngFor', () => { it('should suggest NgForRow members for let initialization expression', () => { - const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-let-i-equal'); - const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); + mockHost.override(TEST_TEMPLATE, ``); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); expectContain(completions, CompletionKind.PROPERTY, [ '$implicit', 'ngForOf', @@ -289,24 +290,44 @@ describe('completions', () => { ]); }); - it('should include field reference', () => { - const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-people'); - const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); - expectContain(completions, CompletionKind.PROPERTY, ['people']); + it('should not provide suggestion before the = sign', () => { + mockHost.override(TEST_TEMPLATE, ``); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expect(completions).toBeUndefined(); }); - it('should include person in the let scope', () => { - const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-interp-person'); - const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); - expectContain(completions, CompletionKind.VARIABLE, ['person']); + it('should include field reference', () => { + mockHost.override(TEST_TEMPLATE, ``); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['title', 'heroes', 'league']); + // the symbol 'x' declared in *ngFor is also in scope. This asserts that + // we are actually taking the AST into account and not just referring to + // the symbol table of the Component. + expectContain(completions, CompletionKind.VARIABLE, ['x']); + }); + + it('should include variable in the let scope in interpolation', () => { + mockHost.override(TEST_TEMPLATE, ` +