Skip to content

Commit dbf2500

Browse files
committed
Implement more advanced status bar
1 parent d256f00 commit dbf2500

File tree

2 files changed

+222
-63
lines changed

2 files changed

+222
-63
lines changed

packages/jupyterlab-lsp/src/adapters/jupyterlab/components/statusbar.tsx

Lines changed: 221 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,42 @@ import { VDomRenderer, VDomModel } from '@jupyterlab/apputils';
88

99
import {
1010
interactiveItem,
11-
// Popup,
12-
// showPopup,
13-
TextItem
11+
Popup,
12+
showPopup,
13+
TextItem,
14+
GroupItem
1415
} from '@jupyterlab/statusbar';
1516

17+
import { DefaultIconReact } from '@jupyterlab/ui-components';
1618
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+
}
1741

1842
/**
1943
* StatusBar item.
2044
*/
2145
export class LSPStatus extends VDomRenderer<LSPStatus.Model> {
46+
protected _popup: Popup = null;
2247
/**
2348
* Construct a new VDomRenderer for the status item.
2449
*/
@@ -36,81 +61,216 @@ export class LSPStatus extends VDomRenderer<LSPStatus.Model> {
3661
if (!this.model) {
3762
return null;
3863
}
39-
return <TextItem source={this.model.message} onClick={this.handleClick} />;
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+
);
4080
}
4181

42-
handleClick() {
43-
console.log('Click;');
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);
44110
}
111+
return collected;
45112
}
46113

47114
export namespace LSPStatus {
48115
/**
49116
* A VDomModel for the LSP of current file editor/notebook.
50117
*/
51118
export class Model extends VDomModel {
52-
get message(): string {
53-
return (
54-
'LSP Code Intelligence: ' +
55-
this.status +
56-
(this._message ? this._message : '')
57-
);
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+
};
58195
}
59196

60-
get status(): string {
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 {
61216
if (!this.adapter) {
62217
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`;
63229
} else {
64-
let connection_manager = this.adapter.connection_manager;
65-
const documents = connection_manager.documents;
66-
let connected_documents = 0;
67-
let initialized_documents = 0;
68-
69-
documents.forEach((document, id_path) => {
70-
let connection = connection_manager.connections.get(id_path);
71-
if (!connection) {
72-
return;
73-
}
74-
75-
// @ts-ignore
76-
if (connection.isConnected) {
77-
connected_documents += 1;
78-
}
79-
// @ts-ignore
80-
if (connection.isInitialized) {
81-
initialized_documents += 1;
82-
}
83-
});
84-
85-
// there may be more open connections than documents if a document was recently closed
86-
// and the grace period has not passed yet
87-
let open_connections = 0;
88-
connection_manager.connections.forEach((connection, path) => {
89-
// @ts-ignore
90-
if (connection.isConnected) {
91-
open_connections += 1;
92-
console.warn('Connected:', path);
93-
} else {
94-
console.warn(path);
95-
}
96-
});
97-
let msg = '';
98-
const plural = documents.size > 1 ? 's' : '';
99-
if (documents.size === 0) {
100-
msg = 'Waiting for documents initialization...';
101-
} else if (initialized_documents === documents.size) {
102-
msg = `Fully connected & initialized (${documents.size} virtual document${plural})`;
103-
} else if (connected_documents === documents.size) {
104-
const uninitialized = documents.size - initialized_documents;
105-
// servers for n documents did not respond ot the initialization request
106-
msg = `Fully connected, but ${uninitialized}/${documents.size} virtual document${plural} stuck uninitialized`;
107-
} else if (open_connections === 0) {
108-
msg = `No open connections (${documents.size} virtual document${plural})`;
109-
} else {
110-
msg = `${connected_documents}/${documents.size} virtual document${plural} connected (${open_connections})`;
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);
111252
}
112-
return `${this.adapter.language} | ${msg}`;
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(', ')})`;
113272
}
273+
return msg;
114274
}
115275

116276
get adapter(): JupyterLabWidgetAdapter | null {
@@ -141,7 +301,6 @@ export namespace LSPStatus {
141301
this.stateChanged.emit(void 0);
142302
}
143303

144-
private _message: string = '';
145304
private _adapter: JupyterLabWidgetAdapter | null = null;
146305
}
147306
}

packages/jupyterlab-lsp/src/virtual/document.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export class VirtualDocument {
129129
private cell_magics_overrides: CellMagicsMap;
130130
private line_magics_overrides: LineMagicsMap;
131131
private static instances_count = 0;
132-
private foreign_documents: Map<VirtualDocument.virtual_id, VirtualDocument>;
132+
public foreign_documents: Map<VirtualDocument.virtual_id, VirtualDocument>;
133133

134134
// TODO: make this configurable, depending on the language used
135135
blank_lines_between_cells: number = 2;

0 commit comments

Comments
 (0)