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
This commit is contained in:
Alex Rickabaugh 2020-10-13 11:14:13 -07:00 committed by Misko Hevery
parent 67c5fe06e6
commit 28a0bcb424
7 changed files with 560 additions and 8 deletions

View File

@ -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`.

View File

@ -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 {
/**

View File

@ -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 {

View File

@ -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<PropertyRead|MethodCall|EmptyExpr|LiteralPrimitive>;
/**
* 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<N extends TmplAstNode|AST> {
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<ts.CompletionInfo>|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<TmplAstNode|AST>):
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<ts.CompletionInfo>|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<ts.CompletionInfo>|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';
}

View File

@ -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<TmplAstNode|AST>|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<ts.CompletionInfo>|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,

View File

@ -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('<h1 [model]="ti¦"></h1>', `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('<h1 [model]="¦"></h1>', `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(`<div #todo></div>{{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(
`<div *ngFor="let hero of heroes">
{{h¦}}
</div>
`,
`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(`<button (click)='t¦'></button>`, `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(`<button (click)='¦'></button>`, `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(`<h1 [(model)]="t¦"></h1>`, `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(`<h1 [(model)]="¦"></h1>`, `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,
};
}

View File

@ -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<ts.CompletionInfo>|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,
};
}