Skip to content

Commit 74236fc

Browse files
authored
Merge pull request #78 from krassowski/statusbar
Statusbar
2 parents 5c94947 + dbf2500 commit 74236fc

File tree

4 files changed

+368
-6
lines changed

4 files changed

+368
-6
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
// Based on the @jupyterlab/codemirror-extension statusbar
4+
5+
import React from 'react';
6+
7+
import { VDomRenderer, VDomModel } from '@jupyterlab/apputils';
8+
9+
import {
10+
interactiveItem,
11+
Popup,
12+
showPopup,
13+
TextItem,
14+
GroupItem
15+
} from '@jupyterlab/statusbar';
16+
17+
import { DefaultIconReact } from '@jupyterlab/ui-components';
18+
import { JupyterLabWidgetAdapter } from '../jl_adapter';
19+
import { VirtualDocument } from '../../../virtual/document';
20+
import { LSPConnection } from '../../../connection';
21+
22+
class LSPPopup extends VDomRenderer<LSPStatus.Model> {
23+
constructor(model: LSPStatus.Model) {
24+
super();
25+
this.model = model;
26+
// TODO: add proper, custom class?
27+
this.addClass('p-Menu');
28+
}
29+
render() {
30+
if (!this.model) {
31+
return null;
32+
}
33+
return (
34+
<GroupItem spacing={4} className={'p-Menu-item'}>
35+
<TextItem source={this.model.lsp_servers} />
36+
<TextItem source={this.model.long_message} />
37+
</GroupItem>
38+
);
39+
}
40+
}
41+
42+
/**
43+
* StatusBar item.
44+
*/
45+
export class LSPStatus extends VDomRenderer<LSPStatus.Model> {
46+
protected _popup: Popup = null;
47+
/**
48+
* Construct a new VDomRenderer for the status item.
49+
*/
50+
constructor() {
51+
super();
52+
this.model = new LSPStatus.Model();
53+
this.addClass(interactiveItem);
54+
this.title.caption = 'LSP status';
55+
}
56+
57+
/**
58+
* Render the status item.
59+
*/
60+
render() {
61+
if (!this.model) {
62+
return null;
63+
}
64+
return (
65+
<GroupItem
66+
spacing={4}
67+
title={'LSP Code Intelligence'}
68+
onClick={this.handleClick}
69+
>
70+
<DefaultIconReact name={'file'} top={'2px'} kind={'statusBar'} />
71+
<TextItem source={this.model.lsp_servers_truncated} />
72+
<DefaultIconReact
73+
name={this.model.status_icon}
74+
top={'2px'}
75+
kind={'statusBar'}
76+
/>
77+
<TextItem source={this.model.short_message} />
78+
</GroupItem>
79+
);
80+
}
81+
82+
handleClick = () => {
83+
if (this._popup) {
84+
this._popup.dispose();
85+
}
86+
this._popup = showPopup({
87+
body: new LSPPopup(this.model),
88+
anchor: this,
89+
align: 'left'
90+
});
91+
};
92+
}
93+
94+
type StatusCode = 'waiting' | 'initializing' | 'initialized' | 'connecting';
95+
96+
export interface IStatus {
97+
connected_documents: Set<VirtualDocument>;
98+
initialized_documents: Set<VirtualDocument>;
99+
open_connections: Array<LSPConnection>;
100+
detected_documents: Set<VirtualDocument>;
101+
status: StatusCode;
102+
}
103+
104+
function collect_languages(virtual_document: VirtualDocument): Set<string> {
105+
let collected = new Set<string>();
106+
collected.add(virtual_document.language);
107+
for (let foreign of virtual_document.foreign_documents.values()) {
108+
let foreign_languages = collect_languages(foreign);
109+
foreign_languages.forEach(collected.add, collected);
110+
}
111+
return collected;
112+
}
113+
114+
export namespace LSPStatus {
115+
/**
116+
* A VDomModel for the LSP of current file editor/notebook.
117+
*/
118+
export class Model extends VDomModel {
119+
get lsp_servers(): string {
120+
if (!this.adapter) {
121+
return '';
122+
}
123+
let document = this.adapter.virtual_editor.virtual_document;
124+
return `Languages detected: ${[...collect_languages(document)].join(
125+
', '
126+
)}`;
127+
}
128+
129+
get lsp_servers_truncated(): string {
130+
if (!this.adapter) {
131+
return '';
132+
}
133+
let document = this.adapter.virtual_editor.virtual_document;
134+
let foreign_languages = collect_languages(document);
135+
foreign_languages.delete(this.adapter.language);
136+
if (foreign_languages.size) {
137+
if (foreign_languages.size < 4) {
138+
return `${this.adapter.language}, ${[...foreign_languages].join(
139+
', '
140+
)}`;
141+
}
142+
return `${this.adapter.language} (+${foreign_languages.size} more)`;
143+
}
144+
return this.adapter.language;
145+
}
146+
147+
get status(): IStatus {
148+
let connection_manager = this.adapter.connection_manager;
149+
const detected_documents = connection_manager.documents;
150+
let connected_documents = new Set<VirtualDocument>();
151+
let initialized_documents = new Set<VirtualDocument>();
152+
153+
detected_documents.forEach((document, id_path) => {
154+
let connection = connection_manager.connections.get(id_path);
155+
if (!connection) {
156+
return;
157+
}
158+
159+
if (connection.isConnected) {
160+
connected_documents.add(document);
161+
}
162+
if (connection.isInitialized) {
163+
initialized_documents.add(document);
164+
}
165+
});
166+
167+
// there may be more open connections than documents if a document was recently closed
168+
// and the grace period has not passed yet
169+
let open_connections = new Array<LSPConnection>();
170+
connection_manager.connections.forEach((connection, path) => {
171+
if (connection.isConnected) {
172+
open_connections.push(connection);
173+
}
174+
});
175+
176+
let status: StatusCode;
177+
if (detected_documents.size === 0) {
178+
status = 'waiting';
179+
// TODO: instead of detected documents, I should use "detected_documents_with_LSP_servers_available"
180+
} else if (initialized_documents.size === detected_documents.size) {
181+
status = 'initialized';
182+
} else if (connected_documents.size === detected_documents.size) {
183+
status = 'initializing';
184+
} else {
185+
status = 'connecting';
186+
}
187+
188+
return {
189+
open_connections,
190+
connected_documents,
191+
initialized_documents,
192+
detected_documents: new Set([...detected_documents.values()]),
193+
status
194+
};
195+
}
196+
197+
get status_icon(): string {
198+
if (!this.adapter) {
199+
return 'stop';
200+
}
201+
let status = this.status;
202+
203+
// TODO: associative array instead
204+
if (status.status === 'waiting') {
205+
return 'refresh';
206+
} else if (status.status === 'initialized') {
207+
return 'running';
208+
} else if (status.status === 'initializing') {
209+
return 'refresh';
210+
} else if (status.status === 'connecting') {
211+
return 'refresh';
212+
}
213+
}
214+
215+
get short_message(): string {
216+
if (!this.adapter) {
217+
return 'not initialized';
218+
}
219+
let status = this.status;
220+
221+
let msg = '';
222+
// TODO: associative array instead
223+
if (status.status === 'waiting') {
224+
msg = 'Waiting...';
225+
} else if (status.status === 'initialized') {
226+
msg = `Fully initialized`;
227+
} else if (status.status === 'initializing') {
228+
msg = `Fully connected & partially initialized`;
229+
} else {
230+
msg = `Connecting...`;
231+
}
232+
return msg;
233+
}
234+
235+
get long_message(): string {
236+
if (!this.adapter) {
237+
return 'not initialized';
238+
}
239+
let status = this.status;
240+
let msg = '';
241+
const plural = status.detected_documents.size > 1 ? 's' : '';
242+
if (status.status === 'waiting') {
243+
msg = 'Waiting for documents initialization...';
244+
} else if (status.status === 'initialized') {
245+
msg = `Fully connected & initialized (${status.detected_documents.size} virtual document${plural})`;
246+
} else if (status.status === 'initializing') {
247+
const uninitialized = new Set<VirtualDocument>(
248+
status.detected_documents
249+
);
250+
for (let initialized of status.initialized_documents.values()) {
251+
uninitialized.delete(initialized);
252+
}
253+
// servers for n documents did not respond ot the initialization request
254+
msg = `Fully connected, but ${uninitialized.size}/${
255+
status.detected_documents.size
256+
} virtual document${plural} stuck uninitialized: ${[...uninitialized]
257+
.map(document => document.id_path)
258+
.join(', ')}`;
259+
} else {
260+
const unconnected = new Set<VirtualDocument>(status.detected_documents);
261+
for (let connected of status.connected_documents.values()) {
262+
unconnected.delete(connected);
263+
}
264+
265+
msg = `${status.connected_documents.size}/${
266+
status.detected_documents.size
267+
} virtual document${plural} connected (${
268+
status.open_connections.length
269+
} connections; waiting for: ${[...unconnected]
270+
.map(document => document.id_path)
271+
.join(', ')})`;
272+
}
273+
return msg;
274+
}
275+
276+
get adapter(): JupyterLabWidgetAdapter | null {
277+
return this._adapter;
278+
}
279+
set adapter(adapter: JupyterLabWidgetAdapter | null) {
280+
const oldAdapter = this._adapter;
281+
if (oldAdapter !== null) {
282+
oldAdapter.connection_manager.connected.disconnect(this._onChange);
283+
oldAdapter.connection_manager.initialized.connect(this._onChange);
284+
oldAdapter.connection_manager.disconnected.disconnect(this._onChange);
285+
oldAdapter.connection_manager.closed.disconnect(this._onChange);
286+
oldAdapter.connection_manager.documents_changed.disconnect(
287+
this._onChange
288+
);
289+
}
290+
291+
let onChange = this._onChange.bind(this);
292+
adapter.connection_manager.connected.connect(onChange);
293+
adapter.connection_manager.initialized.connect(onChange);
294+
adapter.connection_manager.disconnected.connect(onChange);
295+
adapter.connection_manager.closed.connect(onChange);
296+
adapter.connection_manager.documents_changed.connect(onChange);
297+
this._adapter = adapter;
298+
}
299+
300+
private _onChange() {
301+
this.stateChanged.emit(void 0);
302+
}
303+
304+
private _adapter: JupyterLabWidgetAdapter | null = null;
305+
}
306+
}

