javascript-typescript-langserver
Version:
Implementation of the Language Server Protocol for JavaScript and TypeScript
1,127 lines (1,126 loc) • 145 kB
JavaScript
"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',
},