UNPKG

javascript-typescript-langserver

Version:

Implementation of the Language Server Protocol for JavaScript and TypeScript

1,127 lines (1,126 loc) 145 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const chai = require("chai"); const chaiAsPromised = require("chai-as-promised"); const fast_json_patch_1 = require("fast-json-patch"); const rxjs_1 = require("rxjs"); const sinon = require("sinon"); const ts = require("typescript"); const vscode_languageserver_1 = require("vscode-languageserver"); const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const lang_handler_1 = require("../lang-handler"); const util_1 = require("../util"); chai.use(chaiAsPromised); const assert = chai.assert; const DEFAULT_CAPABILITIES = { xcontentProvider: true, xfilesProvider: true, }; /** * Returns a function that initializes the test context with a TypeScriptService instance and initializes it (to be used in `beforeEach`) * * @param createService A factory that creates the TypeScript service. Allows to test subclasses of TypeScriptService * @param files A Map from URI to file content of files that should be available in the workspace */ exports.initializeTypeScriptService = (createService, rootUri, files, clientCapabilities = DEFAULT_CAPABILITIES) => function () { return __awaiter(this, void 0, void 0, function* () { // Stub client this.client = sinon.createStubInstance(lang_handler_1.RemoteLanguageClient); this.client.textDocumentXcontent.callsFake((params) => { if (!files.has(params.textDocument.uri)) { return rxjs_1.Observable.throw(new Error(`Text document ${params.textDocument.uri} does not exist`)); } return rxjs_1.Observable.of({ uri: params.textDocument.uri, text: files.get(params.textDocument.uri), version: 1, languageId: '', }); }); this.client.workspaceXfiles.callsFake((params) => util_1.observableFromIterable(files.keys()) .map(uri => ({ uri })) .toArray()); this.client.xcacheGet.callsFake(() => rxjs_1.Observable.of(null)); this.client.workspaceApplyEdit.callsFake(() => rxjs_1.Observable.of({ applied: true })); this.service = createService(this.client); yield this.service .initialize({ processId: process.pid, rootUri, capabilities: clientCapabilities || DEFAULT_CAPABILITIES, workspaceFolders: [ { uri: rootUri, name: 'test', }, ], }) .toPromise(); }); }; /** * Shuts the TypeScriptService down (to be used in `afterEach()`) */ function shutdownTypeScriptService() { return __awaiter(this, void 0, void 0, function* () { yield this.service.shutdown().toPromise(); }); } exports.shutdownTypeScriptService = shutdownTypeScriptService; /** * Describe a TypeScriptService class * * @param createService Factory function to create the TypeScriptService instance to describe */ function describeTypeScriptService(createService, shutdownService = shutdownTypeScriptService, rootUri) { describe('Workspace without project files', () => { beforeEach(exports.initializeTypeScriptService(createService, rootUri, new Map([ [rootUri + 'a.ts', 'const abc = 1; console.log(abc);'], [rootUri + 'foo/b.ts', ['/* This is class Foo */', 'export class Foo {}'].join('\n')], [rootUri + 'foo/c.ts', 'import {Foo} from "./b";'], [rootUri + 'd.ts', ['export interface I {', ' target: string;', '}'].join('\n')], [ rootUri + 'local_callback.ts', 'function local(): void { function act(handle: () => void): void { handle() } }', ], [ rootUri + 'e.ts', [ 'import * as d from "./d";', '', 'let i: d.I = { target: "hi" };', 'let target = i.target;', ].join('\n'), ], [rootUri + 'foo/f.ts', ['import {Foo} from "./b";', '', 'let foo: Foo = Object({});'].join('\n')], [rootUri + 'foo/g.ts', ['class Foo = {}', '', 'let foo: Foo = Object({});'].join('\n')], ]))); afterEach(shutdownService); describe('textDocumentDefinition()', () => { specify('in same file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentDefinition({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, character: 29, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'a.ts', range: { start: { line: 0, character: 6, }, end: { line: 0, character: 9, }, }, }, ]); }); }); specify('on keyword (non-null)', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentDefinition({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, character: 0, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, []); }); }); specify('in other file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentDefinition({ textDocument: { uri: rootUri + 'foo/c.ts', }, position: { line: 0, character: 9, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'foo/b.ts', range: { start: { line: 1, character: 13, }, end: { line: 1, character: 16, }, }, }, ]); }); }); }); describe('textDocumentTypeDefinition()', () => { specify('in other file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentTypeDefinition({ textDocument: { uri: rootUri + 'foo/f.ts', }, position: { line: 2, character: 5, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'foo/b.ts', range: { start: { line: 1, character: 13, }, end: { line: 1, character: 16, }, }, }, ]); }); }); specify('in same file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentTypeDefinition({ textDocument: { uri: rootUri + 'foo/g.ts', }, position: { line: 2, character: 5, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'foo/g.ts', range: { start: { line: 0, character: 6, }, end: { line: 0, character: 9, }, }, }, ]); }); }); }); describe('textDocumentXdefinition()', () => { specify('on interface field reference', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentXdefinition({ textDocument: { uri: rootUri + 'e.ts', }, position: { line: 3, character: 15, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { location: { uri: rootUri + 'd.ts', range: { start: { line: 1, character: 2, }, end: { line: 1, character: 8, }, }, }, symbol: { filePath: 'd.ts', containerName: 'd.I', containerKind: '', kind: 'property', name: 'target', }, }, ]); }); }); specify('in same file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentXdefinition({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, character: 29, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { location: { uri: rootUri + 'a.ts', range: { start: { line: 0, character: 6, }, end: { line: 0, character: 9, }, }, }, symbol: { filePath: 'a.ts', containerName: '"a"', containerKind: 'module', kind: 'const', name: 'abc', }, }, ]); }); }); }); describe('textDocumentHover()', () => { specify('in same file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentHover({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, character: 29, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, { range: { start: { line: 0, character: 27, }, end: { line: 0, character: 30, }, }, contents: [{ language: 'typescript', value: 'const abc: 1' }, '**const**'], }); }); }); specify('local function with callback argument', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentHover({ textDocument: { uri: rootUri + 'local_callback.ts', }, position: { line: 0, character: 36, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, { range: { start: { line: 0, character: 34, }, end: { line: 0, character: 37, }, }, contents: [ { language: 'typescript', value: 'act(handle: () => void): void' }, '**local function**', ], }); }); }); specify('in other file', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentHover({ textDocument: { uri: rootUri + 'foo/c.ts', }, position: { line: 0, character: 9, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, { range: { end: { line: 0, character: 11, }, start: { line: 0, character: 8, }, }, contents: [{ language: 'typescript', value: 'class Foo\nimport Foo' }, '**alias**'], }); }); }); specify('over keyword (non-null)', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentHover({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, character: 0, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, { contents: [] }); }); }); specify('over non-existent file', function () { return __awaiter(this, void 0, void 0, function* () { yield Promise.resolve(assert.isRejected(this.service .textDocumentHover({ textDocument: { uri: rootUri + 'foo/a.ts', }, position: { line: 0, character: 0, }, }) .toPromise())); }); }); }); }); describe('Workspace with typings directory', () => { beforeEach(exports.initializeTypeScriptService(createService, rootUri, new Map([ [rootUri + 'src/a.ts', "import * as m from 'dep';"], [rootUri + 'typings/dep.d.ts', "declare module 'dep' {}"], [ rootUri + 'src/tsconfig.json', [ '{', ' "compilerOptions": {', ' "target": "ES5",', ' "module": "commonjs",', ' "sourceMap": true,', ' "noImplicitAny": false,', ' "removeComments": false,', ' "preserveConstEnums": true', ' }', '}', ].join('\n'), ], [rootUri + 'src/tsd.d.ts', '/// <reference path="../typings/dep.d.ts" />'], [rootUri + 'src/dir/index.ts', 'import * as m from "dep";'], ]))); afterEach(shutdownService); describe('textDocumentDefinition()', () => { specify('with tsd.d.ts', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentDefinition({ textDocument: { uri: rootUri + 'src/dir/index.ts', }, position: { line: 0, character: 20, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'typings/dep.d.ts', range: { start: { line: 0, character: 15, }, end: { line: 0, character: 20, }, }, }, ]); }); }); describe('on file in project root', () => { specify('on import alias', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentDefinition({ textDocument: { uri: rootUri + 'src/a.ts', }, position: { line: 0, character: 12, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'typings/dep.d.ts', range: { start: { line: 0, character: 15, }, end: { line: 0, character: 20, }, }, }, ]); }); }); specify('on module name', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .textDocumentDefinition({ textDocument: { uri: rootUri + 'src/a.ts', }, position: { line: 0, character: 20, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { uri: rootUri + 'typings/dep.d.ts', range: { start: { line: 0, character: 15, }, end: { line: 0, character: 20, }, }, }, ]); }); }); }); }); }); describe('DefinitelyTyped', () => { beforeEach(exports.initializeTypeScriptService(createService, rootUri, new Map([ [ rootUri + 'package.json', JSON.stringify({ private: true, name: 'definitely-typed', version: '0.0.1', homepage: 'https://github.com/DefinitelyTyped/DefinitelyTyped', repository: { type: 'git', url: 'git+https://github.com/DefinitelyTyped/DefinitelyTyped.git', }, license: 'MIT', bugs: { url: 'https://github.com/DefinitelyTyped/DefinitelyTyped/issues', }, engines: { node: '>= 6.9.1', }, scripts: { 'compile-scripts': 'tsc -p scripts', 'new-package': 'node scripts/new-package.js', 'not-needed': 'node scripts/not-needed.js', lint: 'node scripts/lint.js', test: 'node node_modules/types-publisher/bin/tester/test.js --run-from-definitely-typed --nProcesses 1', }, devDependencies: { 'types-publisher': 'Microsoft/types-publisher#production', }, }, null, 4), ], [ rootUri + 'types/resolve/index.d.ts', [ '/// <reference types="node" />', '', 'type resolveCallback = (err: Error, resolved?: string) => void;', 'declare function resolve(id: string, cb: resolveCallback): void;', '', ].join('\n'), ], [ rootUri + 'types/resolve/tsconfig.json', JSON.stringify({ compilerOptions: { module: 'commonjs', lib: ['es6'], noImplicitAny: true, noImplicitThis: true, strictNullChecks: false, baseUrl: '../', typeRoots: ['../'], types: [], noEmit: true, forceConsistentCasingInFileNames: true, }, files: ['index.d.ts'], }), ], [ rootUri + 'types/notResolve/index.d.ts', [ '/// <reference types="node" />', '', 'type resolveCallback = (err: Error, resolved?: string) => void;', 'declare function resolve(id: string, cb: resolveCallback): void;', '', ].join('\n'), ], [ rootUri + 'types/notResolve/tsconfig.json', JSON.stringify({ compilerOptions: { module: 'commonjs', lib: ['es6'], noImplicitAny: true, noImplicitThis: true, strictNullChecks: false, baseUrl: '../', typeRoots: ['../'], types: [], noEmit: true, forceConsistentCasingInFileNames: true, }, files: ['index.d.ts'], }), ], ]))); afterEach(shutdownService); describe('workspaceSymbol()', () => { it('should find a symbol by SymbolDescriptor query with name and package name', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ symbol: { name: 'resolveCallback', package: { name: '@types/resolve' } }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { kind: vscode_languageserver_types_1.SymbolKind.Variable, location: { range: { end: { character: 63, line: 2, }, start: { character: 0, line: 2, }, }, uri: rootUri + 'types/resolve/index.d.ts', }, name: 'resolveCallback', }, ]); }); }); it('should find a symbol by SymbolDescriptor query with name, containerKind and package name', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ symbol: { name: 'resolveCallback', containerKind: 'module', package: { name: '@types/resolve', }, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result[0], { kind: vscode_languageserver_types_1.SymbolKind.Variable, location: { range: { end: { character: 63, line: 2, }, start: { character: 0, line: 2, }, }, uri: rootUri + 'types/resolve/index.d.ts', }, name: 'resolveCallback', }); }); }); }); }); describe('Workspace with root package.json', () => { beforeEach(exports.initializeTypeScriptService(createService, rootUri, new Map([ [rootUri + 'a.ts', 'class a { foo() { const i = 1;} }'], [ rootUri + 'foo/b.ts', 'class b { bar: number; baz(): number { return this.bar;}}; function qux() {}', ], [rootUri + 'c.ts', 'import { x } from "dep/dep";'], [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], [rootUri + 'node_modules/dep/dep.ts', 'export var x = 1;'], [rootUri + 'node_modules/dep/package.json', JSON.stringify({ name: 'dep' })], ]))); afterEach(shutdownService); describe('workspaceSymbol()', () => { describe('with SymbolDescriptor query', () => { it('should find a symbol by name, kind and package name', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ symbol: { name: 'a', kind: 'class', package: { name: 'mypkg', }, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result[0], { kind: vscode_languageserver_types_1.SymbolKind.Class, location: { range: { end: { character: 33, line: 0, }, start: { character: 0, line: 0, }, }, uri: rootUri + 'a.ts', }, name: 'a', }); }); }); it('should find a symbol by name, kind, package name and ignore package version', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ symbol: { name: 'a', kind: 'class', package: { name: 'mypkg', version: '203940234' } }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result[0], { kind: vscode_languageserver_types_1.SymbolKind.Class, location: { range: { end: { character: 33, line: 0, }, start: { character: 0, line: 0, }, }, uri: rootUri + 'a.ts', }, name: 'a', }); }); }); it('should find a symbol by name', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ symbol: { name: 'a', }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { kind: vscode_languageserver_types_1.SymbolKind.Class, location: { range: { end: { character: 33, line: 0, }, start: { character: 0, line: 0, }, }, uri: rootUri + 'a.ts', }, name: 'a', }, ]); }); }); it('should return no result if the PackageDescriptor does not match', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ symbol: { name: 'a', kind: 'class', package: { name: 'not-mypkg', }, }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, []); }); }); }); describe('with text query', () => { it('should find a symbol', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ query: 'a' }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { kind: vscode_languageserver_types_1.SymbolKind.Class, location: { range: { end: { character: 33, line: 0, }, start: { character: 0, line: 0, }, }, uri: rootUri + 'a.ts', }, name: 'a', }, ]); }); }); it('should return all symbols for an empty query excluding dependencies', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceSymbol({ query: '' }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { name: 'a', kind: vscode_languageserver_types_1.SymbolKind.Class, location: { uri: rootUri + 'a.ts', range: { start: { line: 0, character: 0, }, end: { line: 0, character: 33, }, }, }, }, { name: 'foo', kind: vscode_languageserver_types_1.SymbolKind.Method, location: { uri: rootUri + 'a.ts', range: { start: { line: 0, character: 10, }, end: { line: 0, character: 31, }, }, }, containerName: 'a', }, { name: 'i', kind: vscode_languageserver_types_1.SymbolKind.Constant, location: { uri: rootUri + 'a.ts', range: { start: { line: 0, character: 24, }, end: { line: 0, character: 29, }, }, }, containerName: 'foo', }, { name: '"c"', kind: vscode_languageserver_types_1.SymbolKind.Module, location: { uri: rootUri + 'c.ts', range: { start: { line: 0, character: 0, }, end: { line: 0, character: 28, }, }, }, }, { name: 'x', containerName: '"c"', kind: vscode_languageserver_types_1.SymbolKind.Variable, location: { uri: rootUri + 'c.ts', range: { start: { line: 0, character: 9, }, end: { line: 0, character: 10, }, }, }, }, { name: 'b', kind: vscode_languageserver_types_1.SymbolKind.Class, location: { uri: rootUri + 'foo/b.ts', range: { start: { line: 0, character: 0, }, end: { line: 0, character: 57, }, }, }, }, { name: 'bar', kind: vscode_languageserver_types_1.SymbolKind.Property, location: { uri: rootUri + 'foo/b.ts', range: { start: { line: 0, character: 10, }, end: { line: 0, character: 22, }, }, }, containerName: 'b', }, { name: 'baz', kind: vscode_languageserver_types_1.SymbolKind.Method, location: { uri: rootUri + 'foo/b.ts', range: { start: { line: 0, character: 23, }, end: { line: 0, character: 56, }, }, }, containerName: 'b', }, { name: 'qux', kind: vscode_languageserver_types_1.SymbolKind.Function, location: { uri: rootUri + 'foo/b.ts', range: { start: { line: 0, character: 59, }, end: { line: 0, character: 76, }, }, }, }, ]); }); }); }); }); describe('workspaceXreferences()', () => { it('should return all references to a method', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceXreferences({ query: { name: 'foo', kind: 'method', containerName: 'a' } }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { symbol: { filePath: 'a.ts', containerKind: '', containerName: 'a', name: 'foo', kind: 'method', }, reference: { range: { end: { character: 13, line: 0, }, start: { character: 9, line: 0, }, }, uri: rootUri + 'a.ts', }, }, ]); }); }); it('should return all references to a method with hinted dependee package name', function () { return __awaiter(this, void 0, void 0, function* () { const result = yield this.service .workspaceXreferences({ query: { name: 'foo', kind: 'method', containerName: 'a', }, hints: { dependeePackageName: 'mypkg', }, }) .reduce(fast_json_patch_1.applyReducer, null) .toPromise(); assert.deepEqual(result, [ { symbol: { filePath: 'a.ts', containerKind: '', containerName: 'a', name: 'foo', kind: 'method', }, reference: { range: { end: { character: 13, line: 0, }, start: { character: 9, line: 0, }, }, uri: rootUri + 'a.ts', },