@jupyterlab/lsp
Version:
450 lines • 18.8 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { Signal } from '@lumino/signaling';
import { LSPConnection } from './connection';
import { expandDottedPaths, sleep, untilReady } from './utils';
/**
* Each Widget with a document (whether file or a notebook) has the same DocumentConnectionManager
* (see JupyterLabWidgetAdapter). Using id_path instead of uri led to documents being overwritten
* as two identical id_paths could be created for two different notebooks.
*/
export class DocumentConnectionManager {
constructor(options) {
/**
* Fired the first time a connection is opened. These _should_ be the only
* invocation of `.on` (once remaining LSPFeature.connection_handlers are made
* singletons).
*/
this.onNewConnection = (connection) => {
const errorSignalSlot = (_, e) => {
console.error(e);
let error = e.length && e.length >= 1 ? e[0] : new Error();
if (error.message.indexOf('code = 1005') !== -1) {
console.error(`Connection failed for ${connection}`);
this._forEachDocumentOfConnection(connection, virtualDocument => {
console.error('disconnecting ' + virtualDocument.uri);
this._closed.emit({ connection, virtualDocument });
this._ignoredLanguages.add(virtualDocument.language);
console.error(`Cancelling further attempts to connect ${virtualDocument.uri} and other documents for this language (no support from the server)`);
});
}
else if (error.message.indexOf('code = 1006') !== -1) {
console.error('Connection closed by the server');
}
else {
console.error('Connection error:', e);
}
};
connection.errorSignal.connect(errorSignalSlot);
const serverInitializedSlot = () => {
// Initialize using settings stored in the SettingRegistry
this._forEachDocumentOfConnection(connection, virtualDocument => {
// TODO: is this still necessary, e.g. for status bar to update responsively?
this._initialized.emit({ connection, virtualDocument });
});
this.updateServerConfigurations(this.initialConfigurations);
};
connection.serverInitialized.connect(serverInitializedSlot);
const closeSignalSlot = (_, closedManually) => {
if (!closedManually) {
console.error('Connection unexpectedly disconnected');
}
else {
console.log('Connection closed');
this._forEachDocumentOfConnection(connection, virtualDocument => {
this._closed.emit({ connection, virtualDocument });
});
}
};
connection.closeSignal.connect(closeSignalSlot);
};
this._initialized = new Signal(this);
this._connected = new Signal(this);
this._disconnected = new Signal(this);
this._closed = new Signal(this);
this._documentsChanged = new Signal(this);
this.connections = new Map();
this.documents = new Map();
this.adapters = new Map();
this._ignoredLanguages = new Set();
this.languageServerManager = options.languageServerManager;
Private.setLanguageServerManager(options.languageServerManager);
options.adapterTracker.adapterAdded.connect((_, adapter) => {
const path = adapter.widget.context.path;
this.registerAdapter(path, adapter);
});
}
/**
* Signal emitted when the manager is initialized.
*/
get initialized() {
return this._initialized;
}
/**
* Signal emitted when the manager is connected to the server
*/
get connected() {
return this._connected;
}
/**
* Connection temporarily lost or could not be fully established; a re-connection will be attempted;
*/
get disconnected() {
return this._disconnected;
}
/**
* Connection was closed permanently and no-reconnection will be attempted, e.g.:
* - there was a serious server error
* - user closed the connection,
* - re-connection attempts exceeded,
*/
get closed() {
return this._closed;
}
/**
* Signal emitted when the document is changed.
*/
get documentsChanged() {
return this._documentsChanged;
}
/**
* Promise resolved when the language server manager is ready.
*/
get ready() {
return Private.getLanguageServerManager().ready;
}
/**
* Helper to connect various virtual document signal with callbacks of
* this class.
*
* @param virtualDocument - virtual document to be connected.
*/
connectDocumentSignals(virtualDocument) {
virtualDocument.foreignDocumentOpened.connect(this.onForeignDocumentOpened, this);
virtualDocument.foreignDocumentClosed.connect(this.onForeignDocumentClosed, this);
this.documents.set(virtualDocument.uri, virtualDocument);
this._documentsChanged.emit(this.documents);
}
/**
* Helper to disconnect various virtual document signal with callbacks of
* this class.
*
* @param virtualDocument - virtual document to be disconnected.
*/
disconnectDocumentSignals(virtualDocument, emit = true) {
virtualDocument.foreignDocumentOpened.disconnect(this.onForeignDocumentOpened, this);
virtualDocument.foreignDocumentClosed.disconnect(this.onForeignDocumentClosed, this);
this.documents.delete(virtualDocument.uri);
for (const foreign of virtualDocument.foreignDocuments.values()) {
this.disconnectDocumentSignals(foreign, false);
}
if (emit) {
this._documentsChanged.emit(this.documents);
}
}
/**
* Handle foreign document opened event.
*/
onForeignDocumentOpened(_host, context) {
/** no-op */
}
/**
* Handle foreign document closed event.
*/
onForeignDocumentClosed(_host, context) {
const { foreignDocument } = context;
this.unregisterDocument(foreignDocument.uri, false);
this.disconnectDocumentSignals(foreignDocument);
}
/**
* @deprecated
*
* Register a widget adapter with this manager
*
* @param path - path to the inner document of the adapter
* @param adapter - the adapter to be registered
*/
registerAdapter(path, adapter) {
this.adapters.set(path, adapter);
adapter.widget.context.pathChanged.connect((context, newPath) => {
this.adapters.delete(path);
this.adapters.set(newPath, adapter);
});
adapter.disposed.connect(() => {
if (adapter.virtualDocument) {
this.documents.delete(adapter.virtualDocument.uri);
}
this.adapters.delete(path);
});
}
/**
* Handles the settings that do not require an existing connection
* with a language server (or can influence to which server the
* connection will be created, e.g. `rank`).
*
* This function should be called **before** initialization of servers.
*/
updateConfiguration(allServerSettings) {
this.languageServerManager.setConfiguration(allServerSettings);
}
/**
* Handles the settings that the language servers accept using
* `onDidChangeConfiguration` messages, which should be passed under
* the "serverSettings" keyword in the setting registry.
* Other configuration options are handled by `updateConfiguration` instead.
*
* This function should be called **after** initialization of servers.
*/
updateServerConfigurations(allServerSettings) {
let languageServerId;
for (languageServerId in allServerSettings) {
if (!allServerSettings.hasOwnProperty(languageServerId)) {
continue;
}
const rawSettings = allServerSettings[languageServerId];
const parsedSettings = expandDottedPaths(rawSettings.configuration || {});
const serverSettings = {
settings: parsedSettings
};
Private.updateServerConfiguration(languageServerId, serverSettings);
}
}
/**
* Retry to connect to the server each `reconnectDelay` seconds
* and for `retrialsLeft` times.
* TODO: presently no longer referenced. A failing connection would close
* the socket, triggering the language server on the other end to exit.
*/
async retryToConnect(options, reconnectDelay, retrialsLeft = -1) {
let { virtualDocument } = options;
if (this._ignoredLanguages.has(virtualDocument.language)) {
return;
}
let interval = reconnectDelay * 1000;
let success = false;
while (retrialsLeft !== 0 && !success) {
await this.connect(options)
.then(() => {
success = true;
})
.catch(e => {
console.warn(e);
});
console.log('will attempt to re-connect in ' + interval / 1000 + ' seconds');
await sleep(interval);
// gradually increase the time delay, up to 5 sec
interval = interval < 5 * 1000 ? interval + 500 : interval;
}
}
/**
* Disconnect the connection to the language server of the requested
* language.
*/
disconnect(languageId) {
Private.disconnect(languageId);
}
/**
* Create a new connection to the language server
* @return A promise of the LSP connection
*/
async connect(options, firstTimeoutSeconds = 30, secondTimeoutMinutes = 5) {
let connection = await this._connectSocket(options);
let { virtualDocument } = options;
if (!connection) {
return;
}
if (!connection.isReady) {
try {
// user feedback hinted that 40 seconds was too short and some users are willing to wait more;
// to make the best of both worlds we first check frequently (6.6 times a second) for the first
// 30 seconds, and show the warning early in case if something is wrong; we then continue retrying
// for another 5 minutes, but only once per second.
await untilReady(() => connection.isReady, Math.round((firstTimeoutSeconds * 1000) / 150), 150);
}
catch (_a) {
console.log(`Connection to ${virtualDocument.uri} timed out after ${firstTimeoutSeconds} seconds, will continue retrying for another ${secondTimeoutMinutes} minutes`);
try {
await untilReady(() => connection.isReady, 60 * secondTimeoutMinutes, 1000);
}
catch (_b) {
console.log(`Connection to ${virtualDocument.uri} timed out again after ${secondTimeoutMinutes} minutes, giving up`);
return;
}
}
}
this._connected.emit({ connection, virtualDocument });
return connection;
}
/**
* Disconnect the signals of requested virtual document URI.
*/
unregisterDocument(uri, emit = true) {
const connection = this.connections.get(uri);
if (connection) {
this.connections.delete(uri);
const allConnection = new Set(this.connections.values());
if (!allConnection.has(connection)) {
this.disconnect(connection.serverIdentifier);
connection.dispose();
}
if (emit) {
this._documentsChanged.emit(this.documents);
}
}
}
/**
* Enable or disable the logging of language server communication.
*/
updateLogging(logAllCommunication, setTrace) {
for (const connection of this.connections.values()) {
connection.logAllCommunication = logAllCommunication;
if (setTrace !== null) {
connection.clientNotifications['$/setTrace'].emit({ value: setTrace });
}
}
}
/**
* Create the LSP connection for requested virtual document.
*
* @return Return the promise of the LSP connection.
*/
async _connectSocket(options) {
let { language, capabilities, virtualDocument } = options;
this.connectDocumentSignals(virtualDocument);
const uris = DocumentConnectionManager.solveUris(virtualDocument, language);
const matchingServers = this.languageServerManager.getMatchingServers({
language
});
// for now use only the server with the highest rank.
const languageServerId = matchingServers.length === 0 ? null : matchingServers[0];
// lazily load 1) the underlying library (1.5mb) and/or 2) a live WebSocket-
// like connection: either already connected or potentially in the process
// of connecting.
if (!uris) {
return;
}
const connection = await Private.connection(language, languageServerId, uris, this.onNewConnection, capabilities);
// if connecting for the first time, all documents subsequent documents will
// be re-opened and synced
this.connections.set(virtualDocument.uri, connection);
return connection;
}
/**
* Helper to apply callback on all documents of a connection.
*/
_forEachDocumentOfConnection(connection, callback) {
for (const [virtualDocumentUri, currentConnection] of this.connections.entries()) {
if (connection !== currentConnection) {
continue;
}
callback(this.documents.get(virtualDocumentUri));
}
}
}
(function (DocumentConnectionManager) {
/**
* Generate the URI of a virtual document from input
*
* @param virtualDocument - the virtual document
* @param language - language of the document
*/
function solveUris(virtualDocument, language) {
var _a;
const serverManager = Private.getLanguageServerManager();
const wsBase = serverManager.settings.wsUrl;
const rootUri = PageConfig.getOption('rootUri');
const virtualDocumentsUri = PageConfig.getOption('virtualDocumentsUri');
// for now take the best match only
const serverOptions = {
language
};
const matchingServers = serverManager.getMatchingServers(serverOptions);
const languageServerId = matchingServers.length === 0 ? null : matchingServers[0];
if (languageServerId === null) {
return;
}
const specs = serverManager.getMatchingSpecs(serverOptions);
const spec = specs.get(languageServerId);
if (!spec) {
console.warn(`Specification not available for server ${languageServerId}`);
}
const requiresOnDiskFiles = (_a = spec === null || spec === void 0 ? void 0 : spec.requires_documents_on_disk) !== null && _a !== void 0 ? _a : true;
const supportsInMemoryFiles = !requiresOnDiskFiles;
const baseUri = virtualDocument.hasLspSupportedFile || supportsInMemoryFiles
? rootUri
: virtualDocumentsUri;
// workaround url-parse bug(s) (see https://github.com/jupyter-lsp/jupyterlab-lsp/issues/595)
let documentUri = URLExt.join(baseUri, virtualDocument.uri);
if (!documentUri.startsWith('file:///') &&
documentUri.startsWith('file://')) {
documentUri = documentUri.replace('file://', 'file:///');
if (documentUri.startsWith('file:///users/') &&
baseUri.startsWith('file:///Users/')) {
documentUri = documentUri.replace('file:///users/', 'file:///Users/');
}
}
return {
base: baseUri,
document: documentUri,
server: URLExt.join('ws://jupyter-lsp', language),
socket: URLExt.join(wsBase, 'lsp', 'ws', languageServerId)
};
}
DocumentConnectionManager.solveUris = solveUris;
})(DocumentConnectionManager || (DocumentConnectionManager = {}));
/**
* Namespace primarily for language-keyed cache of LSPConnections
*/
var Private;
(function (Private) {
const _connections = new Map();
let _languageServerManager;
function getLanguageServerManager() {
return _languageServerManager;
}
Private.getLanguageServerManager = getLanguageServerManager;
function setLanguageServerManager(languageServerManager) {
_languageServerManager = languageServerManager;
}
Private.setLanguageServerManager = setLanguageServerManager;
function disconnect(languageServerId) {
const connection = _connections.get(languageServerId);
if (connection) {
connection.close();
_connections.delete(languageServerId);
}
}
Private.disconnect = disconnect;
/**
* Return (or create and initialize) the WebSocket associated with the language
*/
async function connection(language, languageServerId, uris, onCreate, capabilities) {
let connection = _connections.get(languageServerId);
if (!connection) {
const { settings } = Private.getLanguageServerManager();
const socket = new settings.WebSocket(uris.socket);
const connection = new LSPConnection({
languageId: language,
serverUri: uris.server,
rootUri: uris.base,
serverIdentifier: languageServerId,
capabilities: capabilities
});
_connections.set(languageServerId, connection);
connection.connect(socket);
onCreate(connection);
}
connection = _connections.get(languageServerId);
return connection;
}
Private.connection = connection;
function updateServerConfiguration(languageServerId, settings) {
const connection = _connections.get(languageServerId);
if (connection) {
connection.sendConfigurationChange(settings);
}
}
Private.updateServerConfiguration = updateServerConfiguration;
})(Private || (Private = {}));
//# sourceMappingURL=connection_manager.js.map