From 28a0bcb424e4d64df0238986e34ed11adeeea866 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 13 Oct 2020 11:14:13 -0700 Subject: [PATCH] feat(language-service): implement autocompletion for global properties (Ivy) (#39250) This commit adds support in the Ivy Language Service for autocompletion in a global context - e.g. a {{foo|}} completion. Support is added both for the primary function `getCompletionsAtPosition` as well as the detail functions `getCompletionEntryDetails` and `getCompletionEntrySymbol`. These latter operations are not used yet as an upstream change to the extension is required to advertise and support this capability. PR Close #39250 --- .../src/ngtsc/typecheck/api/checker.ts | 2 +- .../src/ngtsc/typecheck/api/symbols.ts | 5 + .../src/ngtsc/typecheck/src/checker.ts | 8 +- packages/language-service/ivy/completions.ts | 287 ++++++++++++++++++ .../language-service/ivy/language_service.ts | 55 +++- .../ivy/test/completions_spec.ts | 169 +++++++++++ packages/language-service/ivy/ts_plugin.ts | 42 +++ 7 files changed, 560 insertions(+), 8 deletions(-) create mode 100644 packages/language-service/ivy/completions.ts create mode 100644 packages/language-service/ivy/test/completions_spec.ts diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index a6bc3c4c40..f04bc887d8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -50,7 +50,7 @@ export interface TemplateTypeChecker { * is not valid. If the template cannot be parsed correctly, no override will occur. */ overrideComponentTemplate(component: ts.ClassDeclaration, template: string): - {nodes: TmplAstNode[], errors?: ParseError[]}; + {nodes: TmplAstNode[], errors: ParseError[]|null}; /** * Get all `ts.Diagnostic`s currently available for the given `ts.SourceFile`. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts index af72642041..5f8bcd7b8b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts @@ -32,6 +32,11 @@ export enum SymbolKind { export type Symbol = InputBindingSymbol|OutputBindingSymbol|ElementSymbol|ReferenceSymbol| VariableSymbol|ExpressionSymbol|DirectiveSymbol|TemplateSymbol|DomBindingSymbol; +/** + * A `Symbol` which declares a new named entity in the template scope. + */ +export type TemplateDeclarationSymbol = ReferenceSymbol|VariableSymbol; + /** Information about where a `ts.Node` can be found in the type check block shim file. */ export interface ShimLocation { /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 4260a7b402..69f4066c22 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -138,16 +138,12 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } overrideComponentTemplate(component: ts.ClassDeclaration, template: string): - {nodes: TmplAstNode[], errors?: ParseError[]} { + {nodes: TmplAstNode[], errors: ParseError[]|null} { const {nodes, errors} = parseTemplate(template, 'override.html', { preserveWhitespaces: true, leadingTriviaChars: [], }); - if (errors !== null) { - return {nodes, errors}; - } - const filePath = absoluteFromSourceFile(component.getSourceFile()); const fileRecord = this.getFileData(filePath); @@ -169,7 +165,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { this.completionCache.delete(component); this.symbolBuilderCache.delete(component); - return {nodes}; + return {nodes, errors}; } isTrackedTypeCheckFile(filePath: AbsoluteFsPath): boolean { diff --git a/packages/language-service/ivy/completions.ts b/packages/language-service/ivy/completions.ts new file mode 100644 index 0000000000..c3cb1ef04c --- /dev/null +++ b/packages/language-service/ivy/completions.ts @@ -0,0 +1,287 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AST, EmptyExpr, ImplicitReceiver, LiteralPrimitive, MethodCall, PropertyRead, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; +import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; +import {CompletionKind, TemplateDeclarationSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import {BoundEvent} from '@angular/compiler/src/render3/r3_ast'; +import * as ts from 'typescript'; + +import {DisplayInfoKind, getDisplayInfo, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts'; + +type PropertyExpressionCompletionBuilder = + CompletionBuilder; + +/** + * Performs autocompletion operations on a given node in the template. + * + * This class acts as a closure around all of the context required to perform the 3 autocompletion + * operations (completions, get details, and get symbol) at a specific node. + * + * The generic `N` type for the template node is narrowed internally for certain operations, as the + * compiler operations required to implement completion may be different for different node types. + * + * @param N type of the template node in question, narrowed accordingly. + */ +export class CompletionBuilder { + private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker(); + private readonly templateTypeChecker = this.compiler.getTemplateTypeChecker(); + + constructor( + private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler, + private readonly component: ts.ClassDeclaration, private readonly node: N, + private readonly nodeParent: TmplAstNode|AST|null, + private readonly context: TmplAstTemplate|null) {} + + /** + * Analogue for `ts.LanguageService.getCompletionsAtPosition`. + */ + getCompletionsAtPosition(options: ts.GetCompletionsAtPositionOptions| + undefined): ts.WithMetadata|undefined { + if (this.isPropertyExpressionCompletion()) { + return this.getPropertyExpressionCompletion(options); + } else { + return undefined; + } + } + + /** + * Analogue for `ts.LanguageService.getCompletionEntryDetails`. + */ + getCompletionEntryDetails( + entryName: string, formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, + preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { + if (this.isPropertyExpressionCompletion()) { + return this.getPropertyExpressionCompletionDetails(entryName, formatOptions, preferences); + } else { + return undefined; + } + } + + /** + * Analogue for `ts.LanguageService.getCompletionEntrySymbol`. + */ + getCompletionEntrySymbol(name: string): ts.Symbol|undefined { + if (this.isPropertyExpressionCompletion()) { + return this.getPropertyExpressionCompletionSymbol(name); + } else { + return undefined; + } + } + + /** + * Determine if the current node is the completion of a property expression, and narrow the type + * of `this.node` if so. + * + * This narrowing gives access to additional methods related to completion of property + * expressions. + */ + private isPropertyExpressionCompletion(this: CompletionBuilder): + this is PropertyExpressionCompletionBuilder { + return this.node instanceof PropertyRead || this.node instanceof MethodCall || + this.node instanceof EmptyExpr || + isBrokenEmptyBoundEventExpression(this.node, this.nodeParent); + } + + /** + * Get completions for property expressions. + */ + private getPropertyExpressionCompletion( + this: PropertyExpressionCompletionBuilder, + options: ts.GetCompletionsAtPositionOptions| + undefined): ts.WithMetadata|undefined { + if (this.node instanceof EmptyExpr || + isBrokenEmptyBoundEventExpression(this.node, this.nodeParent) || + this.node.receiver instanceof ImplicitReceiver) { + return this.getGlobalPropertyExpressionCompletion(options); + } else { + // TODO(alxhub): implement completion of non-global expressions. + return undefined; + } + } + + /** + * Get the details of a specific completion for a property expression. + */ + private getPropertyExpressionCompletionDetails( + this: PropertyExpressionCompletionBuilder, entryName: string, + formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, + preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { + if (this.node instanceof EmptyExpr || + isBrokenEmptyBoundEventExpression(this.node, this.nodeParent) || + this.node.receiver instanceof ImplicitReceiver) { + return this.getGlobalPropertyExpressionCompletionDetails( + entryName, formatOptions, preferences); + } else { + // TODO(alxhub): implement completion of non-global expressions. + return undefined; + } + } + + /** + * Get the `ts.Symbol` for a specific completion for a property expression. + */ + private getPropertyExpressionCompletionSymbol( + this: PropertyExpressionCompletionBuilder, name: string): ts.Symbol|undefined { + if (this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive || + this.node.receiver instanceof ImplicitReceiver) { + return this.getGlobalPropertyExpressionCompletionSymbol(name); + } else { + // TODO(alxhub): implement completion of non-global expressions. + return undefined; + } + } + + /** + * Get completions for a property expression in a global context (e.g. `{{y|}}`). + */ + private getGlobalPropertyExpressionCompletion( + this: PropertyExpressionCompletionBuilder, + options: ts.GetCompletionsAtPositionOptions| + undefined): ts.WithMetadata|undefined { + const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component); + if (completions === null) { + return undefined; + } + + const {componentContext, templateContext} = completions; + + let replacementSpan: ts.TextSpan|undefined = undefined; + // Non-empty nodes get replaced with the completion. + if (!(this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive)) { + replacementSpan = { + start: this.node.nameSpan.start, + length: this.node.nameSpan.end - this.node.nameSpan.start, + }; + } + + // Merge TS completion results with results from the template scope. + let entries: ts.CompletionEntry[] = []; + const tsLsCompletions = this.tsLS.getCompletionsAtPosition( + componentContext.shimPath, componentContext.positionInShimFile, options); + if (tsLsCompletions !== undefined) { + for (const tsCompletion of tsLsCompletions.entries) { + // Skip completions that are shadowed by a template entity definition. + if (templateContext.has(tsCompletion.name)) { + continue; + } + entries.push({ + ...tsCompletion, + // Substitute the TS completion's `replacementSpan` (which uses offsets within the TCB) + // with the `replacementSpan` within the template source. + replacementSpan, + }); + } + } + + for (const [name, entity] of templateContext) { + entries.push({ + name, + sortText: name, + replacementSpan, + kindModifiers: ts.ScriptElementKindModifier.none, + kind: unsafeCastDisplayInfoKindToScriptElementKind( + entity.kind === CompletionKind.Reference ? DisplayInfoKind.REFERENCE : + DisplayInfoKind.VARIABLE), + }); + } + + return { + entries, + // Although this completion is "global" in the sense of an Angular expression (there is no + // explicit receiver), it is not "global" in a TypeScript sense since Angular expressions have + // the component as an implicit receiver. + isGlobalCompletion: false, + isMemberCompletion: true, + isNewIdentifierLocation: false, + }; + } + + /** + * Get the details of a specific completion for a property expression in a global context (e.g. + * `{{y|}}`). + */ + private getGlobalPropertyExpressionCompletionDetails( + this: PropertyExpressionCompletionBuilder, entryName: string, + formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, + preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { + const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component); + if (completions === null) { + return undefined; + } + const {componentContext, templateContext} = completions; + + if (templateContext.has(entryName)) { + const entry = templateContext.get(entryName)!; + // Entries that reference a symbol in the template context refer either to local references or + // variables. + const symbol = this.templateTypeChecker.getSymbolOfNode(entry.node, this.component) as + TemplateDeclarationSymbol | + null; + if (symbol === null) { + return undefined; + } + + const {kind, displayParts, documentation} = + getDisplayInfo(this.tsLS, this.typeChecker, symbol); + return { + kind: unsafeCastDisplayInfoKindToScriptElementKind(kind), + name: entryName, + kindModifiers: ts.ScriptElementKindModifier.none, + displayParts, + documentation, + }; + } else { + return this.tsLS.getCompletionEntryDetails( + componentContext.shimPath, componentContext.positionInShimFile, entryName, formatOptions, + /* source */ undefined, preferences); + } + } + + /** + * Get the `ts.Symbol` of a specific completion for a property expression in a global context + * (e.g. + * `{{y|}}`). + */ + private getGlobalPropertyExpressionCompletionSymbol( + this: PropertyExpressionCompletionBuilder, entryName: string): ts.Symbol|undefined { + const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component); + if (completions === null) { + return undefined; + } + const {componentContext, templateContext} = completions; + if (templateContext.has(entryName)) { + const node: TmplAstReference|TmplAstVariable = templateContext.get(entryName)!.node; + const symbol = this.templateTypeChecker.getSymbolOfNode(node, this.component) as + TemplateDeclarationSymbol | + null; + if (symbol === null || symbol.tsSymbol === null) { + return undefined; + } + return symbol.tsSymbol; + } else { + return this.tsLS.getCompletionEntrySymbol( + componentContext.shimPath, componentContext.positionInShimFile, entryName, + /* source */ undefined); + } + } +} + +/** + * Checks whether the given `node` is (most likely) a synthetic node created by the template parser + * for an empty event binding `(event)=""`. + * + * When parsing such an expression, a synthetic `LiteralPrimitive` node is generated for the + * `BoundEvent`'s handler with the literal text value 'ERROR'. Detecting this case is crucial to + * supporting completions within empty event bindings. + */ +function isBrokenEmptyBoundEventExpression( + node: TmplAstNode|AST, parent: TmplAstNode|AST|null): node is LiteralPrimitive { + return node instanceof LiteralPrimitive && parent !== null && parent instanceof BoundEvent && + node.value === 'ERROR'; +} diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index b1612cf80a..665607c9d0 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -6,17 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {AST, TmplAstNode} from '@angular/compiler'; import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli'; import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck'; import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; -import {ReferenceBuilder} from '@angular/language-service/ivy/references'; import * as ts from 'typescript/lib/tsserverlibrary'; import {LanguageServiceAdapter, LSParseConfigHost} from './adapters'; import {CompilerFactory} from './compiler_factory'; +import {CompletionBuilder} from './completions'; import {DefinitionBuilder} from './definitions'; import {QuickInfoBuilder} from './quick_info'; +import {ReferenceBuilder} from './references'; import {getTargetAtPosition} from './template_target'; import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils'; @@ -105,6 +107,57 @@ export class LanguageService { return results; } + private getCompletionBuilder(fileName: string, position: number): + CompletionBuilder|null { + const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); + if (templateInfo === undefined) { + return null; + } + const positionDetails = getTargetAtPosition(templateInfo.template, position); + if (positionDetails === null) { + return null; + } + return new CompletionBuilder( + this.tsLS, compiler, templateInfo.component, positionDetails.node, positionDetails.parent, + positionDetails.context); + } + + getCompletionsAtPosition( + fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined): + ts.WithMetadata|undefined { + const builder = this.getCompletionBuilder(fileName, position); + if (builder === null) { + return undefined; + } + const result = builder.getCompletionsAtPosition(options); + this.compilerFactory.registerLastKnownProgram(); + return result; + } + + getCompletionEntryDetails( + fileName: string, position: number, entryName: string, + formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, + preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { + const builder = this.getCompletionBuilder(fileName, position); + if (builder === null) { + return undefined; + } + const result = builder.getCompletionEntryDetails(entryName, formatOptions, preferences); + this.compilerFactory.registerLastKnownProgram(); + return result; + } + + getCompletionEntrySymbol(fileName: string, position: number, name: string): ts.Symbol|undefined { + const builder = this.getCompletionBuilder(fileName, position); + if (builder === null) { + return undefined; + } + const result = builder.getCompletionEntrySymbol(name); + this.compilerFactory.registerLastKnownProgram(); + return result; + } + private watchConfigFile(project: ts.server.Project) { // TODO: Check the case when the project is disposed. An InferredProject // could be disposed when a tsconfig.json is added to the workspace, diff --git a/packages/language-service/ivy/test/completions_spec.ts b/packages/language-service/ivy/test/completions_spec.ts new file mode 100644 index 0000000000..b4c3923ed8 --- /dev/null +++ b/packages/language-service/ivy/test/completions_spec.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {TmplAstNode} from '@angular/compiler'; +import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import * as ts from 'typescript'; +import {DisplayInfoKind} from '../display_parts'; +import {LanguageService} from '../language_service'; + +import {LanguageServiceTestEnvironment} from './env'; + +describe('completions', () => { + beforeEach(() => { + initMockFileSystem('Native'); + }); + + describe('in the global scope', () => { + it('should be able to complete an interpolation', () => { + const {ngLS, fileName, cursor} = setup('{{ti¦}}', `title!: string; hero!: number;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title', 'hero']); + }); + + it('should be able to complete an empty interpolation', () => { + const {ngLS, fileName, cursor} = setup('{{ ¦ }}', `title!: string; hero!: number;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title', 'hero']); + }); + + it('should be able to complete a property binding', () => { + const {ngLS, fileName, cursor} = + setup('

', `title!: string; hero!: number;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title', 'hero']); + }); + + it('should be able to complete an empty property binding', () => { + const {ngLS, fileName, cursor} = + setup('

', `title!: string; hero!: number;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title', 'hero']); + }); + + it('should be able to retrieve details for completions', () => { + const {ngLS, fileName, cursor} = setup('{{ti¦}}', ` + /** This is the title of the 'AppCmp' Component. */ + title!: string; + /** This comment should not appear in the output of this test. */ + hero!: number; + `); + const details = ngLS.getCompletionEntryDetails( + fileName, cursor, 'title', /* formatOptions */ undefined, + /* preferences */ undefined)!; + expect(details).toBeDefined(); + expect(toText(details.displayParts)).toEqual('(property) AppCmp.title: string'); + expect(toText(details.documentation)) + .toEqual('This is the title of the \'AppCmp\' Component.'); + }); + + it('should return reference completions when available', () => { + const {ngLS, fileName, cursor} = setup(`
{{t¦}}`, `title!: string;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']); + expectContain(completions, DisplayInfoKind.REFERENCE, ['todo']); + }); + + it('should return variable completions when available', () => { + const {ngLS, fileName, cursor} = setup( + `
+ {{h¦}} +
+ `, + `heroes!: {name: string}[];`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['heroes']); + expectContain(completions, DisplayInfoKind.VARIABLE, ['hero']); + }); + + it('should return completions inside an event binding', () => { + const {ngLS, fileName, cursor} = setup(``, `title!: string;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']); + }); + + it('should return completions inside an empty event binding', () => { + const {ngLS, fileName, cursor} = setup(``, `title!: string;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']); + }); + + it('should return completions inside the RHS of a two-way binding', () => { + const {ngLS, fileName, cursor} = setup(`

`, `title!: string;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']); + }); + + it('should return completions inside an empty RHS of a two-way binding', () => { + const {ngLS, fileName, cursor} = setup(`

`, `title!: string;`); + const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']); + }); + }); +}); + +function expectContain( + completions: ts.CompletionInfo|undefined, kind: ts.ScriptElementKind|DisplayInfoKind, + names: string[]) { + expect(completions).toBeDefined(); + for (const name of names) { + expect(completions!.entries).toContain(jasmine.objectContaining({name, kind} as any)); + } +} + +function toText(displayParts?: ts.SymbolDisplayPart[]): string { + return (displayParts ?? []).map(p => p.text).join(''); +} + +function setup(templateWithCursor: string, classContents: string): { + env: LanguageServiceTestEnvironment, + fileName: AbsoluteFsPath, + AppCmp: ts.ClassDeclaration, + ngLS: LanguageService, + cursor: number, + nodes: TmplAstNode[], +} { + const codePath = absoluteFrom('/test.ts'); + const templatePath = absoluteFrom('/test.html'); + const env = LanguageServiceTestEnvironment.setup([ + { + name: codePath, + contents: ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + templateUrl: './test.html', + selector: 'app-cmp', + }) + export class AppCmp { + ${classContents} + } + + @NgModule({ + declarations: [AppCmp], + }) + export class AppModule {} + `, + isRoot: true, + }, + { + name: templatePath, + contents: 'Placeholder template', + } + ]); + const {nodes, cursor} = env.overrideTemplateWithCursor(codePath, 'AppCmp', templateWithCursor); + return { + env, + fileName: templatePath, + AppCmp: env.getClass(codePath, 'AppCmp'), + ngLS: env.ngLS, + nodes, + cursor, + }; +} \ No newline at end of file diff --git a/packages/language-service/ivy/ts_plugin.ts b/packages/language-service/ivy/ts_plugin.ts index a7d5fee16f..c21a04da47 100644 --- a/packages/language-service/ivy/ts_plugin.ts +++ b/packages/language-service/ivy/ts_plugin.ts @@ -68,6 +68,45 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService { return undefined; } + function getCompletionsAtPosition( + fileName: string, position: number, + options: ts.GetCompletionsAtPositionOptions): ts.WithMetadata|undefined { + if (angularOnly) { + return ngLS.getCompletionsAtPosition(fileName, position, options); + } else { + // If TS could answer the query, then return that result. Otherwise, return from Angular LS. + return tsLS.getCompletionsAtPosition(fileName, position, options) ?? + ngLS.getCompletionsAtPosition(fileName, position, options); + } + } + + function getCompletionEntryDetails( + fileName: string, position: number, entryName: string, + formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, source: string|undefined, + preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { + if (angularOnly) { + return ngLS.getCompletionEntryDetails( + fileName, position, entryName, formatOptions, preferences); + } else { + // If TS could answer the query, then return that result. Otherwise, return from Angular LS. + return tsLS.getCompletionEntryDetails( + fileName, position, entryName, formatOptions, source, preferences) ?? + ngLS.getCompletionEntryDetails(fileName, position, entryName, formatOptions, preferences); + } + } + + function getCompletionEntrySymbol( + fileName: string, position: number, name: string, source: string|undefined): ts.Symbol| + undefined { + if (angularOnly) { + return ngLS.getCompletionEntrySymbol(fileName, position, name); + } else { + // If TS could answer the query, then return that result. Otherwise, return from Angular LS. + return tsLS.getCompletionEntrySymbol(fileName, position, name, source) ?? + ngLS.getCompletionEntrySymbol(fileName, position, name); + } + } + return { ...tsLS, getSemanticDiagnostics, @@ -76,5 +115,8 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService { getDefinitionAndBoundSpan, getReferencesAtPosition, findRenameLocations, + getCompletionsAtPosition, + getCompletionEntryDetails, + getCompletionEntrySymbol, }; }