@jupyterlab/lsp
Version:
376 lines • 13 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Signal } from '@lumino/signaling';
import { Method } from './tokens';
import { untilReady } from './utils';
import { registerServerCapability, unregisterServerCapability } from './ws-connection/server-capability-registration';
import { LspWsConnection } from './ws-connection/ws-connection';
/**
* Helper class to handle client request
*/
class ClientRequestHandler {
constructor(connection, method, emitter) {
this.connection = connection;
this.method = method;
this.emitter = emitter;
}
request(params) {
// TODO check if is ready?
this.emitter.log(MessageKind.clientRequested, {
method: this.method,
message: params
});
return this.connection
.sendRequest(this.method, params)
.then((result) => {
this.emitter.log(MessageKind.resultForClient, {
method: this.method,
message: params
});
return result;
});
}
}
/**
* Helper class to handle server responses
*/
class ServerRequestHandler {
constructor(connection, method, emitter) {
this.connection = connection;
this.method = method;
this.emitter = emitter;
// on request accepts "thenable"
this.connection.onRequest(method, this._handle.bind(this));
this._handler = null;
}
setHandler(handler) {
this._handler = handler;
}
clearHandler() {
this._handler = null;
}
_handle(request) {
this.emitter.log(MessageKind.serverRequested, {
method: this.method,
message: request
});
if (!this._handler) {
return new Promise(() => undefined);
}
return this._handler(request, this.emitter).then(result => {
this.emitter.log(MessageKind.responseForServer, {
method: this.method,
message: result
});
return result;
});
}
}
export const Provider = {
TEXT_DOCUMENT_SYNC: 'textDocumentSync',
COMPLETION: 'completionProvider',
HOVER: 'hoverProvider',
SIGNATURE_HELP: 'signatureHelpProvider',
DECLARATION: 'declarationProvider',
DEFINITION: 'definitionProvider',
TYPE_DEFINITION: 'typeDefinitionProvider',
IMPLEMENTATION: 'implementationProvider',
REFERENCES: 'referencesProvider',
DOCUMENT_HIGHLIGHT: 'documentHighlightProvider',
DOCUMENT_SYMBOL: 'documentSymbolProvider',
CODE_ACTION: 'codeActionProvider',
CODE_LENS: 'codeLensProvider',
DOCUMENT_LINK: 'documentLinkProvider',
COLOR: 'colorProvider',
DOCUMENT_FORMATTING: 'documentFormattingProvider',
DOCUMENT_RANGE_FORMATTING: 'documentRangeFormattingProvider',
DOCUMENT_ON_TYPE_FORMATTING: 'documentOnTypeFormattingProvider',
RENAME: 'renameProvider',
FOLDING_RANGE: 'foldingRangeProvider',
EXECUTE_COMMAND: 'executeCommandProvider',
SELECTION_RANGE: 'selectionRangeProvider',
WORKSPACE_SYMBOL: 'workspaceSymbolProvider',
WORKSPACE: 'workspace'
};
/**
* Create a map between the request method and its handler
*/
function createMethodMap(methods, handlerFactory) {
const result = {};
for (let method of Object.values(methods)) {
result[method] = handlerFactory(method);
}
return result;
}
var MessageKind;
(function (MessageKind) {
MessageKind[MessageKind["clientNotifiedServer"] = 0] = "clientNotifiedServer";
MessageKind[MessageKind["serverNotifiedClient"] = 1] = "serverNotifiedClient";
MessageKind[MessageKind["serverRequested"] = 2] = "serverRequested";
MessageKind[MessageKind["clientRequested"] = 3] = "clientRequested";
MessageKind[MessageKind["resultForClient"] = 4] = "resultForClient";
MessageKind[MessageKind["responseForServer"] = 5] = "responseForServer";
})(MessageKind || (MessageKind = {}));
export class LSPConnection extends LspWsConnection {
constructor(options) {
super(options);
/**
* Is the connection is closed manually?
*/
this._closingManually = false;
this._closeSignal = new Signal(this);
this._errorSignal = new Signal(this);
this._serverInitialized = new Signal(this);
this._options = options;
this.logAllCommunication = false;
this.serverIdentifier = options.serverIdentifier;
this.serverLanguage = options.languageId;
this.documentsToOpen = [];
this.clientNotifications =
this.constructNotificationHandlers(Method.ClientNotification);
this.serverNotifications =
this.constructNotificationHandlers(Method.ServerNotification);
}
/**
* Signal emitted when the connection is closed.
*/
get closeSignal() {
return this._closeSignal;
}
/**
* Signal emitted when the connection receives an error
* message.
*/
get errorSignal() {
return this._errorSignal;
}
/**
* Signal emitted when the connection is initialized.
*/
get serverInitialized() {
return this._serverInitialized;
}
/**
* Dispose the connection.
*/
dispose() {
if (this.isDisposed) {
return;
}
if (this.serverRequests) {
// `serverRequests` may be undefined if dispose is called during initialization sequence
Object.values(this.serverRequests).forEach(request => request.clearHandler());
}
this.close();
super.dispose();
}
/**
* Helper to print the logs to logger, for now we are using
* directly the browser's console.
*/
log(kind, message) {
if (this.logAllCommunication) {
console.log(kind, message);
}
}
/**
* Send the open request to the backend when the server is
* ready.
*/
sendOpenWhenReady(documentInfo) {
if (this.isReady) {
this.sendOpen(documentInfo);
}
else {
this.documentsToOpen.push(documentInfo);
}
}
/**
* Send the document changes to the server.
*/
sendSelectiveChange(changeEvent, documentInfo) {
this._sendChange([changeEvent], documentInfo);
}
/**
* Send all changes to the server.
*/
sendFullTextChange(text, documentInfo) {
this._sendChange([{ text }], documentInfo);
}
/**
* Check if a capability is available in the server capabilities.
*/
provides(capability) {
return !!(this.serverCapabilities && this.serverCapabilities[capability]);
}
/**
* Close the connection to the server.
*/
close() {
try {
this._closingManually = true;
super.close();
}
catch (e) {
this._closingManually = false;
}
}
/**
* Initialize a connection over a web socket that speaks the LSP.
*/
connect(socket) {
super.connect(socket);
untilReady(() => {
return this.isConnected;
}, -1)
.then(() => {
const disposable = this.connection.onClose(() => {
this._isConnected = false;
this._closeSignal.emit(this._closingManually);
});
this._disposables.push(disposable);
})
.catch(() => {
console.error('Could not connect onClose signal');
});
}
/**
* Get send request to the server to get completion results
* from a completion item
*/
async getCompletionResolve(completionItem) {
if (!this.isReady) {
return;
}
return this.connection.sendRequest('completionItem/resolve', completionItem);
}
/**
* Generate the notification handlers
*/
constructNotificationHandlers(methods) {
const factory = () => new Signal(this);
return createMethodMap(methods, factory);
}
/**
* Generate the client request handler
*/
constructClientRequestHandler(methods) {
return createMethodMap(methods, method => new ClientRequestHandler(this.connection, method, this));
}
/**
* Generate the server response handler
*/
constructServerRequestHandler(methods) {
return createMethodMap(methods, method => new ServerRequestHandler(this.connection, method, this));
}
/**
* Initialization parameters to be sent to the language server.
* Subclasses can overload this when adding more features.
*/
initializeParams() {
return {
...super.initializeParams(),
capabilities: this._options.capabilities,
initializationOptions: null,
processId: null,
workspaceFolders: null
};
}
/**
* Callback called when the server is initialized.
*/
onServerInitialized(params) {
this.afterInitialized();
super.onServerInitialized(params);
while (this.documentsToOpen.length) {
this.sendOpen(this.documentsToOpen.pop());
}
this._serverInitialized.emit(this.serverCapabilities);
}
/**
* Once the server is initialized, this method generates the
* client and server handlers
*/
afterInitialized() {
const disposable = this.connection.onError(e => this._errorSignal.emit(e));
this._disposables.push(disposable);
for (const method of Object.values(Method.ServerNotification)) {
const signal = this.serverNotifications[method];
const disposable = this.connection.onNotification(method, params => {
this.log(MessageKind.serverNotifiedClient, {
method,
message: params
});
signal.emit(params);
});
this._disposables.push(disposable);
}
for (const method of Object.values(Method.ClientNotification)) {
const signal = this.clientNotifications[method];
signal.connect((emitter, params) => {
this.log(MessageKind.clientNotifiedServer, {
method,
message: params
});
this.connection.sendNotification(method, params).catch(console.error);
});
}
this.clientRequests = this.constructClientRequestHandler(Method.ClientRequest);
this.serverRequests = this.constructServerRequestHandler(Method.ServerRequest);
this.serverRequests['client/registerCapability'].setHandler(async (params) => {
params.registrations.forEach((capabilityRegistration) => {
try {
const updatedCapabilities = registerServerCapability(this.serverCapabilities, capabilityRegistration);
if (updatedCapabilities === null) {
console.error(`Failed to register server capability: ${capabilityRegistration}`);
return;
}
this.serverCapabilities = updatedCapabilities;
}
catch (err) {
console.error(err);
}
});
});
this.serverRequests['client/unregisterCapability'].setHandler(async (params) => {
params.unregisterations.forEach((capabilityUnregistration) => {
this.serverCapabilities = unregisterServerCapability(this.serverCapabilities, capabilityUnregistration);
});
});
this.serverRequests['workspace/configuration'].setHandler(async (params) => {
return params.items.map(item => {
// LSP: "If the client can’t provide a configuration setting for a given scope
// then `null` needs to be present in the returned array."
// for now we do not support configuration, but yaml server does not respect
// client capability so we have a handler just for that
return null;
});
});
}
/**
* Send the document changed data to the server.
*/
_sendChange(changeEvents, documentInfo) {
if (!this.isReady) {
return;
}
if (documentInfo.uri.length === 0) {
return;
}
if (!this.openedUris.get(documentInfo.uri)) {
this.sendOpen(documentInfo);
}
const textDocumentChange = {
textDocument: {
uri: documentInfo.uri,
version: documentInfo.version
},
contentChanges: changeEvents
};
this.connection
.sendNotification('textDocument/didChange', textDocumentChange)
.catch(console.error);
documentInfo.version++;
}
}
//# sourceMappingURL=connection.js.map