javascript-typescript-langserver
Version:
Implementation of the Language Server Protocol for JavaScript and TypeScript
960 lines • 72.1 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 iterare_1 = require("iterare");
const lodash_1 = require("lodash");
const lodash_2 = require("lodash");
const hashObject = require("object-hash");
const opentracing_1 = require("opentracing");
const rxjs_1 = require("rxjs");
const ts = require("typescript");
const url = require("url");
const vscode_languageserver_1 = require("vscode-languageserver");
const ast_1 = require("./ast");
const diagnostics_1 = require("./diagnostics");
const fs_1 = require("./fs");
const logging_1 = require("./logging");
const memfs_1 = require("./memfs");
const packages_1 = require("./packages");
const project_manager_1 = require("./project-manager");
const symbols_1 = require("./symbols");
const tracing_1 = require("./tracing");
const util_1 = require("./util");
/**
* Maps string-based CompletionEntry::kind to enum-based CompletionItemKind
*/
const completionKinds = new Map([
[`class`, vscode_languageserver_1.CompletionItemKind.Class],
[`constructor`, vscode_languageserver_1.CompletionItemKind.Constructor],
[`enum`, vscode_languageserver_1.CompletionItemKind.Enum],
[`field`, vscode_languageserver_1.CompletionItemKind.Field],
[`file`, vscode_languageserver_1.CompletionItemKind.File],
[`function`, vscode_languageserver_1.CompletionItemKind.Function],
[`interface`, vscode_languageserver_1.CompletionItemKind.Interface],
[`keyword`, vscode_languageserver_1.CompletionItemKind.Keyword],
[`method`, vscode_languageserver_1.CompletionItemKind.Method],
[`module`, vscode_languageserver_1.CompletionItemKind.Module],
[`property`, vscode_languageserver_1.CompletionItemKind.Property],
[`reference`, vscode_languageserver_1.CompletionItemKind.Reference],
[`snippet`, vscode_languageserver_1.CompletionItemKind.Snippet],
[`text`, vscode_languageserver_1.CompletionItemKind.Text],
[`unit`, vscode_languageserver_1.CompletionItemKind.Unit],
[`value`, vscode_languageserver_1.CompletionItemKind.Value],
[`variable`, vscode_languageserver_1.CompletionItemKind.Variable],
]);
/**
* Handles incoming requests and return responses. There is a one-to-one-to-one
* correspondence between TCP connection, TypeScriptService instance, and
* language workspace. TypeScriptService caches data from the compiler across
* requests. The lifetime of the TypeScriptService instance is tied to the
* lifetime of the TCP connection, so its caches are deleted after the
* connection is torn down.
*
* Methods are camelCase versions of the LSP spec methods and dynamically
* dispatched. Methods not to be exposed over JSON RPC are prefixed with an
* underscore.
*/
class TypeScriptService {
constructor(client, options = {}) {
this.client = client;
this.options = options;
/**
* Settings synced though `didChangeConfiguration`
*/
this.settings = {
format: {
tabSize: 4,
indentSize: 4,
newLineCharacter: '\n',
convertTabsToSpaces: false,
insertSpaceAfterCommaDelimiter: true,
insertSpaceAfterSemicolonInForStatements: true,
insertSpaceBeforeAndAfterBinaryOperators: true,
insertSpaceAfterKeywordsInControlFlowStatements: true,
insertSpaceAfterFunctionKeywordForAnonymousFunctions: true,
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false,
insertSpaceBeforeFunctionParenthesis: false,
placeOpenBraceOnNewLineForFunctions: false,
placeOpenBraceOnNewLineForControlBlocks: false,
},
allowLocalPluginLoads: false,
globalPlugins: [],
pluginProbeLocations: [],
};
/**
* Indicates if the client prefers completion results formatted as snippets.
*/
this.supportsCompletionWithSnippets = false;
this.logger = new logging_1.LSPLogger(client);
}
/**
* The initialize request is sent as the first request from the client to the server. If the
* server receives request or notification before the `initialize` request it should act as
* follows:
*
* - for a request the respond should be errored with `code: -32002`. The message can be picked by
* the server.
* - notifications should be dropped, except for the exit notification. This will allow the exit a
* server without an initialize request.
*
* Until the server has responded to the `initialize` request with an `InitializeResult` the
* client must not sent any additional requests or notifications to the server.
*
* During the `initialize` request the server is allowed to sent the notifications
* `window/showMessage`, `window/logMessage` and `telemetry/event` as well as the
* `window/showMessageRequest` request to the client.
*
* @return Observable of JSON Patches that build an `InitializeResult`
*/
initialize(params, span = new opentracing_1.Span()) {
// tslint:disable:deprecation
if (params.rootUri || params.rootPath) {
this.root = params.rootPath || util_1.uri2path(params.rootUri);
this.rootUri = params.rootUri || util_1.path2uri(params.rootPath);
// tslint:enable:deprecation
this.supportsCompletionWithSnippets =
(params.capabilities.textDocument &&
params.capabilities.textDocument.completion &&
params.capabilities.textDocument.completion.completionItem &&
params.capabilities.textDocument.completion.completionItem.snippetSupport) ||
false;
// The root URI always refers to a directory
if (!this.rootUri.endsWith('/')) {
this.rootUri += '/';
}
this._initializeFileSystems(!this.options.strict && !(params.capabilities.xcontentProvider && params.capabilities.xfilesProvider));
this.updater = new fs_1.FileSystemUpdater(this.fileSystem, this.inMemoryFileSystem);
this.projectManager = new project_manager_1.ProjectManager(this.root, this.inMemoryFileSystem, this.updater, this.traceModuleResolution, this.settings, this.logger);
this.packageManager = new packages_1.PackageManager(this.updater, this.inMemoryFileSystem, this.logger);
// Detect DefinitelyTyped
// Fetch root package.json (if exists)
const normRootUri = this.rootUri.endsWith('/') ? this.rootUri : this.rootUri + '/';
const packageJsonUri = normRootUri + 'package.json';
this.isDefinitelyTyped = rxjs_1.Observable.from(this.packageManager.getPackageJson(packageJsonUri, span))
// Check name
.map(packageJson => packageJson.name === 'definitely-typed')
.catch(err => [false])
.publishReplay()
.refCount();
// Pre-fetch files in the background if not DefinitelyTyped
this.isDefinitelyTyped
.mergeMap(isDefinitelyTyped => {
if (!isDefinitelyTyped) {
return this.projectManager.ensureOwnFiles(span);
}
return [];
})
.subscribe(undefined, err => {
this.logger.error(err);
});
}
const result = {
capabilities: {
// Tell the client that the server works in FULL text document sync mode
textDocumentSync: vscode_languageserver_1.TextDocumentSyncKind.Full,
hoverProvider: true,
signatureHelpProvider: {
triggerCharacters: ['(', ','],
},
definitionProvider: true,
typeDefinitionProvider: true,
referencesProvider: true,
documentSymbolProvider: true,
workspaceSymbolProvider: true,
xworkspaceReferencesProvider: true,
xdefinitionProvider: true,
xdependenciesProvider: true,
completionProvider: {
resolveProvider: true,
triggerCharacters: ['.'],
},
codeActionProvider: true,
renameProvider: true,
executeCommandProvider: {
commands: [],
},
xpackagesProvider: true,
},
};
return rxjs_1.Observable.of({
op: 'add',
path: '',
value: result,
});
}
/**
* Initializes the remote file system and in-memory file system.
* Can be overridden
*
* @param accessDisk Whether the language server is allowed to access the local file system
*/
_initializeFileSystems(accessDisk) {
this.fileSystem = accessDisk ? new fs_1.LocalFileSystem(this.rootUri) : new fs_1.RemoteFileSystem(this.client);
this.inMemoryFileSystem = new memfs_1.InMemoryFileSystem(this.root, this.logger);
}
/**
* The shutdown request is sent from the client to the server. It asks the server to shut down,
* but to not exit (otherwise the response might not be delivered correctly to the client).
* There is a separate exit notification that asks the server to exit.
*
* @return Observable of JSON Patches that build a `null` result
*/
shutdown(params = {}, span = new opentracing_1.Span()) {
this.projectManager.dispose();
this.packageManager.dispose();
return rxjs_1.Observable.of({ op: 'add', path: '', value: null });
}
/**
* A notification sent from the client to the server to signal the change of configuration
* settings.
*/
workspaceDidChangeConfiguration(params) {
lodash_1.merge(this.settings, params.settings);
}
/**
* The goto definition request is sent from the client to the server to resolve the definition
* location of a symbol at a given text document position.
*
* @return Observable of JSON Patches that build a `Location[]` result
*/
textDocumentDefinition(params, span = new opentracing_1.Span()) {
return this._getDefinitionLocations(params, span, false)
.map((location) => ({ op: 'add', path: '/-', value: location }))
.startWith({ op: 'add', path: '', value: [] });
}
/**
* The goto type definition request is sent from the client to the server to resolve the type
* location of a symbol at a given text document position.
*
* @return Observable of JSON Patches that build a `Location[]` result
*/
textDocumentTypeDefinition(params, span = new opentracing_1.Span()) {
return this._getDefinitionLocations(params, span, true)
.map((location) => ({ op: 'add', path: '/-', value: location }))
.startWith({ op: 'add', path: '', value: [] });
}
/**
* Returns an Observable of all definition locations found for a symbol.
*/
_getDefinitionLocations(params, span = new opentracing_1.Span(), goToType = false) {
const uri = util_1.normalizeUri(params.textDocument.uri);
// Fetch files needed to resolve definition
return this.projectManager
.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.mergeMap(() => {
const fileName = util_1.uri2path(uri);
const configuration = this.projectManager.getConfiguration(fileName);
configuration.ensureBasicFiles(span);
const sourceFile = this._getSourceFile(configuration, fileName, span);
if (!sourceFile) {
throw new Error(`Expected source file ${fileName} to exist`);
}
const offset = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character);
const definitions = goToType
? configuration.getService().getTypeDefinitionAtPosition(fileName, offset)
: configuration.getService().getDefinitionAtPosition(fileName, offset);
return rxjs_1.Observable.from(definitions || []).map((definition) => {
const sourceFile = this._getSourceFile(configuration, definition.fileName, span);
if (!sourceFile) {
throw new Error('expected source file "' + definition.fileName + '" to exist in configuration');
}
const start = ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start);
const end = ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start + definition.textSpan.length);
return {
uri: symbols_1.locationUri(definition.fileName),
range: {
start,
end,
},
};
});
});
}
/**
* This method is the same as textDocument/definition, except that:
*
* - The method returns metadata about the definition (the same metadata that
* workspace/xreferences searches for).
* - The concrete location to the definition (location field)
* is optional. This is useful because the language server might not be able to resolve a goto
* definition request to a concrete location (e.g. due to lack of dependencies) but still may
* know some information about it.
*
* @return Observable of JSON Patches that build a `SymbolLocationInformation[]` result
*/
textDocumentXdefinition(params, span = new opentracing_1.Span()) {
return this._getSymbolLocationInformations(params, span)
.map(symbol => ({ op: 'add', path: '/-', value: symbol }))
.startWith({ op: 'add', path: '', value: [] });
}
/**
* Returns an Observable of SymbolLocationInformations for the definition of a symbol at the given position
*/
_getSymbolLocationInformations(params, span = new opentracing_1.Span()) {
const uri = util_1.normalizeUri(params.textDocument.uri);
// Ensure files needed to resolve SymbolLocationInformation are fetched
return this.projectManager
.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.mergeMap(() => {
// Convert URI to file path
const fileName = util_1.uri2path(uri);
// Get closest tsconfig configuration
const configuration = this.projectManager.getConfiguration(fileName);
configuration.ensureBasicFiles(span);
const sourceFile = this._getSourceFile(configuration, fileName, span);
if (!sourceFile) {
throw new Error(`Unknown text document ${uri}`);
}
// Convert line/character to offset
const offset = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character);
// Query TypeScript for references
return rxjs_1.Observable.from(configuration.getService().getDefinitionAtPosition(fileName, offset) || []).mergeMap((definition) => {
const definitionUri = symbols_1.locationUri(definition.fileName);
// Get the PackageDescriptor
return this._getPackageDescriptor(definitionUri)
.defaultIfEmpty(undefined)
.map((packageDescriptor) => {
const sourceFile = this._getSourceFile(configuration, definition.fileName, span);
if (!sourceFile) {
throw new Error(`Expected source file ${definition.fileName} to exist in configuration`);
}
const symbol = symbols_1.definitionInfoToSymbolDescriptor(definition, this.root);
if (packageDescriptor) {
symbol.package = packageDescriptor;
}
return {
symbol,
location: {
uri: definitionUri,
range: {
start: ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start),
end: ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start + definition.textSpan.length),
},
},
};
});
});
});
}
/**
* Finds the PackageDescriptor a given file belongs to
*
* @return Observable that emits a single PackageDescriptor or never if the definition does not belong to any package
*/
_getPackageDescriptor(uri, childOf = new opentracing_1.Span()) {
return tracing_1.traceObservable('Get PackageDescriptor', childOf, span => {
span.addTags({ uri });
// Get package name of the dependency in which the symbol is defined in, if any
const packageName = packages_1.extractNodeModulesPackageName(uri);
if (packageName) {
// The symbol is part of a dependency in node_modules
// Build URI to package.json of the Dependency
const encodedPackageName = packageName
.split('/')
.map(encodeURIComponent)
.join('/');
const parts = url.parse(uri);
const packageJsonUri = url.format(Object.assign({}, parts, { pathname: parts.pathname.slice(0, parts.pathname.lastIndexOf('/node_modules/' + encodedPackageName)) +
`/node_modules/${encodedPackageName}/package.json` }));
// Fetch the package.json of the dependency
return this.updater.ensure(packageJsonUri, span).concat(rxjs_1.Observable.defer(() => {
const packageJson = JSON.parse(this.inMemoryFileSystem.getContent(packageJsonUri));
const { name, version } = packageJson;
if (!name) {
return rxjs_1.Observable.empty();
}
// Used by the LSP proxy to shortcut database lookup of repo URL for PackageDescriptor
let repoURL;
if (name.startsWith('@types/')) {
// if the dependency package is an @types/ package, point the repo to DefinitelyTyped
repoURL = 'https://github.com/DefinitelyTyped/DefinitelyTyped';
}
else {
// else use repository field from package.json
repoURL =
typeof packageJson.repository === 'object' ? packageJson.repository.url : undefined;
}
return rxjs_1.Observable.of({ name, version, repoURL });
}));
}
else {
// The symbol is defined in the root package of the workspace, not in a dependency
// Get root package.json
return this.packageManager.getClosestPackageJson(uri, span).mergeMap(packageJson => {
let { name, version } = packageJson;
if (!name) {
return [];
}
let repoURL = typeof packageJson.repository === 'object' ? packageJson.repository.url : undefined;
// If the root package is DefinitelyTyped, find out the proper @types package name for each typing
if (name === 'definitely-typed') {
name = packages_1.extractDefinitelyTypedPackageName(uri);
if (!name) {
this.logger.error(`Could not extract package name from DefinitelyTyped URI ${uri}`);
return [];
}
version = undefined;
repoURL = 'https://github.com/DefinitelyTyped/DefinitelyTyped';
}
return [{ name, version, repoURL }];
});
}
});
}
/**
* The hover request is sent from the client to the server to request hover information at a
* given text document position.
*
* @return Observable of JSON Patches that build a `Hover` result
*/
textDocumentHover(params, span = new opentracing_1.Span()) {
return this._getHover(params, span).map(hover => ({ op: 'add', path: '', value: hover }));
}
/**
* Returns an Observable for a Hover at the given position
*/
_getHover(params, span = new opentracing_1.Span()) {
const uri = util_1.normalizeUri(params.textDocument.uri);
// Ensure files needed to resolve hover are fetched
return this.projectManager
.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.map(() => {
const fileName = util_1.uri2path(uri);
const configuration = this.projectManager.getConfiguration(fileName);
configuration.ensureBasicFiles(span);
const sourceFile = this._getSourceFile(configuration, fileName, span);
if (!sourceFile) {
throw new Error(`Unknown text document ${uri}`);
}
const offset = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character);
const info = configuration.getService().getQuickInfoAtPosition(fileName, offset);
if (!info) {
return { contents: [] };
}
const contents = [];
// Add declaration without the kind
const declaration = ts.displayPartsToString(info.displayParts).replace(/^\(.+?\)\s+/, '');
contents.push({ language: 'typescript', value: declaration });
// Add kind with modifiers, e.g. "method (private, ststic)", "class (exported)"
if (info.kind) {
let kind = '**' + info.kind + '**';
const modifiers = info.kindModifiers
.split(',')
// Filter out some quirks like "constructor (exported)"
.filter(mod => mod &&
(mod !== ts.ScriptElementKindModifier.exportedModifier ||
info.kind !== ts.ScriptElementKind.constructorImplementationElement))
// Make proper adjectives
.map(mod => {
switch (mod) {
case ts.ScriptElementKindModifier.ambientModifier:
return 'ambient';
case ts.ScriptElementKindModifier.exportedModifier:
return 'exported';
default:
return mod;
}
});
if (modifiers.length > 0) {
kind += ' _(' + modifiers.join(', ') + ')_';
}
contents.push(kind);
}
// Add documentation
const documentation = ts.displayPartsToString(info.documentation);
if (documentation) {
contents.push(documentation);
}
const start = ts.getLineAndCharacterOfPosition(sourceFile, info.textSpan.start);
const end = ts.getLineAndCharacterOfPosition(sourceFile, info.textSpan.start + info.textSpan.length);
return {
contents,
range: {
start,
end,
},
};
});
}
/**
* The references request is sent from the client to the server to resolve project-wide
* references for the symbol denoted by the given text document position.
*
* Returns all references to the symbol at the position in the own workspace, including references inside node_modules.
*
* @return Observable of JSON Patches that build a `Location[]` result
*/
textDocumentReferences(params, span = new opentracing_1.Span()) {
const uri = util_1.normalizeUri(params.textDocument.uri);
// Ensure all files were fetched to collect all references
return (this.projectManager
.ensureOwnFiles(span)
.concat(rxjs_1.Observable.defer(() => {
// Convert URI to file path because TypeScript doesn't work with URIs
const fileName = util_1.uri2path(uri);
// Get tsconfig configuration for requested file
const configuration = this.projectManager.getConfiguration(fileName);
// Ensure all files have been added
configuration.ensureAllFiles(span);
const program = configuration.getProgram(span);
if (!program) {
return rxjs_1.Observable.empty();
}
// Get SourceFile object for requested file
const sourceFile = this._getSourceFile(configuration, fileName, span);
if (!sourceFile) {
throw new Error(`Source file ${fileName} does not exist`);
}
// Convert line/character to offset
const offset = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character);
// Request references at position from TypeScript
// Despite the signature, getReferencesAtPosition() can return undefined
return rxjs_1.Observable.from(configuration.getService().getReferencesAtPosition(fileName, offset) || [])
.filter(reference =>
// Filter declaration if not requested
(!reference.isDefinition ||
(params.context && params.context.includeDeclaration)) &&
// Filter references in node_modules
!reference.fileName.includes('/node_modules/'))
.map((reference) => {
const sourceFile = program.getSourceFile(reference.fileName);
if (!sourceFile) {
throw new Error(`Source file ${reference.fileName} does not exist`);
}
// Convert offset to line/character position
const start = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start);
const end = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start + reference.textSpan.length);
return {
uri: util_1.path2uri(reference.fileName),
range: {
start,
end,
},
};
});
}))
.map((location) => ({ op: 'add', path: '/-', value: location }))
// Initialize with array
.startWith({ op: 'add', path: '', value: [] }));
}
/**
* The workspace symbol request is sent from the client to the server to list project-wide
* symbols matching the query string. The text document parameter specifies the active document
* at time of the query. This can be used to rank or limit results.
*
* @return Observable of JSON Patches that build a `SymbolInformation[]` result
*/
workspaceSymbol(params, span = new opentracing_1.Span()) {
// Return cached result for empty query, if available
if (!params.query && !params.symbol && this.emptyQueryWorkspaceSymbols) {
return this.emptyQueryWorkspaceSymbols;
}
/** A sorted array that keeps track of symbol match scores to determine the index to insert the symbol at */
const scores = [];
let observable = this.isDefinitelyTyped
.mergeMap((isDefinitelyTyped) => {
// Use special logic for DefinitelyTyped
// Search only in the correct subdirectory for the given PackageDescriptor
if (isDefinitelyTyped) {
// Error if not passed a SymbolDescriptor query with an `@types` PackageDescriptor
if (!params.symbol ||
!params.symbol.package ||
!params.symbol.package.name ||
!params.symbol.package.name.startsWith('@types/')) {
return rxjs_1.Observable.throw(new Error('workspace/symbol on DefinitelyTyped is only supported with a SymbolDescriptor query with an @types PackageDescriptor'));
}
// Fetch all files in the package subdirectory
// All packages are in the types/ subdirectory
const normRootUri = this.rootUri.endsWith('/') ? this.rootUri : this.rootUri + '/';
const packageRootUri = normRootUri + params.symbol.package.name.substr(1) + '/';
return this.updater
.ensureStructure(span)
.concat(rxjs_1.Observable.defer(() => util_1.observableFromIterable(this.inMemoryFileSystem.uris())))
.filter(uri => uri.startsWith(packageRootUri))
.mergeMap(uri => this.updater.ensure(uri, span))
.concat(rxjs_1.Observable.defer(() => {
span.log({ event: 'fetched package files' });
const config = this.projectManager.getParentConfiguration(packageRootUri, 'ts');
if (!config) {
throw new Error(`Could not find tsconfig for ${packageRootUri}`);
}
// Don't match PackageDescriptor on symbols
return this._getSymbolsInConfig(config, lodash_1.omit(params.symbol, 'package'), span);
}));
}
// Regular workspace symbol search
// Search all symbols in own code, but not in dependencies
return (this.projectManager
.ensureOwnFiles(span)
.concat(rxjs_1.Observable.defer(() => {
if (params.symbol && params.symbol.package && params.symbol.package.name) {
// If SymbolDescriptor query with PackageDescriptor, search for package.jsons with matching package name
return (util_1.observableFromIterable(this.packageManager.packageJsonUris())
.filter(packageJsonUri => JSON.parse(this.inMemoryFileSystem.getContent(packageJsonUri)).name === params.symbol.package.name)
// Find their parent and child tsconfigs
.mergeMap(packageJsonUri => rxjs_1.Observable.merge(lodash_1.castArray(this.projectManager.getParentConfiguration(packageJsonUri) || []),
// Search child directories starting at the directory of the package.json
util_1.observableFromIterable(this.projectManager.getChildConfigurations(url.resolve(packageJsonUri, '.'))))));
}
// Else search all tsconfigs in the workspace
return util_1.observableFromIterable(this.projectManager.configurations());
}))
// If PackageDescriptor is given, only search project with the matching package name
.mergeMap(config => this._getSymbolsInConfig(config, params.query || params.symbol, span)));
})
// Filter duplicate symbols
// There may be few configurations that contain the same file(s)
// or files from different configurations may refer to the same file(s)
.distinct(symbol => hashObject(symbol, { respectType: false }))
// Limit the total amount of symbols returned for text or empty queries
// Higher limit for programmatic symbol queries because it could exclude results with a higher score
.take(params.symbol ? 1000 : 100)
// Find out at which index to insert the symbol to maintain sorting order by score
.map(([score, symbol]) => {
const index = scores.findIndex(s => s < score);
if (index === -1) {
scores.push(score);
return { op: 'add', path: '/-', value: symbol };
}
scores.splice(index, 0, score);
return { op: 'add', path: '/' + index, value: symbol };
})
.startWith({ op: 'add', path: '', value: [] });
if (!params.query && !params.symbol) {
observable = this.emptyQueryWorkspaceSymbols = observable.publishReplay().refCount();
}
return observable;
}
/**
* The document symbol request is sent from the client to the server to list all symbols found
* in a given text document.
*
* @return Observable of JSON Patches that build a `SymbolInformation[]` result
*/
textDocumentDocumentSymbol(params, span = new opentracing_1.Span()) {
const uri = util_1.normalizeUri(params.textDocument.uri);
// Ensure files needed to resolve symbols are fetched
return this.projectManager
.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.mergeMap(() => {
const fileName = util_1.uri2path(uri);
const config = this.projectManager.getConfiguration(fileName);
config.ensureBasicFiles(span);
const sourceFile = this._getSourceFile(config, fileName, span);
if (!sourceFile) {
return [];
}
const tree = config.getService().getNavigationTree(fileName);
return util_1.observableFromIterable(symbols_1.walkNavigationTree(tree))
.filter(({ tree, parent }) => symbols_1.navigationTreeIsSymbol(tree))
.map(({ tree, parent }) => symbols_1.navigationTreeToSymbolInformation(tree, parent, sourceFile, this.root));
})
.map(symbol => ({ op: 'add', path: '/-', value: symbol }))
.startWith({ op: 'add', path: '', value: [] });
}
/**
* The workspace references request is sent from the client to the server to locate project-wide
* references to a symbol given its description / metadata.
*
* @return Observable of JSON Patches that build a `ReferenceInformation[]` result
*/
workspaceXreferences(params, span = new opentracing_1.Span()) {
const queryWithoutPackage = lodash_1.omit(params.query, 'package');
const minScore = Math.min(4.75, util_1.getPropertyCount(queryWithoutPackage));
return this.isDefinitelyTyped
.mergeMap(isDefinitelyTyped => {
if (isDefinitelyTyped) {
throw new Error('workspace/xreferences not supported in DefinitelyTyped');
}
return this.projectManager.ensureAllFiles(span);
})
.concat(rxjs_1.Observable.defer(() => {
// if we were hinted that we should only search a specific package, find it and only search the owning tsconfig.json
if (params.hints && params.hints.dependeePackageName) {
return util_1.observableFromIterable(this.packageManager.packageJsonUris())
.filter(uri => JSON.parse(this.inMemoryFileSystem.getContent(uri)).name ===
params.hints.dependeePackageName)
.take(1)
.mergeMap(uri => {
const config = this.projectManager.getParentConfiguration(uri);
if (!config) {
return util_1.observableFromIterable(this.projectManager.configurations());
}
return [config];
});
}
// else search all tsconfig.jsons
return util_1.observableFromIterable(this.projectManager.configurations());
}))
.mergeMap((config) => {
config.ensureAllFiles(span);
const program = config.getProgram(span);
if (!program) {
return rxjs_1.Observable.empty();
}
return (rxjs_1.Observable.from(program.getSourceFiles())
// Ignore dependency files
.filter(source => !util_1.toUnixPath(source.fileName).includes('/node_modules/'))
.mergeMap(source =>
// Iterate AST of source file
util_1.observableFromIterable(ast_1.walkMostAST(source))
// Filter Identifier Nodes
// TODO: include string-interpolated references
.filter((node) => node.kind === ts.SyntaxKind.Identifier)
.mergeMap(node => {
try {
// Find definition for node
return rxjs_1.Observable.from(config
.getService()
.getDefinitionAtPosition(source.fileName, node.pos + 1) || [])
.mergeMap(definition => {
const symbol = symbols_1.definitionInfoToSymbolDescriptor(definition, this.root);
// Check if SymbolDescriptor without PackageDescriptor matches
const score = util_1.getMatchingPropertyCount(queryWithoutPackage, symbol);
if (score < minScore ||
(params.query.package &&
!definition.fileName.includes(params.query.package.name))) {
return [];
}
span.log({ event: 'match', score });
// If no PackageDescriptor query, return match
if (!params.query.package) {
return [symbol];
}
// If SymbolDescriptor matched and the query contains a PackageDescriptor, get package.json and match PackageDescriptor name
// TODO match full PackageDescriptor (version) and fill out the symbol.package field
const uri = util_1.path2uri(definition.fileName);
return this._getPackageDescriptor(uri, span)
.defaultIfEmpty(undefined)
.filter(packageDescriptor => !!(packageDescriptor &&
packageDescriptor.name === params.query.package.name))
.map(packageDescriptor => {
symbol.package = packageDescriptor;
return symbol;
});
})
.map((symbol) => ({
symbol,
reference: {
uri: symbols_1.locationUri(source.fileName),
range: {
start: ts.getLineAndCharacterOfPosition(source, node.pos),
end: ts.getLineAndCharacterOfPosition(source, node.end),
},
},
}));
}
catch (err) {
// Continue with next node on error
// Workaround for https://github.com/Microsoft/TypeScript/issues/15219
this.logger.error(`workspace/xreferences: Error getting definition for ${source.fileName} at offset ${node.pos + 1}`, err);
span.log({
event: 'error',
'error.object': err,
message: err.message,
stack: err.stack,
});
return [];
}
})));
})
.map((reference) => ({ op: 'add', path: '/-', value: reference }))
.startWith({ op: 'add', path: '', value: [] });
}
/**
* This method returns metadata about the package(s) defined in a workspace and a list of
* dependencies for each package.
*
* This method is necessary to implement cross-repository jump-to-def when it is not possible to
* resolve the global location of the definition from data present or derived from the local
* workspace. For example, a package manager might not include information about the source
* repository of each dependency. In this case, definition resolution requires mapping from
* package descriptor to repository revision URL. A reverse index can be constructed from calls
* to workspace/xpackages to provide an efficient mapping.
*
* @return Observable of JSON Patches that build a `PackageInformation[]` result
*/
workspaceXpackages(params = {}, span = new opentracing_1.Span()) {
return this.isDefinitelyTyped
.mergeMap((isDefinitelyTyped) => {
// In DefinitelyTyped, report all @types/ packages
if (isDefinitelyTyped) {
const typesUri = url.resolve(this.rootUri, 'types/');
return (util_1.observableFromIterable(this.inMemoryFileSystem.uris())
// Find all types/ subdirectories
.filter(uri => uri.startsWith(typesUri))
// Get the directory names
.map((uri) => ({
package: {
name: '@types/' +
decodeURIComponent(uri.substr(typesUri.length).split('/')[0]),
},
// TODO parse /// <reference types="node" /> comments in .d.ts files for collecting dependencies between @types packages
dependencies: [],
})));
}
// For other workspaces, search all package.json files
return (this.projectManager
.ensureModuleStructure(span)
// Iterate all files
.concat(rxjs_1.Observable.defer(() => util_1.observableFromIterable(this.inMemoryFileSystem.uris())))
// Filter own package.jsons
.filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/'))
// Map to contents of package.jsons
.mergeMap(uri => this.packageManager.getPackageJson(uri))
// Map each package.json to a PackageInformation
.mergeMap(packageJson => {
if (!packageJson.name) {
return [];
}
const packageDescriptor = {
name: packageJson.name,
version: packageJson.version,
repoURL: (typeof packageJson.repository === 'object' && packageJson.repository.url) ||
undefined,
};
// Collect all dependencies for this package.json
return (rxjs_1.Observable.from(packages_1.DEPENDENCY_KEYS)
.filter(key => !!packageJson[key])
// Get [name, version] pairs
.mergeMap(key => lodash_2.toPairs(packageJson[key]))
// Map to DependencyReferences
.map(([name, version]) => ({
attributes: {
name,
version,
},
hints: {
dependeePackageName: packageJson.name,
},
}))
.toArray()
.map((dependencies) => ({
package: packageDescriptor,
dependencies,
})));
}));
})
.map((packageInfo) => ({ op: 'add', path: '/-', value: packageInfo }))
.startWith({ op: 'add', path: '', value: [] });
}
/**
* Returns all dependencies of a workspace.
* Superseded by workspace/xpackages
*
* @return Observable of JSON Patches that build a `DependencyReference[]` result
*/
workspaceXdependencies(params = {}, span = new opentracing_1.Span()) {
// Ensure package.json files
return (this.projectManager
.ensureModuleStructure()
// Iterate all files
.concat(rxjs_1.Observable.defer(() => util_1.observableFromIterable(this.inMemoryFileSystem.uris())))
// Filter own package.jsons
.filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/'))
// Ensure contents of own package.jsons
.mergeMap(uri => this.packageManager.getPackageJson(uri))
// Map package.json to DependencyReferences
.mergeMap(packageJson => rxjs_1.Observable.from(packages_1.DEPENDENCY_KEYS)
.filter(key => !!packageJson[key])
// Get [name, version] pairs
.mergeMap(key => lodash_2.toPairs(packageJson[key]))
.map(([name, version]) => ({
attributes: {
name,
version,
},
hints: {
dependeePackageName: packageJson.name,
},
})))
.map((dependency) => ({ op: 'add', path: '/-', value: dependency }))
.startWith({ op: 'add', path: '', value: [] }));
}
/**
* The Completion request is sent from the client to the server to compute completion items at a
* given cursor position. Completion items are presented in the
* [IntelliSense](https://code.visualstudio.com/docs/editor/editingevolved#_intellisense) user
* interface. If computing full completion items is expensive, servers can additionally provide
* a handler for the completion item resolve request ('completionItem/resolve'). This request is
* sent when a completion item is selected in the user interface. A typically use case is for
* example: the 'textDocument/completion' request doesn't fill in the `documentation` property
* for returned completion items since it is expensive to compute. When the item is selected in
* the user interface then a 'completionItem/resolve' request is sent with the selected
* completion item as a param. The returned completion item should have the documentation
* property filled in.
*
* @return Observable of JSON Patches that build a `CompletionList` result
*/
textDocumentCompletion(params, span = new opentracing_1.Span()) {
const uri = util_1.normalizeUri(params.textDocument.uri);
// Ensure files needed to suggest completions are fetched
return this.projectManager
.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.mergeMap(() => {
const fileName = util_1.uri2path(uri);
const configuration = this.projectManager.getConfiguration(fileName);
configuration.ensureBasicFiles(span);
const sourceFile = this._getSourceFile(configuration, fileName, span);
if (!sourceFile) {
return [];
}
const offset = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character);
const completions = configuration.getService().getCompletionsAtPosition(fileName, offset, undefined);
if (!completions) {
return [];
}
return rxjs_1.Observable.from(completions.entries)
.map(entry => {
const item = { label: entry.name };
const kind = completionKinds.get(entry.kind);
if (kind) {
item.kind = kind;
}
if (entry.sortText) {
item.sortText = entry.sortText;
}
// context for future resolve requests:
item.data = {
uri,
offset,
entryName: entry.name,
};
return { op: 'add', path: '/items/-', value: item };
})
.startWith({ op: 'add', path: '/isIncomplete', value: false });
})
.startWith({ op: 'add', path: '', value: { isIncomplete: true, items: [] } });
}
/**
* The completionItem/resolve request is used to fill in additional details from an incomplete
* CompletionItem returned from the textDocument/completions call.
*
* @return Observable of JSON Patches that build a `CompletionItem` result
*/
completionItemResolve(item, span = new opentracing_1.Span()) {
if (!item.data) {
throw new Error('Cannot resolve completion item without data');
}
const { uri, offset, entryName } = item.data;
const fileName = util_1.uri2path(uri);
return this.projectManager
.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.map(() => {
const configuration = this.projectManager.getConfiguration(fileName);
configuration.ensu