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, ` +
+ {{~{cursor}}} +
+ `); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.VARIABLE, ['h']); }); it('should be able to infer the type of a ngForOf', () => { - for (const location of ['for-interp-name', 'for-interp-age']) { - const marker = mockHost.getLocationMarkerFor(PARSING_CASES, location); - const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start); - expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']); - } + mockHost.override(TEST_TEMPLATE, ` +
+ {{ h.~{cursor} }} +
+ `); + const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); + const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); + expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']); }); it('should be able to infer the type of a ngForOf with an async pipe', () => { diff --git a/packages/language-service/test/project/app/main.ts b/packages/language-service/test/project/app/main.ts index 6ef6479ace..701780c7c4 100644 --- a/packages/language-service/test/project/app/main.ts +++ b/packages/language-service/test/project/app/main.ts @@ -36,9 +36,6 @@ import * as ParsingCases from './parsing-cases'; ParsingCases.CaseUnknown, ParsingCases.EmptyInterpolation, ParsingCases.EventBinding, - ParsingCases.ForLetIEqual, - ParsingCases.ForOfLetEmpty, - ParsingCases.ForUsingComponent, ParsingCases.NoValueAttribute, ParsingCases.NumberModel, ParsingCases.Pipes, diff --git a/packages/language-service/test/project/app/parsing-cases.ts b/packages/language-service/test/project/app/parsing-cases.ts index 537574f70f..7b2b3fe36c 100644 --- a/packages/language-service/test/project/app/parsing-cases.ts +++ b/packages/language-service/test/project/app/parsing-cases.ts @@ -99,29 +99,6 @@ interface Person { street: string; } -@Component({ - template: '
', -}) -export class ForOfLetEmpty { -} - -@Component({ - template: '
', -}) -export class ForLetIEqual { -} - -@Component({ - template: ` -
- Name: {{~{for-interp-person}person.~{for-interp-name}name}} - Age: {{person.~{for-interp-age}age}} -
`, -}) -export class ForUsingComponent { - people: Person[] = []; -} - @Component({ template: `
diff --git a/packages/language-service/test/typescript_host_spec.ts b/packages/language-service/test/typescript_host_spec.ts index f9527c2d8e..816bb81f34 100644 --- a/packages/language-service/test/typescript_host_spec.ts +++ b/packages/language-service/test/typescript_host_spec.ts @@ -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(16); + expect(templates.length).toBe(13); }); it('should be able to find external template', () => {