From 94a92b016a077aba846651eac1d3e4f3b4924d01 Mon Sep 17 00:00:00 2001 From: krassowski Date: Mon, 28 Oct 2019 03:05:00 +0000 Subject: [PATCH 1/3] Add a stub for statusbar; currently the serverInitialized event is not emitted (fixing requires creating a fork for the connection implementation) --- .../jupyterlab/components/statusbar.tsx | 147 ++++++++++++++++++ src/connection_manager.ts | 14 +- src/index.ts | 52 ++++++- 3 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 src/adapters/jupyterlab/components/statusbar.tsx diff --git a/src/adapters/jupyterlab/components/statusbar.tsx b/src/adapters/jupyterlab/components/statusbar.tsx new file mode 100644 index 000000000..a92fd967b --- /dev/null +++ b/src/adapters/jupyterlab/components/statusbar.tsx @@ -0,0 +1,147 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +// Based on the @jupyterlab/codemirror-extension statusbar + +import React from 'react'; + +import { VDomRenderer, VDomModel } from '@jupyterlab/apputils'; + +import { + interactiveItem, + // Popup, + // showPopup, + TextItem +} from '@jupyterlab/statusbar'; + +import { JupyterLabWidgetAdapter } from '../jl_adapter'; + +/** + * StatusBar item. + */ +export class LSPStatus extends VDomRenderer { + /** + * Construct a new VDomRenderer for the status item. + */ + constructor() { + super(); + this.model = new LSPStatus.Model(); + this.addClass(interactiveItem); + this.title.caption = 'LSP status'; + } + + /** + * Render the status item. + */ + render() { + if (!this.model) { + return null; + } + return ; + } + + handleClick() { + console.log('Click;'); + } +} + +export namespace LSPStatus { + /** + * A VDomModel for the LSP of current file editor/notebook. + */ + export class Model extends VDomModel { + get message(): string { + return ( + 'LSP Code Intelligence: ' + + this.status + + (this._message ? this._message : '') + ); + } + + get status(): string { + if (!this.adapter) { + return 'not initialized'; + } else { + let connection_manager = this.adapter.connection_manager; + const documents = connection_manager.documents; + let connected_documents = 0; + let initialized_documents = 0; + + documents.forEach((document, id_path) => { + let connection = connection_manager.connections.get(id_path); + if (!connection) { + return; + } + + // @ts-ignore + if (connection.isConnected) { + connected_documents += 1; + } + // @ts-ignore + if (connection.isInitialized) { + initialized_documents += 1; + } + }); + + // there may be more open connections than documents if a document was recently closed + // and the grace period has not passed yet + let open_connections = 0; + connection_manager.connections.forEach((connection, path) => { + // @ts-ignore + if (connection.isConnected) { + open_connections += 1; + console.warn('Connected:', path); + } else { + console.warn(path); + } + }); + let msg = ''; + const plural = documents.size > 1 ? 's' : ''; + if (documents.size === 0) { + msg = 'Waiting for documents initialization...'; + } else if (initialized_documents === documents.size) { + msg = `Fully connected & initialized (${documents.size} virtual document${plural})`; + } else if (connected_documents === documents.size) { + const uninitialized = documents.size - initialized_documents; + // servers for n documents did not respond ot the initialization request + msg = `Fully connected, but ${uninitialized}/${documents.size} virtual document${plural} stuck uninitialized`; + } else if (open_connections === 0) { + msg = `No open connections (${documents.size} virtual document${plural})`; + } else { + msg = `${connected_documents}/${documents.size} virtual document${plural} connected (${open_connections})`; + } + return `${this.adapter.language} | ${msg}`; + } + } + + get adapter(): JupyterLabWidgetAdapter | null { + return this._adapter; + } + set adapter(adapter: JupyterLabWidgetAdapter | null) { + const oldAdapter = this._adapter; + if (oldAdapter !== null) { + oldAdapter.connection_manager.connected.disconnect(this._onChange); + oldAdapter.connection_manager.initialized.connect(this._onChange); + oldAdapter.connection_manager.disconnected.disconnect(this._onChange); + oldAdapter.connection_manager.closed.disconnect(this._onChange); + oldAdapter.connection_manager.documents_changed.disconnect( + this._onChange + ); + } + + let onChange = this._onChange.bind(this); + adapter.connection_manager.connected.connect(onChange); + adapter.connection_manager.initialized.connect(onChange); + adapter.connection_manager.disconnected.connect(onChange); + adapter.connection_manager.closed.connect(onChange); + adapter.connection_manager.documents_changed.connect(onChange); + this._adapter = adapter; + } + + private _onChange() { + this.stateChanged.emit(void 0); + } + + private _message: string = ''; + private _adapter: JupyterLabWidgetAdapter | null = null; + } +} diff --git a/src/connection_manager.ts b/src/connection_manager.ts index acb680592..b4a2d5918 100644 --- a/src/connection_manager.ts +++ b/src/connection_manager.ts @@ -36,6 +36,7 @@ interface ISocketConnectionOptions { export class DocumentConnectionManager { connections: Map; documents: Map; + initialized: Signal; connected: Signal; /** * Connection temporarily lost or could not be fully established; a re-connection will be attempted; @@ -48,6 +49,10 @@ export class DocumentConnectionManager { * - re-connection attempts exceeded, */ closed: Signal; + documents_changed: Signal< + DocumentConnectionManager, + Map + >; private ignored_languages: Set; constructor() { @@ -55,8 +60,10 @@ export class DocumentConnectionManager { this.documents = new Map(); this.ignored_languages = new Set(); this.connected = new Signal(this); + this.initialized = new Signal(this); this.disconnected = new Signal(this); this.closed = new Signal(this); + this.documents_changed = new Signal(this); } connect_document_signals(virtual_document: VirtualDocument) { @@ -72,9 +79,11 @@ export class DocumentConnectionManager { this.connections.get(foreign_document.id_path).close(); this.connections.delete(foreign_document.id_path); this.documents.delete(foreign_document.id_path); + this.documents_changed.emit(this.documents); } ); this.documents.set(virtual_document.id_path, virtual_document); + this.documents_changed.emit(this.documents); } private connect_socket(options: ISocketConnectionOptions): LSPConnection { @@ -98,7 +107,6 @@ export class DocumentConnectionManager { // NOTE: Update is async now and this is not really used, as an alternative method // which is compatible with async is used. // This should be only used in the initialization step. - // @ts-ignore // if (main_connection.isConnected) { // console.warn('documentText is deprecated for use in JupyterLab LSP'); // } @@ -170,6 +178,10 @@ export class DocumentConnectionManager { async connect(options: ISocketConnectionOptions) { let connection = this.connect_socket(options); + connection.on('serverInitialized', capabilities => { + this.initialized.emit({ connection, virtual_document }); + }); + let { virtual_document, document_path } = options; await until_ready( diff --git a/src/index.ts b/src/index.ts index c95a0130b..ed6508794 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,12 @@ import { + ILabShell, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ICommandPalette } from '@jupyterlab/apputils'; -import { INotebookTracker } from '@jupyterlab/notebook'; +import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; import { CodeMirrorEditor } from '@jupyterlab/codemirror'; -import { IEditorTracker } from '@jupyterlab/fileeditor'; +import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor'; import { ISettingRegistry } from '@jupyterlab/coreutils'; import { IDocumentManager } from '@jupyterlab/docmanager'; @@ -30,6 +31,9 @@ import { NotebookCommandManager } from './command_manager'; import IPaths = JupyterFrontEnd.IPaths; +import { IStatusBar } from '@jupyterlab/statusbar'; +import { LSPStatus } from './adapters/jupyterlab/components/statusbar'; +import { IDocumentWidget } from '@jupyterlab/docregistry/lib/registry'; const lsp_commands: Array = [].concat( ...lsp_features.map(feature => feature.commands) @@ -48,7 +52,9 @@ const plugin: JupyterFrontEndPlugin = { IDocumentManager, ICompletionManager, IRenderMimeRegistry, - IPaths + IPaths, + ILabShell, + IStatusBar ], activate: ( app: JupyterFrontEnd, @@ -59,7 +65,9 @@ const plugin: JupyterFrontEndPlugin = { documentManager: IDocumentManager, completion_manager: ICompletionManager, rendermime_registry: IRenderMimeRegistry, - paths: IPaths + paths: IPaths, + labShell: ILabShell, + status_bar: IStatusBar ) => { // temporary workaround for getting the absolute path let server_root = paths.directories.serverRoot; @@ -82,6 +90,42 @@ const plugin: JupyterFrontEndPlugin = { } } + const status_bar_item = new LSPStatus(); + + labShell.currentChanged.connect(() => { + const current = labShell.currentWidget; + if (!current) { + return; + } + let adapter = null; + if (notebookTracker.has(current)) { + let id = (current as NotebookPanel).id; + console.warn(id); + adapter = notebook_adapters.get(id); + } else if (fileEditorTracker.has(current)) { + let id = (current as IDocumentWidget).content.id; + adapter = file_editor_adapters.get(id); + } + + if (adapter !== null) { + status_bar_item.model.adapter = adapter; + } + }); + + status_bar.registerStatusItem( + '@krassowski/jupyterlab-lsp:language-server-status', + { + item: status_bar_item, + align: 'left', + rank: 1, + isActive: () => + labShell.currentWidget && + (fileEditorTracker.currentWidget || notebookTracker.currentWidget) && + (labShell.currentWidget === fileEditorTracker.currentWidget || + labShell.currentWidget === notebookTracker.currentWidget) + } + ); + fileEditorTracker.widgetUpdated.connect((sender, widget) => { console.log(sender); console.log(widget); From 3898a4418649bed08e0d7607b6c7dd0d69fa749b Mon Sep 17 00:00:00 2001 From: krassowski Date: Mon, 28 Oct 2019 03:09:10 +0000 Subject: [PATCH 2/3] Set jsx --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 1a9d97575..01a8bd074 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "declaration": true, + "jsx": "react", "lib": ["es2015", "dom"], "module": "commonjs", "moduleResolution": "node", From dbf2500d4ceb4b62f723ee513735b865f8f5817b Mon Sep 17 00:00:00 2001 From: krassowski Date: Thu, 31 Oct 2019 11:05:24 +0000 Subject: [PATCH 3/3] Implement more advanced status bar --- .../jupyterlab/components/statusbar.tsx | 283 ++++++++++++++---- .../jupyterlab-lsp/src/virtual/document.ts | 2 +- 2 files changed, 222 insertions(+), 63 deletions(-) diff --git a/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx b/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx index a92fd967b..9a9ffedc8 100644 --- a/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx +++ b/packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx @@ -8,17 +8,42 @@ import { VDomRenderer, VDomModel } from '@jupyterlab/apputils'; import { interactiveItem, - // Popup, - // showPopup, - TextItem + Popup, + showPopup, + TextItem, + GroupItem } from '@jupyterlab/statusbar'; +import { DefaultIconReact } from '@jupyterlab/ui-components'; import { JupyterLabWidgetAdapter } from '../jl_adapter'; +import { VirtualDocument } from '../../../virtual/document'; +import { LSPConnection } from '../../../connection'; + +class LSPPopup extends VDomRenderer { + constructor(model: LSPStatus.Model) { + super(); + this.model = model; + // TODO: add proper, custom class? + this.addClass('p-Menu'); + } + render() { + if (!this.model) { + return null; + } + return ( + + + + + ); + } +} /** * StatusBar item. */ export class LSPStatus extends VDomRenderer { + protected _popup: Popup = null; /** * Construct a new VDomRenderer for the status item. */ @@ -36,12 +61,54 @@ export class LSPStatus extends VDomRenderer { if (!this.model) { return null; } - return ; + return ( + + + + + + + ); } - handleClick() { - console.log('Click;'); + handleClick = () => { + if (this._popup) { + this._popup.dispose(); + } + this._popup = showPopup({ + body: new LSPPopup(this.model), + anchor: this, + align: 'left' + }); + }; +} + +type StatusCode = 'waiting' | 'initializing' | 'initialized' | 'connecting'; + +export interface IStatus { + connected_documents: Set; + initialized_documents: Set; + open_connections: Array; + detected_documents: Set; + status: StatusCode; +} + +function collect_languages(virtual_document: VirtualDocument): Set { + let collected = new Set(); + collected.add(virtual_document.language); + for (let foreign of virtual_document.foreign_documents.values()) { + let foreign_languages = collect_languages(foreign); + foreign_languages.forEach(collected.add, collected); } + return collected; } export namespace LSPStatus { @@ -49,68 +116,161 @@ export namespace LSPStatus { * A VDomModel for the LSP of current file editor/notebook. */ export class Model extends VDomModel { - get message(): string { - return ( - 'LSP Code Intelligence: ' + - this.status + - (this._message ? this._message : '') - ); + get lsp_servers(): string { + if (!this.adapter) { + return ''; + } + let document = this.adapter.virtual_editor.virtual_document; + return `Languages detected: ${[...collect_languages(document)].join( + ', ' + )}`; + } + + get lsp_servers_truncated(): string { + if (!this.adapter) { + return ''; + } + 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( + ', ' + )}`; + } + return `${this.adapter.language} (+${foreign_languages.size} more)`; + } + return this.adapter.language; + } + + get status(): IStatus { + let connection_manager = this.adapter.connection_manager; + const detected_documents = connection_manager.documents; + let connected_documents = new Set(); + let initialized_documents = new Set(); + + detected_documents.forEach((document, id_path) => { + let connection = connection_manager.connections.get(id_path); + if (!connection) { + return; + } + + if (connection.isConnected) { + connected_documents.add(document); + } + if (connection.isInitialized) { + initialized_documents.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 open_connections = new Array(); + connection_manager.connections.forEach((connection, path) => { + if (connection.isConnected) { + open_connections.push(connection); + } + }); + + let status: StatusCode; + if (detected_documents.size === 0) { + status = 'waiting'; + // TODO: instead of detected documents, I should use "detected_documents_with_LSP_servers_available" + } else if (initialized_documents.size === detected_documents.size) { + status = 'initialized'; + } else if (connected_documents.size === detected_documents.size) { + status = 'initializing'; + } else { + status = 'connecting'; + } + + return { + open_connections, + connected_documents, + initialized_documents, + detected_documents: new Set([...detected_documents.values()]), + status + }; } - get status(): string { + get status_icon(): string { + if (!this.adapter) { + return 'stop'; + } + let status = this.status; + + // TODO: associative array instead + if (status.status === 'waiting') { + return 'refresh'; + } else if (status.status === 'initialized') { + return 'running'; + } else if (status.status === 'initializing') { + return 'refresh'; + } else if (status.status === 'connecting') { + return 'refresh'; + } + } + + get short_message(): string { if (!this.adapter) { return 'not initialized'; + } + let status = this.status; + + let msg = ''; + // TODO: associative array instead + if (status.status === 'waiting') { + msg = 'Waiting...'; + } else if (status.status === 'initialized') { + msg = `Fully initialized`; + } else if (status.status === 'initializing') { + msg = `Fully connected & partially initialized`; } else { - let connection_manager = this.adapter.connection_manager; - const documents = connection_manager.documents; - let connected_documents = 0; - let initialized_documents = 0; - - documents.forEach((document, id_path) => { - let connection = connection_manager.connections.get(id_path); - if (!connection) { - return; - } - - // @ts-ignore - if (connection.isConnected) { - connected_documents += 1; - } - // @ts-ignore - if (connection.isInitialized) { - initialized_documents += 1; - } - }); - - // there may be more open connections than documents if a document was recently closed - // and the grace period has not passed yet - let open_connections = 0; - connection_manager.connections.forEach((connection, path) => { - // @ts-ignore - if (connection.isConnected) { - open_connections += 1; - console.warn('Connected:', path); - } else { - console.warn(path); - } - }); - let msg = ''; - const plural = documents.size > 1 ? 's' : ''; - if (documents.size === 0) { - msg = 'Waiting for documents initialization...'; - } else if (initialized_documents === documents.size) { - msg = `Fully connected & initialized (${documents.size} virtual document${plural})`; - } else if (connected_documents === documents.size) { - const uninitialized = documents.size - initialized_documents; - // servers for n documents did not respond ot the initialization request - msg = `Fully connected, but ${uninitialized}/${documents.size} virtual document${plural} stuck uninitialized`; - } else if (open_connections === 0) { - msg = `No open connections (${documents.size} virtual document${plural})`; - } else { - msg = `${connected_documents}/${documents.size} virtual document${plural} connected (${open_connections})`; + msg = `Connecting...`; + } + return msg; + } + + get long_message(): string { + if (!this.adapter) { + return 'not initialized'; + } + let status = this.status; + let msg = ''; + const plural = status.detected_documents.size > 1 ? 's' : ''; + if (status.status === 'waiting') { + msg = 'Waiting for documents initialization...'; + } else if (status.status === 'initialized') { + msg = `Fully connected & initialized (${status.detected_documents.size} virtual document${plural})`; + } else if (status.status === 'initializing') { + const uninitialized = new Set( + status.detected_documents + ); + for (let initialized of status.initialized_documents.values()) { + uninitialized.delete(initialized); } - return `${this.adapter.language} | ${msg}`; + // servers for n documents did not respond ot the initialization request + msg = `Fully connected, but ${uninitialized.size}/${ + status.detected_documents.size + } virtual document${plural} stuck uninitialized: ${[...uninitialized] + .map(document => document.id_path) + .join(', ')}`; + } else { + const unconnected = new Set(status.detected_documents); + for (let connected of status.connected_documents.values()) { + unconnected.delete(connected); + } + + msg = `${status.connected_documents.size}/${ + status.detected_documents.size + } virtual document${plural} connected (${ + status.open_connections.length + } connections; waiting for: ${[...unconnected] + .map(document => document.id_path) + .join(', ')})`; } + return msg; } get adapter(): JupyterLabWidgetAdapter | null { @@ -141,7 +301,6 @@ export namespace LSPStatus { this.stateChanged.emit(void 0); } - private _message: string = ''; private _adapter: JupyterLabWidgetAdapter | null = null; } } diff --git a/packages/jupyterlab-lsp/src/virtual/document.ts b/packages/jupyterlab-lsp/src/virtual/document.ts index d3655be33..e396471b7 100644 --- a/packages/jupyterlab-lsp/src/virtual/document.ts +++ b/packages/jupyterlab-lsp/src/virtual/document.ts @@ -129,7 +129,7 @@ export class VirtualDocument { private cell_magics_overrides: CellMagicsMap; private line_magics_overrides: LineMagicsMap; private static instances_count = 0; - private foreign_documents: Map; + public foreign_documents: Map; // TODO: make this configurable, depending on the language used blank_lines_between_cells: number = 2;