packages/jupyterlab-lsp/src/connection_manager.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface ISocketConnectionOptions {
3636
export class DocumentConnectionManager {
3737
connections: Map<VirtualDocument.id_path, LSPConnection>;
3838
documents: Map<VirtualDocument.id_path, VirtualDocument>;
39+
initialized: Signal<DocumentConnectionManager, IDocumentConnectionData>;
3940
connected: Signal<DocumentConnectionManager, IDocumentConnectionData>;
4041
/**
4142
* Connection temporarily lost or could not be fully established; a re-connection will be attempted;
@@ -48,15 +49,21 @@ export class DocumentConnectionManager {
4849
* - re-connection attempts exceeded,
4950
*/
5051
closed: Signal<DocumentConnectionManager, IDocumentConnectionData>;
52+
documents_changed: Signal<
53+
DocumentConnectionManager,
54+
Map<VirtualDocument.id_path, VirtualDocument>
55+
>;
5156
private ignored_languages: Set<string>;
5257

5358
constructor() {
5459
this.connections = new Map();
5560
this.documents = new Map();
5661
this.ignored_languages = new Set();
5762
this.connected = new Signal(this);
63+
this.initialized = new Signal(this);
5864
this.disconnected = new Signal(this);
5965
this.closed = new Signal(this);
66+
this.documents_changed = new Signal(this);
6067
}
6168

6269
connect_document_signals(virtual_document: VirtualDocument) {
@@ -72,9 +79,11 @@ export class DocumentConnectionManager {
7279
this.connections.get(foreign_document.id_path).close();
7380
this.connections.delete(foreign_document.id_path);
7481
this.documents.delete(foreign_document.id_path);
82+
this.documents_changed.emit(this.documents);
7583
}
7684
);
7785
this.documents.set(virtual_document.id_path, virtual_document);
86+
this.documents_changed.emit(this.documents);
7887
}
7988

8089
private connect_socket(options: ISocketConnectionOptions): LSPConnection {
@@ -98,7 +107,6 @@ export class DocumentConnectionManager {
98107
// NOTE: Update is async now and this is not really used, as an alternative method
99108
// which is compatible with async is used.
100109
// This should be only used in the initialization step.
101-
// @ts-ignore
102110
// if (main_connection.isConnected) {
103111
// console.warn('documentText is deprecated for use in JupyterLab LSP');
104112
// }
@@ -170,6 +178,10 @@ export class DocumentConnectionManager {
170178
async connect(options: ISocketConnectionOptions) {
171179
let connection = this.connect_socket(options);
172180

181+
connection.on('serverInitialized', capabilities => {
182+
this.initialized.emit({ connection, virtual_document });
183+
});
184+
173185
let { virtual_document, document_path } = options;
174186

175187
await until_ready(

0 commit comments

Comments
 (0)