javascript-typescript-langserver
Version:
Implementation of the Language Server Protocol for JavaScript and TypeScript
302 lines • 12.2 kB
JavaScript
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 events_1 = require("events");
const fast_json_patch_1 = require("fast-json-patch");
const lodash_1 = require("lodash");
const opentracing_1 = require("opentracing");
const rxjs_1 = require("rxjs");
const util_1 = require("util");
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const messages_1 = require("vscode-jsonrpc/lib/messages");
const logging_1 = require("./logging");
/**
* Returns true if the passed argument has a meta field
*/
function hasMeta(candidate) {
return (typeof candidate === 'object' &&
candidate !== null &&
typeof candidate.meta === 'object' &&
candidate.meta !== null);
}
/**
* Returns true if the passed argument is an object with a `.then()` method
*/
function isPromiseLike(candidate) {
return typeof candidate === 'object' && candidate !== null && typeof candidate.then === 'function';
}
/**
* Returns true if the passed argument is an object with a `[Symbol.observable]` method
*/
function isObservable(candidate) {
return typeof candidate === 'object' && candidate !== null && typeof candidate[rxjs_1.Symbol.observable] === 'function';
}
/**
* Takes a NodeJS ReadableStream and emits parsed messages received on the stream.
* In opposite to StreamMessageReader, supports multiple listeners and is compatible with Observables
*/
class MessageEmitter extends events_1.EventEmitter {
constructor(input, options = {}) {
super();
const reader = new vscode_jsonrpc_1.StreamMessageReader(input);
// Forward events
reader.listen(msg => {
this.emit('message', msg);
});
reader.onError(err => {
this.emit('error', err);
});
reader.onClose(() => {
this.emit('close');
});
this.setMaxListeners(Infinity);
// Register message listener to log messages if configured
if (options.logMessages && options.logger) {
const logger = options.logger;
this.on('message', message => {
logger.log('-->', message);
});
}
}
/* istanbul ignore next */
on(event, listener) {
return super.on(event, listener);
}
/* istanbul ignore next */
once(event, listener) {
return super.on(event, listener);
}
}
exports.MessageEmitter = MessageEmitter;
/**
* Wraps vscode-jsonrpcs StreamMessageWriter to support logging messages,
* decouple our code from the vscode-jsonrpc module and provide a more
* consistent event API
*/
class MessageWriter {
/**
* @param output The output stream to write to (e.g. STDOUT or a socket)
* @param options
*/
constructor(output, options = {}) {
this.vscodeWriter = new vscode_jsonrpc_1.StreamMessageWriter(output);
this.logger = options.logger || new logging_1.NoopLogger();
this.logMessages = !!options.logMessages;
}
/**
* Writes a JSON RPC message to the output stream.
* Logs it if configured
*
* @param message A complete JSON RPC message object
*/
write(message) {
if (this.logMessages) {
this.logger.log('<--', message);
}
this.vscodeWriter.write(message);
}
}
exports.MessageWriter = MessageWriter;
/**
* Registers all method implementations of a LanguageHandler on a connection
*
* @param messageEmitter MessageEmitter to listen on
* @param messageWriter MessageWriter to write to
* @param handler TypeScriptService object that contains methods for all methods to be handled
*/
function registerLanguageHandler(messageEmitter, messageWriter, handler, options = {}) {
const logger = options.logger || new logging_1.NoopLogger();
const tracer = options.tracer || new opentracing_1.Tracer();
/** Tracks Subscriptions for results to unsubscribe them on $/cancelRequest */
const subscriptions = new Map();
/**
* Whether the handler is in an initialized state.
* `initialize` sets this to true, `shutdown` to false.
* Used to determine whether a manual `shutdown` call is needed on error/close
*/
let initialized = false;
/** Whether the client supports streaming with $/partialResult */
let streaming = false;
messageEmitter.on('message', (message) => __awaiter(this, void 0, void 0, function* () {
// Ignore responses
if (messages_1.isResponseMessage(message)) {
return;
}
if (!messages_1.isRequestMessage(message) && !messages_1.isNotificationMessage(message)) {
logger.error('Received invalid message:', message);
return;
}
switch (message.method) {
case 'initialize':
initialized = true;
streaming = !!message.params.capabilities.streaming;
break;
case 'shutdown':
initialized = false;
break;
case 'exit':
// Ignore exit notification, it's not the responsibility of the TypeScriptService to handle it,
// but the TCP / STDIO server which needs to close the socket or kill the process
for (const subscription of subscriptions.values()) {
subscription.unsubscribe();
}
return;
case '$/cancelRequest':
// Cancel another request by unsubscribing from the Observable
const subscription = subscriptions.get(message.params.id);
if (!subscription) {
logger.warn(`$/cancelRequest for unknown request ID ${message.params.id}`);
return;
}
subscription.unsubscribe();
subscriptions.delete(message.params.id);
messageWriter.write({
jsonrpc: '2.0',
id: message.params.id,
error: {
message: 'Request cancelled',
code: vscode_jsonrpc_1.ErrorCodes.RequestCancelled,
},
});
return;
}
const method = lodash_1.camelCase(message.method);
let context;
// If message is request and has tracing metadata, extract the span context
if (messages_1.isRequestMessage(message) && hasMeta(message)) {
context = tracer.extract(opentracing_1.FORMAT_TEXT_MAP, message.meta) || undefined;
}
const span = tracer.startSpan('Handle ' + message.method, { childOf: context });
span.setTag('params', util_1.inspect(message.params));
if (typeof handler[method] !== 'function') {
// Method not implemented
if (messages_1.isRequestMessage(message)) {
messageWriter.write({
jsonrpc: '2.0',
id: message.id,
error: {
code: vscode_jsonrpc_1.ErrorCodes.MethodNotFound,
message: `Method ${method} not implemented`,
},
});
}
else {
logger.warn(`Method ${method} not implemented`);
}
return;
}
// Call handler method with params and span
let observable;
try {
// Convert return value to Observable
const returnValue = handler[method](message.params, span);
if (isObservable(returnValue)) {
observable = returnValue;
}
else if (isPromiseLike(returnValue)) {
observable = rxjs_1.Observable.from(returnValue);
}
else {
observable = rxjs_1.Observable.of(returnValue);
}
}
catch (err) {
observable = rxjs_1.Observable.throw(err);
}
if (messages_1.isRequestMessage(message)) {
const subscription = observable
.do(patch => {
if (streaming) {
span.log({ event: 'partialResult', patch });
// Send $/partialResult for partial result patches only if client supports it
messageWriter.write({
jsonrpc: '2.0',
method: '$/partialResult',
params: {
id: message.id,
patch: [patch],
},
});
}
})
// Build up final result for BC
// TODO send null if client declared streaming capability
.reduce(fast_json_patch_1.applyReducer, null)
.finally(() => {
// Finish span
span.finish();
// Delete subscription from Map
// Make sure to not run this before subscription.set() was called
// (in case the Observable is synchronous)
process.nextTick(() => {
subscriptions.delete(message.id);
});
})
.subscribe(result => {
// Send final result
messageWriter.write({
jsonrpc: '2.0',
id: message.id,
result,
});
}, err => {
// Set error on span
span.setTag('error', true);
span.log({ event: 'error', 'error.object': err, message: err.message, stack: err.stack });
// Log error
logger.error(`Handler for ${message.method} failed:`, err, '\nMessage:', message);
// Send error response
messageWriter.write({
jsonrpc: '2.0',
id: message.id,
error: {
message: err.message + '',
code: typeof err.code === 'number' ? err.code : vscode_jsonrpc_1.ErrorCodes.UnknownErrorCode,
data: lodash_1.omit(err, ['message', 'code']),
},
});
});
// Save subscription for $/cancelRequest
subscriptions.set(message.id, subscription);
}
else {
// For notifications, still subscribe and log potential error
observable.subscribe(undefined, err => {
logger.error(`Handle ${method}:`, err);
});
}
}));
// On stream close, shutdown handler if it was initialized
messageEmitter.once('close', () => {
// Cancel all outstanding requests
for (const subscription of subscriptions.values()) {
subscription.unsubscribe();
}
if (initialized) {
initialized = false;
logger.error('Stream was closed without shutdown notification');
handler.shutdown();
}
});
// On stream error, shutdown handler if it was initialized
messageEmitter.once('error', err => {
// Cancel all outstanding requests
for (const subscription of subscriptions.values()) {
subscription.unsubscribe();
}
if (initialized) {
initialized = false;
logger.error('Stream:', err);
handler.shutdown();
}
});
}
exports.registerLanguageHandler = registerLanguageHandler;
//# sourceMappingURL=connection.js.map
;