UNPKG

typescript-language-server

Version:

Language Server Protocol (LSP) implementation for TypeScript using tsserver

1,387 lines (1,379 loc) 71.3 kB
/* * Copyright (C) 2017, 2018 TypeFox and others. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import * as chai from 'chai'; import fs from 'fs-extra'; import * as lsp from 'vscode-languageserver'; import * as lspcalls from './lsp-protocol.calls.proposed.js'; import { uri, createServer, position, lastPosition, filePath, positionAfter, readContents, toPlatformEOL } from './test-utils.js'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { Commands } from './commands.js'; import { CodeActionKind } from './utils/types.js'; const assert = chai.assert; const diagnostics = new Map(); let server; before(async () => { server = await createServer({ rootUri: uri(), publishDiagnostics: args => diagnostics.set(args.uri, args), }); server.didChangeConfiguration({ settings: { completions: { completeFunctionCalls: true, }, }, }); }); beforeEach(() => { server.closeAll(); // "closeAll" triggers final publishDiagnostics with an empty list so clear last. diagnostics.clear(); server.workspaceEdits = []; }); after(() => { server.closeAll(); server.shutdown(); }); describe('completion', () => { it('simple test', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` export function foo(): void { console.log('test') } `, }; server.didOpenTextDocument({ textDocument: doc, }); const pos = position(doc, 'console'); const proposals = await server.completion({ textDocument: doc, position: pos }); assert.isNotNull(proposals); assert.isAtLeast(proposals.items.length, 800); const item = proposals.items.find(i => i.label === 'addEventListener'); assert.isDefined(item); const resolvedItem = await server.completionResolve(item); assert.isNotTrue(resolvedItem.deprecated, 'resolved item is not deprecated'); assert.isDefined(resolvedItem.detail); server.didCloseTextDocument({ textDocument: doc }); }); it('simple JS test', async () => { const doc = { uri: uri('bar.js'), languageId: 'javascript', version: 1, text: ` export function foo() { console.log('test') } `, }; server.didOpenTextDocument({ textDocument: doc, }); const pos = position(doc, 'console'); const proposals = await server.completion({ textDocument: doc, position: pos }); assert.isNotNull(proposals); assert.isAtLeast(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); const containsInvalidCompletions = proposals.items.reduce((accumulator, current) => { if (accumulator) { return accumulator; } // console.log as a warning is erroneously mapped to a non-function type return current.label === 'log' && (current.kind !== lsp.CompletionItemKind.Function && current.kind !== lsp.CompletionItemKind.Method); }, false); assert.isFalse(containsInvalidCompletions); server.didCloseTextDocument({ textDocument: doc }); }); it('deprecated by JSDoc', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` /** * documentation * @deprecated for a reason */ export function foo() { console.log('test') } foo(); // call me `, }; server.didOpenTextDocument({ textDocument: doc, }); const pos = position(doc, 'foo(); // call me'); const proposals = await server.completion({ textDocument: doc, position: pos }); assert.isNotNull(proposals); const item = proposals.items.find(i => i.label === 'foo'); assert.isDefined(item); const resolvedItem = await server.completionResolve(item); assert.isDefined(resolvedItem.detail); assert.isArray(resolvedItem.tags); assert.include(resolvedItem.tags, lsp.CompletionItemTag.Deprecated, 'resolved item is deprecated'); server.didCloseTextDocument({ textDocument: doc }); }); it('incorrect source location', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` export function foo(): void { console.log('test') } `, }; server.didOpenTextDocument({ textDocument: doc, }); const pos = position(doc, 'foo'); const proposals = await server.completion({ textDocument: doc, position: pos }); assert.isNotNull(proposals); assert.strictEqual(proposals?.items.length, 0); server.didCloseTextDocument({ textDocument: doc }); }); 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 }); }); 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 }); }); it('completions for clients that support insertReplaceSupport', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` class Foo { getById() {}; } const foo = new Foo() foo.getById() `, }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, '.get') }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'getById'); assert.isDefined(completion); assert.isDefined(completion.textEdit); assert.containsAllKeys(completion.textEdit, ['newText', 'insert', 'replace']); server.didCloseTextDocument({ textDocument: doc }); }); it('completions for clients that do not support insertReplaceSupport', async () => { const clientCapabilitiesOverride = { textDocument: { completion: { completionItem: { insertReplaceSupport: false, }, }, }, }; const localServer = await createServer({ rootUri: null, publishDiagnostics: () => { }, clientCapabilitiesOverride, }); const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` class Foo { getById() {}; } const foo = new Foo() foo.getById() `, }; localServer.didOpenTextDocument({ textDocument: doc }); const proposals = await localServer.completion({ textDocument: doc, position: positionAfter(doc, '.get') }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'getById'); assert.isDefined(completion); assert.isUndefined(completion.textEdit); localServer.didCloseTextDocument({ textDocument: doc }); localServer.closeAll(); localServer.shutdown(); }); it('includes detail field with package name for auto-imports', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: 'readFile', }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, 'readFile') }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'readFile'); assert.isDefined(completion); assert.strictEqual(completion.detail, 'fs'); assert.strictEqual(completion.insertTextFormat, /* snippet */ 2); server.didCloseTextDocument({ textDocument: doc }); }); it('resolves text edit for auto-import completion', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: 'readFile', }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, 'readFile') }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'readFile'); assert.isDefined(completion); const resolvedItem = await server.completionResolve(completion); assert.deepEqual(resolvedItem.additionalTextEdits, [ { newText: 'import { readFile } from "fs";\n\n', range: { end: { character: 0, line: 0, }, start: { character: 0, line: 0, }, }, }, ]); server.didCloseTextDocument({ textDocument: doc }); }); it('resolves text edit for auto-import completion in right format', async () => { server.didChangeConfiguration({ settings: { typescript: { format: { semicolons: 'remove', insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false, }, }, }, }); const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: 'readFile', }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, 'readFile') }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'readFile'); assert.isDefined(completion); const resolvedItem = await server.completionResolve(completion); assert.deepEqual(resolvedItem.additionalTextEdits, [ { newText: 'import {readFile} from "fs"\n\n', range: { end: { character: 0, line: 0, }, start: { character: 0, line: 0, }, }, }, ]); server.didCloseTextDocument({ textDocument: doc }); server.didChangeConfiguration({ settings: { completions: { completeFunctionCalls: true, }, typescript: { format: { semicolons: 'ignore', insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, }, }, }, }); }); it('resolves a snippet for method completion', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` import fs from 'fs' fs.readFile `, }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, 'readFile') }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'readFile'); assert.strictEqual(completion.insertTextFormat, lsp.InsertTextFormat.Snippet); assert.strictEqual(completion.label, 'readFile'); const resolvedItem = await server.completionResolve(completion); assert.strictEqual(resolvedItem.insertTextFormat, lsp.InsertTextFormat.Snippet); // eslint-disable-next-line no-template-curly-in-string assert.strictEqual(resolvedItem.insertText, 'readFile(${1:path}, ${2:options}, ${3:callback})$0'); server.didCloseTextDocument({ textDocument: doc }); }); it('includes textEdit for string completion', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` function test(value: "fs/read" | "hello/world") { return true; } test("fs/r") `, }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, 'test("fs/'), context: { triggerCharacter: '/', triggerKind: 2, }, }); assert.isNotNull(proposals); const completion = proposals.items.find(completion => completion.label === 'fs/read'); assert.strictEqual(completion.label, 'fs/read'); assert.deepStrictEqual(completion.textEdit, { newText: 'fs/read', insert: { start: { line: 5, character: 20, }, end: { line: 5, character: 23, }, }, replace: { start: { line: 5, character: 20, }, end: { line: 5, character: 24, }, }, }); }); it('includes labelDetails with useLabelDetailsInCompletionEntries enabled', async () => { const doc = { uri: uri('foo.ts'), languageId: 'typescript', version: 1, text: ` interface IFoo { bar(x: number): void; } const obj: IFoo = { /*a*/ } `, }; server.didOpenTextDocument({ textDocument: doc }); const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, '/*a*/'), }); assert.isNotNull(proposals); assert.lengthOf(proposals.items, 2); assert.deepInclude(proposals.items[0], { label: 'bar', kind: 2, insertTextFormat: 2, }); assert.deepInclude(proposals.items[1], { label: 'bar', labelDetails: { detail: '(x)', }, kind: 2, insertTextFormat: 2, insertText: toPlatformEOL('bar(x) {\n $0\n},'), }); }); }); describe('definition', () => { it('goes to definition', async () => { // NOTE: This test needs to reference files that physically exist for the feature to work. const indexUri = uri('source-definition', 'index.ts'); const indexDoc = { uri: indexUri, languageId: 'typescript', version: 1, text: readContents(filePath('source-definition', 'index.ts')), }; server.didOpenTextDocument({ textDocument: indexDoc }); const definitions = await server.definition({ textDocument: indexDoc, position: position(indexDoc, 'a/*identifier*/'), }); assert.isArray(definitions); assert.equal(definitions.length, 1); assert.deepEqual(definitions[0], { uri: uri('source-definition', 'a.d.ts'), range: { start: { line: 0, character: 21, }, end: { line: 0, character: 22, }, }, }); }); }); describe('definition (definition link supported)', () => { let localServer; before(async () => { const clientCapabilitiesOverride = { textDocument: { definition: { linkSupport: true, }, }, }; localServer = await createServer({ rootUri: uri('source-definition'), publishDiagnostics: args => diagnostics.set(args.uri, args), clientCapabilitiesOverride, }); }); beforeEach(() => { localServer.closeAll(); // "closeAll" triggers final publishDiagnostics with an empty list so clear last. diagnostics.clear(); localServer.workspaceEdits = []; }); after(() => { localServer.closeAll(); localServer.shutdown(); }); it('goes to definition', async () => { // NOTE: This test needs to reference files that physically exist for the feature to work. const indexUri = uri('source-definition', 'index.ts'); const indexDoc = { uri: indexUri, languageId: 'typescript', version: 1, text: readContents(filePath('source-definition', 'index.ts')), }; localServer.didOpenTextDocument({ textDocument: indexDoc }); const definitions = await localServer.definition({ textDocument: indexDoc, position: position(indexDoc, 'a/*identifier*/'), }); assert.isArray(definitions); assert.equal(definitions.length, 1); assert.deepEqual(definitions[0], { originSelectionRange: { start: { line: 1, character: 0, }, end: { line: 1, character: 1, }, }, targetRange: { start: { line: 0, character: 0, }, end: { line: 0, character: 30, }, }, targetUri: uri('source-definition', 'a.d.ts'), targetSelectionRange: { start: { line: 0, character: 21, }, end: { line: 0, character: 22, }, }, }); }); }); describe('diagnostics', () => { it('simple test', async () => { const doc = { uri: uri('diagnosticsBar.ts'), languageId: 'typescript', version: 1, text: ` export function foo(): void { missing('test') } `, }; server.didOpenTextDocument({ textDocument: doc, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const resultsForFile = diagnostics.get(doc.uri); assert.isDefined(resultsForFile); const fileDiagnostics = resultsForFile.diagnostics; assert.equal(fileDiagnostics.length, 1); assert.equal("Cannot find name 'missing'.", fileDiagnostics[0].message); }); it('supports diagnostic tags', async () => { const doc = { uri: uri('diagnosticsBar.ts'), languageId: 'typescript', version: 1, text: ` import { join } from 'path'; /** @deprecated */ function foo(): void {} foo(); `, }; server.didOpenTextDocument({ textDocument: doc, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const resultsForFile = diagnostics.get(doc.uri); assert.isDefined(resultsForFile); const fileDiagnostics = resultsForFile.diagnostics; assert.equal(fileDiagnostics.length, 2); const unusedDiagnostic = fileDiagnostics.find(d => d.code === 6133); assert.isDefined(unusedDiagnostic); assert.deepEqual(unusedDiagnostic.tags, [lsp.DiagnosticTag.Unnecessary]); const deprecatedDiagnostic = fileDiagnostics.find(d => d.code === 6387); assert.isDefined(deprecatedDiagnostic); assert.deepEqual(deprecatedDiagnostic.tags, [lsp.DiagnosticTag.Deprecated]); }); it('multiple files test', async () => { const doc = { uri: uri('multipleFileDiagnosticsBar.ts'), languageId: 'typescript', version: 1, text: ` export function bar(): void { missing('test') } `, }; const doc2 = { uri: uri('multipleFileDiagnosticsFoo.ts'), languageId: 'typescript', version: 1, text: ` export function foo(): void { missing('test') } `, }; server.didOpenTextDocument({ textDocument: doc, }); server.didOpenTextDocument({ textDocument: doc2, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); assert.equal(diagnostics.size, 2); const diagnosticsForDoc = diagnostics.get(doc.uri); const diagnosticsForDoc2 = diagnostics.get(doc2.uri); assert.isDefined(diagnosticsForDoc); assert.isDefined(diagnosticsForDoc2); assert.equal(diagnosticsForDoc.diagnostics.length, 1, JSON.stringify(diagnostics)); assert.equal(diagnosticsForDoc2.diagnostics.length, 1, JSON.stringify(diagnostics)); }); it('code 6133 (ununsed variable) is ignored', async () => { server.didChangeConfiguration({ settings: { diagnostics: { ignoredCodes: [6133], }, }, }); const doc = { uri: uri('diagnosticsBar2.ts'), languageId: 'typescript', version: 1, text: ` export function foo() { const x = 42; return 1; } `, }; server.didOpenTextDocument({ textDocument: doc, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const diagnosticsForThisFile = diagnostics.get(doc.uri); assert.isDefined(diagnosticsForThisFile); const fileDiagnostics = diagnosticsForThisFile.diagnostics; assert.equal(fileDiagnostics.length, 0, JSON.stringify(fileDiagnostics)); }); }); describe('document symbol', () => { it('simple test', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` export class Foo { protected foo: string; public myFunction(arg: string) { } } `, }; server.didOpenTextDocument({ textDocument: doc, }); const symbols = await server.documentSymbol({ textDocument: doc }); assert.equal(` Foo foo myFunction `, symbolsAsString(symbols) + '\n'); }); it('merges interfaces correctly', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` interface Box { height: number; width: number; } interface Box { scale: number; }`, }; server.didOpenTextDocument({ textDocument: doc, }); const symbols = await server.documentSymbol({ textDocument: doc }); assert.equal(` Box height width Box scale `, symbolsAsString(symbols) + '\n'); }); it('duplication test', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` export class Foo { protected foo: string; public myFunction(arg: string) { } } export class Foo { protected foo: string; public myFunction(arg: string) { } } `, }; server.didOpenTextDocument({ textDocument: doc, }); const symbols = await server.documentSymbol({ textDocument: doc }); const expectation = ` Foo foo myFunction Foo foo myFunction `; assert.equal(symbolsAsString(symbols) + '\n', expectation); assert.deepEqual(symbols[0].selectionRange, { start: { line: 1, character: 21 }, end: { line: 1, character: 24 } }); assert.deepEqual(symbols[0].range, { start: { line: 1, character: 8 }, end: { line: 5, character: 9 } }); assert.deepEqual(symbols[1].selectionRange, symbols[1].range); assert.deepEqual(symbols[1].range, { start: { line: 6, character: 8 }, end: { line: 10, character: 9 } }); }); }); function symbolsAsString(symbols, indentation = '') { return symbols.map(symbol => { let result = '\n' + indentation + symbol.name; if (lsp.DocumentSymbol.is(symbol)) { if (symbol.children) { result = result + symbolsAsString(symbol.children, indentation + ' '); } } else { if (symbol.containerName) { result = result + ` in ${symbol.containerName}`; } } return result; }).join(''); } describe('editing', () => { it('open and change', async () => { const doc = { uri: uri('openAndChangeBar.ts'), languageId: 'typescript', version: 1, text: ` export function foo(): void { } `, }; server.didOpenTextDocument({ textDocument: doc, }); server.didChangeTextDocument({ textDocument: doc, contentChanges: [ { text: ` export function foo(): void { missing('test'); } `, }, ], }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const resultsForFile = diagnostics.get(doc.uri); assert.isDefined(resultsForFile); const fileDiagnostics = resultsForFile.diagnostics; assert.isTrue(fileDiagnostics.length >= 1, fileDiagnostics.map(d => d.message).join(',')); assert.equal("Cannot find name 'missing'.", fileDiagnostics[0].message); }); }); describe('references', () => { it('respects "includeDeclaration" in the request', async () => { const doc = { uri: uri('foo.ts'), languageId: 'typescript', version: 1, text: ` function foo() {}; foo(); `, }; server.didOpenTextDocument({ textDocument: doc, }); // Without declaration/definition. const position = lastPosition(doc, 'function foo()'); let references = await server.references({ context: { includeDeclaration: false }, textDocument: doc, position, }); assert.strictEqual(references.length, 1); assert.strictEqual(references[0].range.start.line, 2); // With declaration/definition. references = await server.references({ context: { includeDeclaration: true }, textDocument: doc, position, }); assert.strictEqual(references.length, 2); }); }); // describe('workspace configuration', () => { // it('receives workspace configuration notification', async ()=>{ // const doc = { // uri: uri('bar.ts'), // languageId: 'typescript', // version: 1, // text: ` // export function foo(): void { // console.log('test') // } // ` // }; // server.didOpenTextDocument({ // textDocument: doc // }); // server.didChangeConfiguration({ // settings: { // typescript: { // format: { // insertSpaceAfterCommaDelimiter: true // } // }, // javascript: { // format: { // insertSpaceAfterCommaDelimiter: false // } // } // } // }); // const file = filePath('bar.ts'); // const settings = server.getWorkspacePreferencesForDocument(file); // assert.deepEqual(settings, { format: { insertSpaceAfterCommaDelimiter: true } }); // }); // }); describe('formatting', () => { const uriString = uri('bar.ts'); const languageId = 'typescript'; const version = 1; it('full document formatting', async () => { const text = 'export function foo ( ) : void { }'; const textDocument = { uri: uriString, languageId, version, text, }; server.didOpenTextDocument({ textDocument }); const edits = await server.documentFormatting({ textDocument, options: { tabSize: 4, insertSpaces: true, }, }); const result = TextDocument.applyEdits(TextDocument.create(uriString, languageId, version, text), edits); assert.equal('export function foo(): void { }', result); }); it('indent settings (3 spaces)', async () => { const text = 'function foo() {\n// some code\n}'; const textDocument = { uri: uriString, languageId, version, text, }; server.didOpenTextDocument({ textDocument }); const edits = await server.documentFormatting({ textDocument, options: { tabSize: 3, insertSpaces: true, }, }); const result = TextDocument.applyEdits(TextDocument.create(uriString, languageId, version, text), edits); assert.equal('function foo() {\n // some code\n}', result); }); it('indent settings (tabs)', async () => { const text = 'function foo() {\n// some code\n}'; const textDocument = { uri: uriString, languageId, version, text, }; server.didOpenTextDocument({ textDocument }); const edits = await server.documentFormatting({ textDocument, options: { tabSize: 4, insertSpaces: false, }, }); const result = TextDocument.applyEdits(TextDocument.create(uriString, languageId, version, text), edits); assert.equal('function foo() {\n\t// some code\n}', result); }); it('formatting setting set through workspace configuration', async () => { const text = 'function foo() {\n// some code\n}'; const textDocument = { uri: uriString, languageId, version, text, }; server.didOpenTextDocument({ textDocument }); server.didChangeConfiguration({ settings: { typescript: { format: { newLineCharacter: '\n', placeOpenBraceOnNewLineForFunctions: true, }, }, }, }); const edits = await server.documentFormatting({ textDocument, options: { tabSize: 4, insertSpaces: false, }, }); const result = TextDocument.applyEdits(TextDocument.create(uriString, languageId, version, text), edits); assert.equal('function foo()\n{\n\t// some code\n}', result); }); it('selected range', async () => { const text = 'function foo() {\nconst first = 1;\nconst second = 2;\nconst val = foo( "something" );\n//const fourth = 4;\n}'; const textDocument = { uri: uriString, languageId, version, text, }; server.didOpenTextDocument({ textDocument }); const edits = await server.documentRangeFormatting({ textDocument, range: { start: { line: 2, character: 0, }, end: { line: 3, character: 30, }, }, options: { tabSize: 4, insertSpaces: true, }, }); const result = TextDocument.applyEdits(TextDocument.create(uriString, languageId, version, text), edits); assert.equal('function foo() {\nconst first = 1;\n const second = 2;\n const val = foo("something");\n//const fourth = 4;\n}', result); }); }); describe('signatureHelp', () => { it('simple test', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` export function foo(bar: string, baz?:boolean): void {} export function foo(n: number, baz?: boolean): void foo(param1, param2) `, }; server.didOpenTextDocument({ textDocument: doc, }); let result = (await server.signatureHelp({ textDocument: doc, position: position(doc, 'param1'), })); assert.equal(result.signatures.length, 2); assert.equal('bar: string', result.signatures[result.activeSignature].parameters[result.activeParameter].label); result = (await server.signatureHelp({ textDocument: doc, position: position(doc, 'param2'), })); assert.equal('baz?: boolean', result.signatures[result.activeSignature].parameters[result.activeParameter].label); }); it('retrigger with specific signature active', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: ` export function foo(bar: string, baz?: boolean): void {} export function foo(n: number, baz?: boolean): void foo(param1, param2) `, }; server.didOpenTextDocument({ textDocument: doc }); let result = await server.signatureHelp({ textDocument: doc, position: position(doc, 'param1'), }); assert.equal(result.signatures.length, 2); result = (await server.signatureHelp({ textDocument: doc, position: position(doc, 'param1'), context: { isRetrigger: true, triggerKind: lsp.SignatureHelpTriggerKind.Invoked, activeSignatureHelp: { signatures: result.signatures, activeSignature: 1, // select second signature }, }, })); const { activeSignature, signatures } = result; assert.equal(activeSignature, 1); assert.deepInclude(signatures[activeSignature], { label: 'foo(n: number, baz?: boolean): void', }); }); }); describe('code actions', () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: `import { something } from "something"; export function foo(bar: string, baz?:boolean): void {} foo(param1, param2) `, }; it('can provide quickfix code actions', async () => { server.didOpenTextDocument({ textDocument: doc, }); const result = (await server.codeAction({ textDocument: doc, range: { start: { line: 1, character: 25 }, end: { line: 1, character: 49 }, }, context: { diagnostics: [{ range: { start: { line: 1, character: 25 }, end: { line: 1, character: 49 }, }, code: 6133, message: 'unused arg', }], }, })); assert.strictEqual(result.length, 2); const quickFixDiagnostic = result.find(diagnostic => diagnostic.kind === 'quickfix'); assert.isDefined(quickFixDiagnostic); assert.deepEqual(quickFixDiagnostic, { title: "Prefix 'bar' with an underscore", command: { title: "Prefix 'bar' with an underscore", command: '_typescript.applyWorkspaceEdit', arguments: [ { documentChanges: [ { textDocument: { uri: uri('bar.ts'), version: 1, }, edits: [ { range: { start: { line: 1, character: 24, }, end: { line: 1, character: 27, }, }, newText: '_bar', }, ], }, ], }, ], }, kind: 'quickfix', }); const refactorDiagnostic = result.find(diagnostic => diagnostic.kind === 'refactor'); assert.isDefined(refactorDiagnostic); assert.deepEqual(refactorDiagnostic, { title: 'Convert parameters to destructured object', command: { title: 'Convert parameters to destructured object', command: '_typescript.applyRefactoring', arguments: [ { file: filePath('bar.ts'), startLine: 2, startOffset: 26, endLine: 2, endOffset: 50, refactor: 'Convert parameters to destructured object', action: 'Convert parameters to destructured object', }, ], }, kind: 'refactor', }); }); it('can filter quickfix code actions filtered by only', async () => { server.didOpenTextDocument({ textDocument: doc, }); const result = (await server.codeAction({ textDocument: doc, range: { start: { line: 1, character: 25 }, end: { line: 1, character: 49 }, }, context: { diagnostics: [{ range: { start: { line: 1, character: 25 }, end: { line: 1, character: 49 }, }, code: 6133, message: 'unused arg', }], only: ['refactor', 'invalid-action'], }, })); assert.deepEqual(result, [ { command: { arguments: [ { action: 'Convert parameters to destructured object', endLine: 2, endOffset: 50, file: filePath('bar.ts'), refactor: 'Convert parameters to destructured object', startLine: 2, startOffset: 26, }, ], command: '_typescript.applyRefactoring', title: 'Convert parameters to destructured object', }, kind: 'refactor', title: 'Convert parameters to destructured object', }, ]); }); it('does not provide organize imports when there are errors', async () => { server.didOpenTextDocument({ textDocument: doc, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const result = (await server.codeAction({ textDocument: doc, range: { start: { line: 1, character: 29 }, end: { line: 1, character: 53 }, }, context: { diagnostics: [{ range: { start: { line: 1, character: 25 }, end: { line: 1, character: 49 }, }, code: 6133, message: 'unused arg', }], only: [CodeActionKind.SourceOrganizeImportsTs.value], }, })); assert.deepEqual(result, []); }); it('provides "add missing imports" when explicitly requested in only', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: 'existsSync(\'t\');', }; server.didOpenTextDocument({ textDocument: doc, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const result = (await server.codeAction({ textDocument: doc, range: { start: { line: 1, character: 29 }, end: { line: 1, character: 53 }, }, context: { diagnostics: [], only: [CodeActionKind.SourceAddMissingImportsTs.value], }, })); assert.deepEqual(result, [ { kind: CodeActionKind.SourceAddMissingImportsTs.value, title: 'Add all missing imports', edit: { documentChanges: [ { edits: [ { // Prefers import that is declared in package.json. newText: 'import { existsSync } from "fs-extra";\n\n', range: { end: { character: 0, line: 0, }, start: { character: 0, line: 0, }, }, }, ], textDocument: { uri: uri('bar.ts'), version: 1, }, }, ], }, }, ]); }); it('provides "fix all" when explicitly requested in only', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: `function foo() { return setTimeout(() => {}) }`, }; server.didOpenTextDocument({ textDocument: doc, }); await server.requestDiagnostics(); await new Promise(resolve => setTimeout(resolve, 200)); const result = (await server.codeAction({ textDocument: doc, range: { start: { line: 0, character: 0 }, end: { line: 4, character: 0 }, }, context: { diagnostics: [], only: [CodeActionKind.SourceFixAllTs.value], }, })); assert.deepEqual(result, [ { kind: CodeActionKind.SourceFixAllTs.value, title: 'Fix all', edit: { documentChanges: [ { edits: [ { newText: '', range: { end: { character: 0, line: 3, }, start: { character: 0, line: 2, }, }, }, ], textDocument: { uri: uri('bar.ts'), version: 1, }, }, ], }, }, ]); }); it('provides organize imports when explicitly requested in only', async () => { const doc = { uri: uri('bar.ts'), languageId: 'typescript', version: 1, text: `import { existsSync } from 'fs'; import { accessSync } from 'fs'; existsSync('t');`, }; server.didOpenTextDocument({ textDocument: doc, }); const result = (await server.codeAction({ textDocument: doc, range: { start: { line: 0, character: 0 }, end: { line: 3, character: 0 }, }, context: { diagnostics: [{ range: { start: { line: 1, character: 25 }, end: { line: 1, character: 49 }, }, code: 6133, message: 'unused arg', }], only: [CodeActionKind.SourceOrganizeImportsTs.value], }, })); assert.deepEqual(result, [ { kind: CodeActionKind.SourceOrganizeImportsTs.value, title: 'Organize imports', edit: { documentChanges: [ { edits: [ { newText: "import { accessSync, existsSync } from 'fs';\n", range: { end: { character: 0, line: 1, }, start: { character: 0, line: 0, }, }, }, { newText: '', range: { end: {