From 65f67f50121488f4f1efa3121919e86fb6312d6e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 5 Jul 2021 23:26:14 +0200 Subject: [PATCH 1/2] Don't pass deprecated options to Completion request The deprecated options "includeExternalModuleExports" and "includeInsertTextCompletions" are now passed as global, non-deprecated typescript preferences. To achieve that, added those as default preferences to the preferences object that the user can override. Also switch to non-deprecated "CompletionInfo" request and handle "isIncomplete" flag (I don't know when typescript actually makes that "true") and fix some Completion-related types (use LSP types instead of TSC types). --- server/src/completion.ts | 2 +- server/src/lsp-server.spec.ts | 85 ++++++++++++++++++++++++++--------- server/src/lsp-server.ts | 33 ++++++++------ server/src/test-utils.ts | 4 ++ 4 files changed, 87 insertions(+), 37 deletions(-) diff --git a/server/src/completion.ts b/server/src/completion.ts index 6b38669f..fc7626f1 100644 --- a/server/src/completion.ts +++ b/server/src/completion.ts @@ -145,7 +145,7 @@ function asCommitCharacters(kind: ScriptElementKind): string[] | undefined { return commitCharacters.length === 0 ? undefined : commitCharacters; } -export function asResolvedCompletionItem(item: TSCompletionItem, details: tsp.CompletionEntryDetails): TSCompletionItem { +export function asResolvedCompletionItem(item: lsp.CompletionItem, details: tsp.CompletionEntryDetails): lsp.CompletionItem { item.detail = asDetail(details); item.documentation = asDocumentation(details); Object.assign(item, asCodeActions(details, item.data.file)); diff --git a/server/src/lsp-server.spec.ts b/server/src/lsp-server.spec.ts index 54d68ff9..19175145 100644 --- a/server/src/lsp-server.spec.ts +++ b/server/src/lsp-server.spec.ts @@ -9,7 +9,7 @@ import * as chai from 'chai'; import * as lsp from 'vscode-languageserver/node'; import * as lspcalls from './lsp-protocol.calls.proposed'; import { LspServer } from './lsp-server'; -import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities } from './test-utils'; +import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities, positionAfter } from './test-utils'; import { TSCompletionItem } from './completion'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -51,14 +51,13 @@ describe('completion', () => { textDocument: doc }); const pos = position(doc, 'console'); - const proposals = await server.completion({ - textDocument: doc, - position: pos - }) as TSCompletionItem[]; - assert.isTrue(proposals.length > 800, String(proposals.length)); - const item = proposals.filter(i => i.label === 'addEventListener')[0]; - const resolvedItem = await server.completionResolve(item); - assert.isTrue(resolvedItem.detail !== undefined, JSON.stringify(resolvedItem, undefined, 2)); + const proposals = await server.completion({ textDocument: doc, position: pos }); + assert.isNotNull(proposals); + assert.isAbove(proposals!.items.length, 800); + const item = proposals!.items.find(i => i.label === 'addEventListener'); + assert.isDefined(item); + const resolvedItem = await server.completionResolve(item!); + assert.isDefined(resolvedItem.detail, JSON.stringify(resolvedItem, undefined, 2)); server.didCloseTextDocument({ textDocument: doc }); @@ -79,16 +78,14 @@ describe('completion', () => { textDocument: doc }); const pos = position(doc, 'console'); - const proposals = await server.completion({ - textDocument: doc, - position: pos - }) as TSCompletionItem[]; - assert.isTrue(proposals.length > 800, String(proposals.length)); - const item = proposals.filter(i => i.label === 'addEventListener')[0]; - const resolvedItem = await server.completionResolve(item); + const proposals = await server.completion({ textDocument: doc, position: pos }); + assert.isNotNull(proposals); + assert.isAbove(proposals!.items.length, 800); + const item = proposals!.items.find(i => i.label === 'addEventListener'); + const resolvedItem = await server.completionResolve(item!); assert.isTrue(resolvedItem.detail !== undefined, JSON.stringify(resolvedItem, undefined, 2)); - const containsInvalidCompletions = proposals.reduce((accumulator, current) => { + const containsInvalidCompletions = proposals!.items.reduce((accumulator, current) => { if (accumulator) { return accumulator; } @@ -119,11 +116,55 @@ describe('completion', () => { textDocument: doc }); const pos = position(doc, 'foo'); - const proposals = await server.completion({ - textDocument: doc, - position: pos - }) as TSCompletionItem[]; - assert.isTrue(proposals === null); + const proposals = await server.completion({ textDocument: doc, position: pos }); + assert.isNull(proposals); + server.didCloseTextDocument({ + textDocument: doc + }); + }).timeout(10000); + + it('includes completions from global modules', async () => { + const doc = { + uri: uri('bar.ts'), + languageId: 'typescript', + version: 1, + text: 'pathex' + }; + server.didOpenTextDocument({ + textDocument: doc + }); + const proposals = await server.completion({ textDocument: doc, position: position(doc, 'ex') }); + assert.isNotNull(proposals); + const pathExistsCompletion = proposals!.items.find(completion => completion.label === 'pathExists'); + assert.isDefined(pathExistsCompletion); + server.didCloseTextDocument({ + textDocument: doc + }); + }).timeout(10000); + + it('includes completions with invalid identifier names', async () => { + const doc = { + uri: uri('bar.ts'), + languageId: 'typescript', + version: 1, + text: ` + interface Foo { + 'invalid-identifier-name': string + } + + const foo: Foo + foo.i + ` + }; + server.didOpenTextDocument({ + textDocument: doc + }); + const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, '.i') }); + assert.isNotNull(proposals); + const completion = proposals!.items.find(completion => completion.label === 'invalid-identifier-name'); + assert.isDefined(completion); + assert.isDefined(completion!.textEdit); + assert.equal(completion!.textEdit!.newText, '["invalid-identifier-name"]'); server.didCloseTextDocument({ textDocument: doc }); diff --git a/server/src/lsp-server.ts b/server/src/lsp-server.ts index e18519a6..742be373 100644 --- a/server/src/lsp-server.ts +++ b/server/src/lsp-server.ts @@ -104,12 +104,18 @@ export class LspServer { ); } - const { logVerbosity, plugins, preferences, hostInfo }: TypeScriptInitializationOptions = { - logVerbosity: this.options.tsserverLogVerbosity, - plugins: [], - preferences: {}, - ...this.initializeParams.initializationOptions + const userInitializationOptions: TypeScriptInitializationOptions = this.initializeParams.initializationOptions || {}; + const { hostInfo } = userInitializationOptions; + const { logVerbosity, plugins, preferences }: TypeScriptInitializationOptions = { + logVerbosity: userInitializationOptions.logVerbosity || this.options.tsserverLogVerbosity, + plugins: userInitializationOptions.plugins || [], + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + ...userInitializationOptions.preferences + } }; + const logFile = this.getLogFile(logVerbosity); const globalPlugins: string[] = []; const pluginProbeLocations: string[] = []; @@ -435,11 +441,11 @@ export class LspServer { * implemented based on * https://github.com/Microsoft/vscode/blob/master/extensions/typescript-language-features/src/features/completions.ts */ - async completion(params: lsp.CompletionParams): Promise { + async completion(params: lsp.CompletionParams): Promise { const file = uriToPath(params.textDocument.uri); this.logger.log('completion', params, file); if (!file) { - return []; + return lsp.CompletionList.create([]); } const document = this.documents.get(file); @@ -448,17 +454,16 @@ export class LspServer { } try { - const result = await this.interuptDiagnostics(() => this.tspClient.request(CommandTypes.Completions, { + const result = await this.interuptDiagnostics(() => this.tspClient.request(CommandTypes.CompletionInfo, { file, line: params.position.line + 1, - offset: params.position.character + 1, - includeExternalModuleExports: true, - includeInsertTextCompletions: true + offset: params.position.character + 1 })); - const body = result.body || []; - return body + const { body } = result; + const completions = (body ? body.entries : []) .filter(entry => entry.kind !== 'warning') .map(entry => asCompletionItem(entry, file, params.position, document)); + return lsp.CompletionList.create(completions, body?.isIncomplete); } catch (error) { if (error.message === 'No content available.') { this.logger.info('No content was available for completion request'); @@ -469,7 +474,7 @@ export class LspServer { } } - async completionResolve(item: TSCompletionItem): Promise { + async completionResolve(item: lsp.CompletionItem): Promise { this.logger.log('completion/resolve', item); const { body } = await this.interuptDiagnostics(() => this.tspClient.request(CommandTypes.CompletionDetails, item.data)); const details = body && body.length && body[0]; diff --git a/server/src/test-utils.ts b/server/src/test-utils.ts index a0cec6c5..ad489a43 100644 --- a/server/src/test-utils.ts +++ b/server/src/test-utils.ts @@ -58,6 +58,10 @@ export function position(document: lsp.TextDocumentItem, match: string): lsp.Pos return positionAt(document, document.text.indexOf(match)); } +export function positionAfter(document: lsp.TextDocumentItem, match: string): lsp.Position { + return positionAt(document, document.text.indexOf(match) + match.length); +} + export function lastPosition(document: lsp.TextDocumentItem, match: string): lsp.Position { return positionAt(document, document.text.lastIndexOf(match)); } From 06f95688c682d7e3f6f3cc09f86c5a8dd4758182 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 6 Jul 2021 21:12:58 +0200 Subject: [PATCH 2/2] fix linting --- server/src/completion.ts | 2 +- server/src/lsp-server.spec.ts | 1 - server/src/lsp-server.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/completion.ts b/server/src/completion.ts index fc7626f1..eacfb97b 100644 --- a/server/src/completion.ts +++ b/server/src/completion.ts @@ -12,7 +12,7 @@ import { ScriptElementKind } from './tsp-command-types'; import { asRange, toTextEdit, asPlainText, asDocumentation } from './protocol-translation'; import { Commands } from './commands'; -export interface TSCompletionItem extends lsp.CompletionItem { +interface TSCompletionItem extends lsp.CompletionItem { data: tsp.CompletionDetailsRequestArgs; } diff --git a/server/src/lsp-server.spec.ts b/server/src/lsp-server.spec.ts index 19175145..a6db7cf0 100644 --- a/server/src/lsp-server.spec.ts +++ b/server/src/lsp-server.spec.ts @@ -10,7 +10,6 @@ import * as lsp from 'vscode-languageserver/node'; import * as lspcalls from './lsp-protocol.calls.proposed'; import { LspServer } from './lsp-server'; import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities, positionAfter } from './test-utils'; -import { TSCompletionItem } from './completion'; import { TextDocument } from 'vscode-languageserver-textdocument'; const assert = chai.assert; diff --git a/server/src/lsp-server.ts b/server/src/lsp-server.ts index 742be373..b9f7d91d 100644 --- a/server/src/lsp-server.ts +++ b/server/src/lsp-server.ts @@ -28,7 +28,7 @@ import { } from './protocol-translation'; import { getTsserverExecutable } from './utils'; import { LspDocuments, LspDocument } from './document'; -import { asCompletionItem, TSCompletionItem, asResolvedCompletionItem } from './completion'; +import { asCompletionItem, asResolvedCompletionItem } from './completion'; import { asSignatureHelp } from './hover'; import { Commands } from './commands'; import { provideQuickFix } from './quickfix';