diff --git a/server/src/completion.ts b/server/src/completion.ts index 6b38669f..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; } @@ -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..a6db7cf0 100644 --- a/server/src/lsp-server.spec.ts +++ b/server/src/lsp-server.spec.ts @@ -9,8 +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 { TSCompletionItem } from './completion'; +import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities, positionAfter } from './test-utils'; import { TextDocument } from 'vscode-languageserver-textdocument'; const assert = chai.assert; @@ -51,14 +50,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 +77,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 +115,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 f1270e62..944c05de 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'; @@ -105,12 +105,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[] = []; @@ -436,11 +442,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); @@ -449,17 +455,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'); @@ -470,7 +475,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)); }