diff --git a/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx b/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx
index a9cb43db0..3c2fe927a 100644
--- a/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx
+++ b/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx
@@ -5,6 +5,7 @@
import React from 'react';
import { VDomRenderer, VDomModel } from '@jupyterlab/apputils';
+import '../../../../style/statusbar.css';
import {
interactiveItem,
@@ -18,23 +19,187 @@ import { DefaultIconReact } from '@jupyterlab/ui-components';
import { JupyterLabWidgetAdapter } from '../jl_adapter';
import { VirtualDocument } from '../../../virtual/document';
import { LSPConnection } from '../../../connection';
+import { PageConfig } from '@jupyterlab/coreutils';
+
+interface IServerStatusProps {
+ server: IServer;
+}
+
+function ServerStatus(props: IServerStatusProps) {
+ let list = props.server.spec.languages.map(language =>
{language});
+ return (
+
+
{props.server.spec.display_name}
+
+
+ );
+}
+
+export interface IListProps {
+ /**
+ * A title to display.
+ */
+ title: string;
+ list: any[];
+ /**
+ * By default the list will be expanded; to change the initial state to collapsed, set to true.
+ */
+ startCollapsed?: boolean;
+}
+
+export interface ICollapsibleListStates {
+ isCollapsed: boolean;
+}
+
+class CollapsibleList extends React.Component<
+ IListProps,
+ ICollapsibleListStates
+> {
+ constructor(props: any) {
+ super(props);
+ this.state = { isCollapsed: props.startCollapsed || false };
+
+ // This binding is necessary to make `this` work in the callback
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick() {
+ this.setState(state => ({
+ isCollapsed: !state.isCollapsed
+ }));
+ }
+
+ render() {
+ return (
+
+
+
+ {this.props.title} ({this.props.list.length})
+
+
{this.props.list}
+
+ );
+ }
+}
class LSPPopup extends VDomRenderer {
constructor(model: LSPStatus.Model) {
super();
this.model = model;
- // TODO: add proper, custom class?
- this.addClass('p-Menu');
+ this.addClass('lsp-popover');
}
render() {
if (!this.model) {
return null;
}
+ const servers_available = this.model.servers_available_not_in_use.map(
+ (session: IServer) =>
+ );
+
+ let running_servers = new Array();
+ for (let [
+ session,
+ documents_by_language
+ ] of this.model.documents_by_server.entries()) {
+ let documents_html = new Array();
+ for (let [language, documents] of documents_by_language) {
+ // TODO user readable document ids: filename, [cell id]
+ // TODO: stop button
+ // TODO: add a config buttons next to the language header
+ let list = documents.map(document => {
+ let connection = this.model.adapter.connection_manager.connections.get(
+ document.id_path
+ );
+
+ let status = '';
+ if (connection.isInitialized) {
+ status = 'initialized';
+ } else if (connection.isConnected) {
+ status = 'connected';
+ } else {
+ status = 'not connected';
+ }
+
+ return (
+
+ {document.id_path}
+
+ {status}
+
+
+
+ );
+ });
+
+ documents_html.push(
+
+
+ {language}{' '}
+
+ ({session.spec.display_name})
+
+
+
+
+ );
+ }
+
+ running_servers.push({documents_html}
);
+ }
+
+ const missing_languages = this.model.missing_languages.map(language => (
+ {language}
+ ));
return (
-
-
-
-
+
+
+
LSP servers
+
+ {servers_available.length ? (
+
+ ) : (
+ ''
+ )}
+ {running_servers.length ? (
+
+ ) : (
+ ''
+ )}
+ {missing_languages.length ? (
+
+ ) : (
+ ''
+ )}
+
+
+
+
);
}
}
@@ -64,15 +229,14 @@ export class LSPStatus extends VDomRenderer {
return (
-
-
@@ -102,16 +266,25 @@ export interface IStatus {
status: StatusCode;
}
-function collect_languages(virtual_document: VirtualDocument): Set {
- let collected = new Set();
- collected.add(virtual_document.language);
+function collect_documents(
+ virtual_document: VirtualDocument
+): Set {
+ let collected = new Set();
+ collected.add(virtual_document);
for (let foreign of virtual_document.foreign_documents.values()) {
- let foreign_languages = collect_languages(foreign);
+ let foreign_languages = collect_documents(foreign);
foreign_languages.forEach(collected.add, collected);
}
return collected;
}
+function collect_languages(virtual_document: VirtualDocument): Set {
+ let documents = collect_documents(virtual_document);
+ return new Set(
+ [...documents].map(document => document.language.toLocaleLowerCase())
+ );
+}
+
type StatusMap = Record;
const iconByStatus: StatusMap = {
@@ -124,41 +297,133 @@ const iconByStatus: StatusMap = {
const shortMessageByStatus: StatusMap = {
waiting: 'Waiting...',
initialized: 'Fully initialized',
- initializing: 'Fully connected & partially initialized',
+ initializing: 'Partially initialized',
connecting: 'Connecting...'
};
+// TODO ideally, this would be generated from schema
+export interface IServerSpecification {
+ display_name: string;
+ install: object;
+ urls: object;
+ languages: string[];
+}
+
+export interface IServer {
+ spec: IServerSpecification;
+ status: string;
+ handler_count: number;
+ last_server_message_at: string;
+ last_handler_message_at: string;
+}
+
export namespace LSPStatus {
/**
* A VDomModel for the LSP of current file editor/notebook.
*/
export class Model extends VDomModel {
- get lsp_servers(): string {
- if (!this.adapter) {
- return '';
+ server_extension_status: any = null;
+
+ constructor() {
+ super();
+
+ // PathExt.join skips on of the slashes in https://
+ let url = PageConfig.getBaseUrl() + 'lsp';
+ fetch(url)
+ .then(response => {
+ // TODO: retry a few times
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ response
+ .json()
+ .then(data => (this.server_extension_status = data))
+ .catch(console.warn);
+ })
+ .catch(console.error);
+ }
+
+ get available_servers(): Array {
+ return this.server_extension_status.sessions;
+ }
+
+ get supported_languages(): Set {
+ const languages = new Set();
+ for (let server of this.available_servers) {
+ for (let language of server.spec.languages) {
+ languages.add(language.toLocaleLowerCase());
+ }
}
- let document = this.adapter.virtual_editor.virtual_document;
- return `Languages detected: ${[...collect_languages(document)].join(
- ', '
- )}`;
+ return languages;
}
- get lsp_servers_truncated(): string {
+ private is_server_running(server: IServer): boolean {
+ for (let language of server.spec.languages) {
+ if (this.detected_languages.has(language.toLocaleLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ get documents_by_server(): Map> {
+ let data = new Map();
if (!this.adapter) {
- return '';
+ return new Map();
}
- let document = this.adapter.virtual_editor.virtual_document;
- let foreign_languages = collect_languages(document);
- foreign_languages.delete(this.adapter.language);
- if (foreign_languages.size) {
- if (foreign_languages.size < 4) {
- return `${this.adapter.language}, ${[...foreign_languages].join(
- ', '
- )}`;
+
+ let main_document = this.adapter.virtual_editor.virtual_document;
+ let documents = collect_documents(main_document);
+
+ for (let document of documents.values()) {
+ let language = document.language.toLocaleLowerCase();
+ let servers = this.available_servers.filter(
+ server => server.spec.languages.indexOf(language) !== -1
+ );
+ if (servers.length > 1) {
+ console.warn('More than one server per language for' + language);
+ }
+ if (servers.length === 0) {
+ continue;
}
- return `${this.adapter.language} (+${foreign_languages.size} more)`;
+ let server = servers[0];
+
+ if (!data.has(server)) {
+ data.set(server, new Map());
+ }
+
+ let documents_map = data.get(server);
+
+ if (!documents_map.has(language)) {
+ documents_map.set(language, new Array());
+ }
+
+ let documents = documents_map.get(language);
+ documents.push(document);
+ }
+ return data;
+ }
+
+ get servers_available_not_in_use(): Array {
+ return this.available_servers.filter(
+ server => !this.is_server_running(server)
+ );
+ }
+
+ get detected_languages(): Set {
+ if (!this.adapter) {
+ return new Set();
}
- return this.adapter.language;
+
+ let document = this.adapter.virtual_editor.virtual_document;
+ return collect_languages(document);
+ }
+
+ get missing_languages(): Array {
+ // TODO: false negative for r vs R?
+ return [...this.detected_languages].filter(
+ language => !this.supported_languages.has(language.toLocaleLowerCase())
+ );
}
get status(): IStatus {
diff --git a/packages/jupyterlab-lsp/style/statusbar.css b/packages/jupyterlab-lsp/style/statusbar.css
new file mode 100644
index 000000000..4f28de051
--- /dev/null
+++ b/packages/jupyterlab-lsp/style/statusbar.css
@@ -0,0 +1,109 @@
+.lsp-popover {
+ /* jupyterlab Menu */
+ z-index: 10000;
+ padding: 4px 0px;
+ background: var(--jp-layout-color0);
+ color: var(--jp-ui-font-color1);
+ border: var(--jp-border-width) solid var(--jp-border-color1);
+ font-size: var(--jp-ui-font-size1);
+ box-shadow: var(--jp-elevation-z6);
+}
+
+.lsp-popover-content {
+ padding: 5px 10px;
+}
+
+.lsp-servers-title {
+ margin-bottom: 5px;
+}
+
+.lsp-servers-menu {
+ height: 350px;
+ overflow-y: auto;
+}
+
+.lsp-server-status,
+.lsp-missing-server,
+.lsp-documents-by-language {
+ margin-left: 20px;
+}
+
+h5 .lsp-language-server-name {
+ color: var(--jp-ui-font-color2);
+}
+
+.lsp-documents-by-language ul,
+.lsp-server-status ul {
+ margin: 0;
+}
+
+.lsp-servers-menu h5 {
+ font-weight: normal;
+ font-size: 100%;
+}
+
+.lsp-servers-lists {
+ margin-left: 5px;
+}
+
+.lsp-popover-status {
+ padding-top: 5px;
+ border-top: 1px solid var(--jp-border-color1);
+}
+
+.lsp-popover-status a {
+ color: var(--jp-content-link-color);
+}
+
+.lsp-servers-menu h3,
+h4,
+h5 {
+ margin: 0;
+}
+
+.lsp-caret,
+.lsp-document-status-icon {
+ height: 16px;
+ width: 16px;
+ background-size: 18px;
+ background-image: 18px;
+ background-repeat: no-repeat;
+ background-position: center;
+ display: inline-block;
+ position: relative;
+ top: 3px;
+}
+
+.lsp-documents-by-language li {
+ clear: both;
+ width: 100%;
+}
+
+.lsp-document-status {
+ float: right;
+ padding-left: 15px;
+}
+
+.lsp-document-status-icon {
+ height: 13px;
+ width: 13px;
+ background-size: 15px;
+ background-image: 15px;
+ margin-left: 10px;
+}
+
+.lsp-collapsible-list h4 {
+ cursor: pointer;
+}
+
+.lsp-collapsible-list .lsp-caret {
+ background-image: var(--jp-icon-caretdown);
+}
+
+.lsp-collapsible-list.lsp-collapsed .lsp-caret {
+ background-image: var(--jp-icon-caretright);
+}
+
+.lsp-collapsible-list.lsp-collapsed div {
+ display: none;
+}