UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

495 lines 24.3 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. // Based on the @jupyterlab/codemirror-extension statusbar import { VDomModel, VDomRenderer, Dialog, showDialog } from '@jupyterlab/apputils'; import { collectDocuments } from '@jupyterlab/lsp'; import { GroupItem, TextItem, showPopup } from '@jupyterlab/statusbar'; import { caretDownIcon, caretUpIcon, circleEmptyIcon, circleIcon, stopIcon } from '@jupyterlab/ui-components'; import React from 'react'; import '../../style/statusbar.css'; import { SERVER_EXTENSION_404 } from '../errors'; import { codeCheckIcon, codeClockIcon, codeWarningIcon } from './icons'; import { DocumentLocator, ServerLinksList } from './utils'; var okButton = Dialog.okButton; function ServerStatus(props) { let list = props.server.spec.languages.map((language, i) => (React.createElement("li", { key: i }, language))); return (React.createElement("div", { className: 'lsp-server-status' }, React.createElement("h5", null, props.server.spec.display_name), React.createElement("ul", null, list))); } class CollapsibleList extends React.Component { constructor(props) { super(props); this.handleClick = () => { this.setState(state => ({ isCollapsed: !state.isCollapsed })); }; this.state = { isCollapsed: props.startCollapsed || false }; } render() { const collapseExpandIcon = !this.state.isCollapsed ? caretUpIcon : caretDownIcon; return (React.createElement("div", { className: 'lsp-collapsible-list ' + (this.state.isCollapsed ? 'lsp-collapsed' : '') }, React.createElement("h4", { onClick: this.handleClick }, React.createElement(collapseExpandIcon.react, { tag: "span", className: "lsp-caret-icon" }), this.props.title, ": ", this.props.list.length), React.createElement("div", null, this.props.list))); } } class LanguageServerInfo extends React.Component { render() { const specification = this.props.specs; const trans = this.props.trans; return (React.createElement("div", null, React.createElement("h3", null, specification.display_name), React.createElement("div", null, React.createElement(ServerLinksList, { specification: specification }), React.createElement("h4", null, trans.__('Troubleshooting')), React.createElement("p", { className: 'lsp-troubleshoot-section' }, specification.troubleshoot ? specification.troubleshoot : trans.__('In case of issues with installation feel welcome to ask a question on GitHub.')), React.createElement("h4", null, trans.__('Installation')), React.createElement("ul", null, (specification === null || specification === void 0 ? void 0 : specification.install) ? Object.entries((specification === null || specification === void 0 ? void 0 : specification.install) || {}).map(([name, command]) => (React.createElement("li", { key: this.props.serverId + '-install-' + name }, name, ": ", React.createElement("code", null, command)))) : trans.__('No installation instructions were provided with this specification.'))))); } } class HelpButton extends React.Component { constructor() { super(...arguments); this.handleClick = () => { const trans = this.props.trans; showDialog({ title: trans.__('No language server for %1 detected', this.props.language), body: (React.createElement("div", null, this.props.servers.size ? (React.createElement("div", null, React.createElement("p", null, trans._n('There is %1 language server you can easily install that supports %2.', 'There are %1 language servers you can easily install that supports %2.', this.props.servers.size, this.props.language)), [...this.props.servers.entries()].map(([key, specification]) => (React.createElement(LanguageServerInfo, { specs: specification, serverId: key, key: key, trans: trans }))))) : (React.createElement("div", null, React.createElement("p", null, trans.__('We do not have an auto-detection ready for a language servers supporting %1 yet.', this.props.language)), React.createElement("p", null, trans.__('You may contribute a specification for auto-detection as described in our '), ' ', React.createElement("a", { href: 'https://jupyterlab-lsp.readthedocs.io/en/latest/Contributing.html#specs' }, trans.__('documentation'))))))), buttons: [okButton()] }).catch(console.warn); }; } render() { return (React.createElement("button", { type: 'button', className: 'jp-Button lsp-help-button', onClick: this.handleClick }, "?")); } } class LSPPopup extends VDomRenderer { constructor(model) { super(model); this.addClass('lsp-popover'); } render() { var _a; if (!((_a = this.model) === null || _a === void 0 ? void 0 : _a.connectionManager)) { return null; } const serversAvailable = this.model.serversAvailableNotInUse.map((session, i) => React.createElement(ServerStatus, { key: i, server: session })); let runningServers = new Array(); let key = -1; for (let [session, documentsByLanguage] of this.model.documentsByServer.entries()) { key += 1; let documentsHtml = new Array(); for (let [language, documents] of documentsByLanguage) { // TODO: stop button // TODO: add a config buttons next to the language header let list = documents.map((document, i) => { let connection = this.model.connectionManager.connections.get(document.uri); let status = ''; if (connection === null || connection === void 0 ? void 0 : connection.isInitialized) { status = 'initialized'; } else if (connection === null || connection === void 0 ? void 0 : connection.isConnected) { status = 'connected'; } else { status = 'not connected'; } const icon = status === 'initialized' ? circleIcon : circleEmptyIcon; return (React.createElement("li", { key: i }, React.createElement(DocumentLocator, { document: document, adapter: this.model.adapter }), React.createElement("span", { className: 'lsp-document-status' }, this.model.trans.__(status), React.createElement(icon.react, { tag: "span", className: "lsp-document-status-icon", elementSize: 'small' })))); }); documentsHtml.push(React.createElement("div", { key: key, className: 'lsp-documents-by-language' }, React.createElement("h5", null, language, ' ', React.createElement("span", { className: 'lsp-language-server-name' }, "(", session.spec.display_name, ")")), React.createElement("ul", null, list))); } runningServers.push(React.createElement("div", { key: key }, documentsHtml)); } const missingLanguages = this.model.missingLanguages.map((language, i) => { const specsForMissing = this.model.languageServerManager.getMatchingSpecs({ language }); return (React.createElement("div", { key: i, className: 'lsp-missing-server' }, language, specsForMissing.size ? (React.createElement(HelpButton, { language: language, servers: specsForMissing, trans: this.model.trans })) : (''))); }); const trans = this.model.trans; return (React.createElement("div", { className: 'lsp-popover-content' }, React.createElement("div", { className: 'lsp-servers-menu' }, React.createElement("h3", { className: 'lsp-servers-title' }, trans.__('LSP servers')), React.createElement("div", { className: 'lsp-servers-lists' }, serversAvailable.length ? (React.createElement(CollapsibleList, { key: 'available', title: trans.__('Available'), list: serversAvailable, startCollapsed: true })) : (''), runningServers.length ? (React.createElement(CollapsibleList, { key: 'running', title: trans.__('Running'), list: runningServers })) : (''), missingLanguages.length ? (React.createElement(CollapsibleList, { key: 'missing', title: trans.__('Missing'), list: missingLanguages })) : (''))), React.createElement("div", { className: 'lsp-popover-status' }, trans.__('Documentation:'), ' ', React.createElement("a", { href: 'https://jupyterlab-lsp.readthedocs.io/en/latest/Language%20Servers.html', target: "_blank", rel: "noreferrer" }, trans.__('Language Servers'))))); } } /** * StatusBar item. */ export class LSPStatus extends VDomRenderer { /** * Construct a new VDomRenderer for the status item. */ constructor(displayText = true, shell, trans) { super(new LSPStatus.Model(shell, trans)); this.displayText = displayText; this._popup = null; this.handleClick = () => { if (this._popup) { this._popup.dispose(); } if (this.model.status.status == 'noServerExtension') { showDialog({ title: this.trans.__('LSP server extension not found'), body: SERVER_EXTENSION_404, buttons: [okButton()] }).catch(console.warn); } else { this._popup = showPopup({ body: new LSPPopup(this.model), anchor: this, align: 'left', hasDynamicSize: true }); } }; this.addClass('jp-mod-highlighted'); this.addClass('lsp-statusbar-item'); this.trans = trans; this.title.caption = this.trans.__('LSP status'); } /** * Render the status item. */ render() { const { model } = this; if (model == null) { return null; } return (React.createElement(GroupItem, { spacing: this.displayText ? 2 : 0, title: model.longMessage, onClick: this.handleClick, className: 'lsp-status-group' }, React.createElement(model.statusIcon.react, { top: '2px', stylesheet: 'statusBar', title: this.trans.__('LSP Code Intelligence') }), this.displayText ? (React.createElement(TextItem, { className: 'lsp-status-message', source: model.shortMessage })) : (React.createElement(React.Fragment, null)))); } } export class StatusButtonExtension { constructor(options) { this.options = options; } /** * For statusbar registration and for internal use. */ createItem(displayText = true) { const statusBarItem = new LSPStatus(displayText, this.options.shell, this.options.translatorBundle); statusBarItem.model.languageServerManager = this.options.languageServerManager; statusBarItem.model.connectionManager = this.options.connectionManager; return statusBarItem; } /** * For registration on notebook panels. */ createNew(panel, context) { const item = this.createItem(false); item.addClass('jp-ToolbarButton'); panel.toolbar.insertAfter('spacer', 'LSPStatus', item); return item; } } function collectLanguages(virtualDocument) { let documents = collectDocuments(virtualDocument); return new Set([...documents].map(document => document.language.toLocaleLowerCase())); } const classByStatus = { noServerExtension: 'error', waiting: 'inactive', initialized: 'ready', initializing: 'preparing', initializedButSomeMissing: 'ready', connecting: 'preparing' }; const iconByStatus = { noServerExtension: codeWarningIcon, waiting: codeClockIcon, initialized: codeCheckIcon, initializing: codeClockIcon, initializedButSomeMissing: codeWarningIcon, connecting: codeClockIcon }; (function (LSPStatus) { /** * A VDomModel for the LSP of current file editor/notebook. */ class Model extends VDomModel { constructor(_shell, trans) { super(); this._shell = _shell; this._onChange = () => { this.stateChanged.emit(void 0); }; this.trans = trans; this._shortMessageByStatus = { noServerExtension: trans.__('Server extension missing'), waiting: trans.__('Waiting…'), initialized: trans.__('Fully initialized'), initializedButSomeMissing: trans.__('Initialized (additional servers needed)'), initializing: trans.__('Initializing…'), connecting: trans.__('Connecting…') }; } get availableServers() { return this.languageServerManager.sessions; } get supportedLanguages() { const languages = new Set(); for (let server of this.availableServers.values()) { for (let language of server.spec.languages) { languages.add(language.toLocaleLowerCase()); } } return languages; } _isServerRunning(id, server) { for (const language of this.detectedLanguages) { const matchedServers = this.languageServerManager.getMatchingServers({ language }); // TODO server.status === "started" ? // TODO update once multiple servers are allowed if (matchedServers.length && matchedServers[0] === id) { return true; } } return false; } get documentsByServer() { var _a; let data = new Map(); if (!((_a = this.adapter) === null || _a === void 0 ? void 0 : _a.virtualDocument)) { return data; } let mainDocument = this.adapter.virtualDocument; let documents = collectDocuments(mainDocument); for (let document of documents.values()) { let language = document.language.toLocaleLowerCase(); let serverIds = this._connectionManager.languageServerManager.getMatchingServers({ language: document.language }); if (serverIds.length === 0) { continue; } // For now only use the server with the highest priority let server = this.languageServerManager.sessions.get(serverIds[0]); if (!data.has(server)) { data.set(server, new Map()); } let documentsMap = data.get(server); if (!documentsMap.has(language)) { documentsMap.set(language, new Array()); } let documents = documentsMap.get(language); documents.push(document); } return data; } get serversAvailableNotInUse() { return [...this.availableServers.entries()] .filter(([id, server]) => !this._isServerRunning(id, server)) .map(([id, server]) => server); } get detectedLanguages() { var _a; if (!((_a = this.adapter) === null || _a === void 0 ? void 0 : _a.virtualDocument)) { return new Set(); } let document = this.adapter.virtualDocument; return collectLanguages(document); } get missingLanguages() { // TODO: false negative for r vs R? return [...this.detectedLanguages].filter(language => !this.supportedLanguages.has(language.toLocaleLowerCase())); } get status() { var _a; let detectedDocuments; if (!((_a = this.adapter) === null || _a === void 0 ? void 0 : _a.virtualDocument)) { detectedDocuments = new Map(); } else { let mainDocument = this.adapter.virtualDocument; const allDocuments = this._connectionManager.documents; // detected documents that are open in the current virtual editor const detectedDocumentsSet = collectDocuments(mainDocument); detectedDocuments = new Map([...allDocuments].filter(([id, doc]) => detectedDocumentsSet.has(doc))); } let connectedDocuments = new Set(); let initializedDocuments = new Set(); let absentDocuments = new Set(); // detected documents with LSP servers available let documentsWithAvailableServers = new Set(); // detected documents with LSP servers known let documentsWithKnownServers = new Set(); detectedDocuments.forEach((document, uri) => { let connection = this._connectionManager.connections.get(uri); let serverIds = this._connectionManager.languageServerManager.getMatchingServers({ language: document.language }); if (serverIds.length !== 0) { documentsWithKnownServers.add(document); } if (!connection) { absentDocuments.add(document); return; } else { documentsWithAvailableServers.add(document); } if (connection.isConnected) { connectedDocuments.add(document); } if (connection.isInitialized) { initializedDocuments.add(document); } }); // there may be more open connections than documents if a document was recently closed // and the grace period has not passed yet let openConnections = new Array(); this._connectionManager.connections.forEach((connection, path) => { if (connection.isConnected) { openConnections.push(connection); } }); let status; if (this.languageServerManager.statusCode === 404) { status = 'noServerExtension'; } else if (detectedDocuments.size === 0) { status = 'waiting'; } else if (initializedDocuments.size === detectedDocuments.size) { status = 'initialized'; } else if (initializedDocuments.size === documentsWithAvailableServers.size && detectedDocuments.size > documentsWithKnownServers.size) { status = 'initializedButSomeMissing'; } else if (connectedDocuments.size === documentsWithAvailableServers.size) { status = 'initializing'; } else { status = 'connecting'; } return { openConnections, connectedDocuments, initializedDocuments, detectedDocuments: new Set([...detectedDocuments.values()]), status }; } get statusIcon() { if (!this.adapter) { return stopIcon; } return iconByStatus[this.status.status].bindprops({ className: 'lsp-status-icon ' + classByStatus[this.status.status] }); } get shortMessage() { if (!this.adapter) { return this.trans.__('Not initialized'); } return this._shortMessageByStatus[this.status.status]; } get longMessage() { if (!this.adapter) { return this.trans.__('not initialized'); } let status = this.status; let msg = ''; if (status.status === 'waiting') { msg = this.trans.__('Waiting for documents initialization...'); } else if (status.status === 'initialized') { msg = this.trans._n('Fully connected & initialized (%2 virtual document)', 'Fully connected & initialized (%2 virtual document)', status.detectedDocuments.size, status.detectedDocuments.size); } else if (status.status === 'initializing') { const uninitialized = new Set(status.detectedDocuments); for (let initialized of status.initializedDocuments.values()) { uninitialized.delete(initialized); } // servers for n documents did not respond to the initialization request msg = this.trans._np('pluralized', 'Fully connected, but %2/%3 virtual document stuck uninitialized: %4', 'Fully connected, but %2/%3 virtual documents stuck uninitialized: %4', status.detectedDocuments.size, uninitialized.size, status.detectedDocuments.size, [...uninitialized].map(document => document.idPath).join(', ')); } else { const unconnected = new Set(status.detectedDocuments); for (let connected of status.connectedDocuments.values()) { unconnected.delete(connected); } msg = this.trans._np('pluralized', '%2/%3 virtual document connected (%4 connections; waiting for: %5)', '%2/%3 virtual documents connected (%4 connections; waiting for: %5)', status.detectedDocuments.size, status.connectedDocuments.size, status.detectedDocuments.size, status.openConnections.length, [...unconnected].map(document => document.idPath).join(', ')); } return msg; } get adapter() { const adapter = [...this.connectionManager.adapters.values()].find(adapter => adapter.widget == this._shell.currentWidget); return adapter !== null && adapter !== void 0 ? adapter : null; } get connectionManager() { return this._connectionManager; } /** * Note: it is ever only set once, as connectionManager is a singleton. */ set connectionManager(connectionManager) { if (this._connectionManager != null) { this._connectionManager.connected.disconnect(this._onChange); this._connectionManager.initialized.connect(this._onChange); this._connectionManager.disconnected.disconnect(this._onChange); this._connectionManager.closed.disconnect(this._onChange); this._connectionManager.documentsChanged.disconnect(this._onChange); } if (connectionManager != null) { connectionManager.connected.connect(this._onChange); connectionManager.initialized.connect(this._onChange); connectionManager.disconnected.connect(this._onChange); connectionManager.closed.connect(this._onChange); connectionManager.documentsChanged.connect(this._onChange); } this._connectionManager = connectionManager; } } LSPStatus.Model = Model; })(LSPStatus || (LSPStatus = {})); //# sourceMappingURL=statusbar.js.map