@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
495 lines • 24.3 kB
JavaScript
// 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