@jupyterlab/lsp 4.0.0-alpha.19 → 4.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,684 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
5
+ import { IDocumentWidget } from '@jupyterlab/docregistry';
6
+ import { ISignal, Signal } from '@lumino/signaling';
7
+
8
+ import { WidgetLSPAdapter } from './adapters/adapter';
9
+ import { LSPConnection } from './connection';
10
+ import { ClientCapabilities } from './lsp';
11
+ import { AskServersToSendTraceNotifications } from './plugin';
12
+ import {
13
+ Document,
14
+ IDocumentConnectionData,
15
+ ILanguageServerManager,
16
+ ILSPConnection,
17
+ ILSPDocumentConnectionManager,
18
+ ISocketConnectionOptions,
19
+ TLanguageServerConfigurations,
20
+ TLanguageServerId,
21
+ TServerKeys
22
+ } from './tokens';
23
+ import { expandDottedPaths, sleep, untilReady } from './utils';
24
+ import { VirtualDocument } from './virtual/document';
25
+
26
+ import type * as protocol from 'vscode-languageserver-protocol';
27
+
28
+ /**
29
+ * Each Widget with a document (whether file or a notebook) has the same DocumentConnectionManager
30
+ * (see JupyterLabWidgetAdapter). Using id_path instead of uri led to documents being overwritten
31
+ * as two identical id_paths could be created for two different notebooks.
32
+ */
33
+ export class DocumentConnectionManager
34
+ implements ILSPDocumentConnectionManager
35
+ {
36
+ constructor(options: DocumentConnectionManager.IOptions) {
37
+ this.connections = new Map();
38
+ this.documents = new Map();
39
+ this.adapters = new Map();
40
+ this._ignoredLanguages = new Set();
41
+ this.languageServerManager = options.languageServerManager;
42
+ Private.setLanguageServerManager(options.languageServerManager);
43
+ }
44
+
45
+ /**
46
+ * Map between the URI of the virtual document and its connection
47
+ * to the language server
48
+ */
49
+ readonly connections: Map<VirtualDocument.uri, LSPConnection>;
50
+
51
+ /**
52
+ * Map between the path of the document and its adapter
53
+ */
54
+ readonly adapters: Map<string, WidgetLSPAdapter<IDocumentWidget>>;
55
+
56
+ /**
57
+ * Map between the URI of the virtual document and the document itself.
58
+ */
59
+ readonly documents: Map<VirtualDocument.uri, VirtualDocument>;
60
+ /**
61
+ * The language server manager plugin.
62
+ */
63
+ readonly languageServerManager: ILanguageServerManager;
64
+
65
+ /**
66
+ * Initial configuration for the language servers.
67
+ */
68
+ initialConfigurations: TLanguageServerConfigurations;
69
+
70
+ /**
71
+ * Signal emitted when the manager is initialized.
72
+ */
73
+ get initialized(): ISignal<
74
+ ILSPDocumentConnectionManager,
75
+ IDocumentConnectionData
76
+ > {
77
+ return this._initialized;
78
+ }
79
+
80
+ /**
81
+ * Signal emitted when the manager is connected to the server
82
+ */
83
+ get connected(): ISignal<
84
+ ILSPDocumentConnectionManager,
85
+ IDocumentConnectionData
86
+ > {
87
+ return this._connected;
88
+ }
89
+
90
+ /**
91
+ * Connection temporarily lost or could not be fully established; a re-connection will be attempted;
92
+ */
93
+ get disconnected(): ISignal<
94
+ ILSPDocumentConnectionManager,
95
+ IDocumentConnectionData
96
+ > {
97
+ return this._disconnected;
98
+ }
99
+
100
+ /**
101
+ * Connection was closed permanently and no-reconnection will be attempted, e.g.:
102
+ * - there was a serious server error
103
+ * - user closed the connection,
104
+ * - re-connection attempts exceeded,
105
+ */
106
+ get closed(): ISignal<
107
+ ILSPDocumentConnectionManager,
108
+ IDocumentConnectionData
109
+ > {
110
+ return this._closed;
111
+ }
112
+
113
+ /**
114
+ * Signal emitted when the document is changed.
115
+ */
116
+ get documentsChanged(): ISignal<
117
+ ILSPDocumentConnectionManager,
118
+ Map<VirtualDocument.uri, VirtualDocument>
119
+ > {
120
+ return this._documentsChanged;
121
+ }
122
+
123
+ /**
124
+ * Promise resolved when the language server manager is ready.
125
+ */
126
+ get ready(): Promise<void> {
127
+ return Private.getLanguageServerManager().ready;
128
+ }
129
+
130
+ /**
131
+ * Helper to connect various virtual document signal with callbacks of
132
+ * this class.
133
+ *
134
+ * @param virtualDocument - virtual document to be connected.
135
+ */
136
+ connectDocumentSignals(virtualDocument: VirtualDocument): void {
137
+ virtualDocument.foreignDocumentOpened.connect(
138
+ this.onForeignDocumentOpened,
139
+ this
140
+ );
141
+
142
+ virtualDocument.foreignDocumentClosed.connect(
143
+ this.onForeignDocumentClosed,
144
+ this
145
+ );
146
+ this.documents.set(virtualDocument.uri, virtualDocument);
147
+ this._documentsChanged.emit(this.documents);
148
+ }
149
+
150
+ /**
151
+ * Helper to disconnect various virtual document signal with callbacks of
152
+ * this class.
153
+ *
154
+ * @param virtualDocument - virtual document to be disconnected.
155
+ */
156
+ disconnectDocumentSignals(
157
+ virtualDocument: VirtualDocument,
158
+ emit = true
159
+ ): void {
160
+ virtualDocument.foreignDocumentOpened.disconnect(
161
+ this.onForeignDocumentOpened,
162
+ this
163
+ );
164
+
165
+ virtualDocument.foreignDocumentClosed.disconnect(
166
+ this.onForeignDocumentClosed,
167
+ this
168
+ );
169
+ this.documents.delete(virtualDocument.uri);
170
+ for (const foreign of virtualDocument.foreignDocuments.values()) {
171
+ this.disconnectDocumentSignals(foreign, false);
172
+ }
173
+
174
+ if (emit) {
175
+ this._documentsChanged.emit(this.documents);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Handle foreign document opened event.
181
+ */
182
+ onForeignDocumentOpened(
183
+ _host: VirtualDocument,
184
+ context: Document.IForeignContext
185
+ ): void {
186
+ /** no-op */
187
+ }
188
+
189
+ /**
190
+ * Handle foreign document closed event.
191
+ */
192
+ onForeignDocumentClosed(
193
+ _host: VirtualDocument,
194
+ context: Document.IForeignContext
195
+ ): void {
196
+ const { foreignDocument } = context;
197
+ this.unregisterDocument(foreignDocument.uri, false);
198
+ this.disconnectDocumentSignals(foreignDocument);
199
+ }
200
+
201
+ /**
202
+ * Register a widget adapter with this manager
203
+ *
204
+ * @param path - path to the inner document of the adapter
205
+ * @param adapter - the adapter to be registered
206
+ */
207
+ registerAdapter(
208
+ path: string,
209
+ adapter: WidgetLSPAdapter<IDocumentWidget>
210
+ ): void {
211
+ this.adapters.set(path, adapter);
212
+ adapter.disposed.connect(() => {
213
+ if (adapter.virtualDocument) {
214
+ this.documents.delete(adapter.virtualDocument.uri);
215
+ }
216
+ this.adapters.delete(path);
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Handles the settings that do not require an existing connection
222
+ * with a language server (or can influence to which server the
223
+ * connection will be created, e.g. `rank`).
224
+ *
225
+ * This function should be called **before** initialization of servers.
226
+ */
227
+ updateConfiguration(allServerSettings: TLanguageServerConfigurations): void {
228
+ this.languageServerManager.setConfiguration(allServerSettings);
229
+ }
230
+
231
+ /**
232
+ * Handles the settings that the language servers accept using
233
+ * `onDidChangeConfiguration` messages, which should be passed under
234
+ * the "serverSettings" keyword in the setting registry.
235
+ * Other configuration options are handled by `updateConfiguration` instead.
236
+ *
237
+ * This function should be called **after** initialization of servers.
238
+ */
239
+ updateServerConfigurations(
240
+ allServerSettings: TLanguageServerConfigurations
241
+ ): void {
242
+ let languageServerId: TServerKeys;
243
+
244
+ for (languageServerId in allServerSettings) {
245
+ if (!allServerSettings.hasOwnProperty(languageServerId)) {
246
+ continue;
247
+ }
248
+ const rawSettings = allServerSettings[languageServerId]!;
249
+
250
+ const parsedSettings = expandDottedPaths(rawSettings.configuration || {});
251
+
252
+ const serverSettings: protocol.DidChangeConfigurationParams = {
253
+ settings: parsedSettings
254
+ };
255
+
256
+ Private.updateServerConfiguration(languageServerId, serverSettings);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Fired the first time a connection is opened. These _should_ be the only
262
+ * invocation of `.on` (once remaining LSPFeature.connection_handlers are made
263
+ * singletons).
264
+ */
265
+ onNewConnection = (connection: LSPConnection): void => {
266
+ const errorSignalSlot = (_: ILSPConnection, e: any): void => {
267
+ console.error(e);
268
+ let error: Error = e.length && e.length >= 1 ? e[0] : new Error();
269
+ if (error.message.indexOf('code = 1005') !== -1) {
270
+ console.error(`Connection failed for ${connection}`);
271
+ this._forEachDocumentOfConnection(connection, virtualDocument => {
272
+ console.error('disconnecting ' + virtualDocument.uri);
273
+ this._closed.emit({ connection, virtualDocument });
274
+ this._ignoredLanguages.add(virtualDocument.language);
275
+ console.error(
276
+ `Cancelling further attempts to connect ${virtualDocument.uri} and other documents for this language (no support from the server)`
277
+ );
278
+ });
279
+ } else if (error.message.indexOf('code = 1006') !== -1) {
280
+ console.error('Connection closed by the server');
281
+ } else {
282
+ console.error('Connection error:', e);
283
+ }
284
+ };
285
+ connection.errorSignal.connect(errorSignalSlot);
286
+
287
+ const serverInitializedSlot = (): void => {
288
+ // Initialize using settings stored in the SettingRegistry
289
+ this._forEachDocumentOfConnection(connection, virtualDocument => {
290
+ // TODO: is this still necessary, e.g. for status bar to update responsively?
291
+ this._initialized.emit({ connection, virtualDocument });
292
+ });
293
+ this.updateServerConfigurations(this.initialConfigurations);
294
+ };
295
+ connection.serverInitialized.connect(serverInitializedSlot);
296
+
297
+ const closeSignalSlot = (_: ILSPConnection, closedManually: boolean) => {
298
+ if (!closedManually) {
299
+ console.error('Connection unexpectedly disconnected');
300
+ } else {
301
+ console.log('Connection closed');
302
+ this._forEachDocumentOfConnection(connection, virtualDocument => {
303
+ this._closed.emit({ connection, virtualDocument });
304
+ });
305
+ }
306
+ };
307
+ connection.closeSignal.connect(closeSignalSlot);
308
+ };
309
+
310
+ /**
311
+ * Retry to connect to the server each `reconnectDelay` seconds
312
+ * and for `retrialsLeft` times.
313
+ * TODO: presently no longer referenced. A failing connection would close
314
+ * the socket, triggering the language server on the other end to exit.
315
+ */
316
+ async retryToConnect(
317
+ options: ISocketConnectionOptions,
318
+ reconnectDelay: number,
319
+ retrialsLeft = -1
320
+ ): Promise<void> {
321
+ let { virtualDocument } = options;
322
+
323
+ if (this._ignoredLanguages.has(virtualDocument.language)) {
324
+ return;
325
+ }
326
+
327
+ let interval = reconnectDelay * 1000;
328
+ let success = false;
329
+
330
+ while (retrialsLeft !== 0 && !success) {
331
+ await this.connect(options)
332
+ .then(() => {
333
+ success = true;
334
+ })
335
+ .catch(e => {
336
+ console.warn(e);
337
+ });
338
+
339
+ console.log(
340
+ 'will attempt to re-connect in ' + interval / 1000 + ' seconds'
341
+ );
342
+ await sleep(interval);
343
+
344
+ // gradually increase the time delay, up to 5 sec
345
+ interval = interval < 5 * 1000 ? interval + 500 : interval;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Disconnect the connection to the language server of the requested
351
+ * language.
352
+ */
353
+ disconnect(languageId: TLanguageServerId): void {
354
+ Private.disconnect(languageId);
355
+ }
356
+
357
+ /**
358
+ * Create a new connection to the language server
359
+ * @return A promise of the LSP connection
360
+ */
361
+ async connect(
362
+ options: ISocketConnectionOptions,
363
+ firstTimeoutSeconds = 30,
364
+ secondTimeoutMinutes = 5
365
+ ): Promise<ILSPConnection | undefined> {
366
+ let connection = await this._connectSocket(options);
367
+ let { virtualDocument } = options;
368
+ if (!connection) {
369
+ return;
370
+ }
371
+ if (!connection.isReady) {
372
+ try {
373
+ // user feedback hinted that 40 seconds was too short and some users are willing to wait more;
374
+ // to make the best of both worlds we first check frequently (6.6 times a second) for the first
375
+ // 30 seconds, and show the warning early in case if something is wrong; we then continue retrying
376
+ // for another 5 minutes, but only once per second.
377
+ await untilReady(
378
+ () => connection!.isReady,
379
+ Math.round((firstTimeoutSeconds * 1000) / 150),
380
+ 150
381
+ );
382
+ } catch {
383
+ console.log(
384
+ `Connection to ${virtualDocument.uri} timed out after ${firstTimeoutSeconds} seconds, will continue retrying for another ${secondTimeoutMinutes} minutes`
385
+ );
386
+ try {
387
+ await untilReady(
388
+ () => connection!.isReady,
389
+ 60 * secondTimeoutMinutes,
390
+ 1000
391
+ );
392
+ } catch {
393
+ console.log(
394
+ `Connection to ${virtualDocument.uri} timed out again after ${secondTimeoutMinutes} minutes, giving up`
395
+ );
396
+ return;
397
+ }
398
+ }
399
+ }
400
+
401
+ this._connected.emit({ connection, virtualDocument });
402
+
403
+ return connection;
404
+ }
405
+
406
+ /**
407
+ * Disconnect the signals of requested virtual document uri.
408
+ */
409
+ unregisterDocument(uri: string, emit: boolean = true): void {
410
+ const connection = this.connections.get(uri);
411
+ if (connection) {
412
+ this.connections.delete(uri);
413
+ const allConnection = new Set(this.connections.values());
414
+
415
+ if (!allConnection.has(connection)) {
416
+ this.disconnect(connection.serverIdentifier as TLanguageServerId);
417
+ connection.dispose();
418
+ }
419
+ if (emit) {
420
+ this._documentsChanged.emit(this.documents);
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Enable or disable the logging feature of the language servers
427
+ */
428
+ updateLogging(
429
+ logAllCommunication: boolean,
430
+ setTrace: AskServersToSendTraceNotifications
431
+ ): void {
432
+ for (const connection of this.connections.values()) {
433
+ connection.logAllCommunication = logAllCommunication;
434
+ if (setTrace !== null) {
435
+ connection.clientNotifications['$/setTrace'].emit({ value: setTrace });
436
+ }
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Create the LSP connection for requested virtual document.
442
+ *
443
+ * @return Return the promise of the LSP connection.
444
+ */
445
+
446
+ private async _connectSocket(
447
+ options: ISocketConnectionOptions
448
+ ): Promise<LSPConnection | undefined> {
449
+ let { language, capabilities, virtualDocument } = options;
450
+
451
+ this.connectDocumentSignals(virtualDocument);
452
+
453
+ const uris = DocumentConnectionManager.solveUris(virtualDocument, language);
454
+ const matchingServers = this.languageServerManager.getMatchingServers({
455
+ language
456
+ });
457
+
458
+ // for now use only the server with the highest rank.
459
+ const languageServerId =
460
+ matchingServers.length === 0 ? null : matchingServers[0];
461
+
462
+ // lazily load 1) the underlying library (1.5mb) and/or 2) a live WebSocket-
463
+ // like connection: either already connected or potentially in the process
464
+ // of connecting.
465
+ if (!uris) {
466
+ return;
467
+ }
468
+ const connection = await Private.connection(
469
+ language,
470
+ languageServerId!,
471
+ uris,
472
+ this.onNewConnection,
473
+ capabilities
474
+ );
475
+
476
+ // if connecting for the first time, all documents subsequent documents will
477
+ // be re-opened and synced
478
+ this.connections.set(virtualDocument.uri, connection);
479
+
480
+ return connection;
481
+ }
482
+
483
+ /**
484
+ * Helper to apply callback on all documents of a connection.
485
+ */
486
+ private _forEachDocumentOfConnection(
487
+ connection: ILSPConnection,
488
+ callback: (virtualDocument: VirtualDocument) => void
489
+ ) {
490
+ for (const [
491
+ virtualDocumentUri,
492
+ currentConnection
493
+ ] of this.connections.entries()) {
494
+ if (connection !== currentConnection) {
495
+ continue;
496
+ }
497
+ callback(this.documents.get(virtualDocumentUri)!);
498
+ }
499
+ }
500
+
501
+ private _initialized: Signal<
502
+ ILSPDocumentConnectionManager,
503
+ IDocumentConnectionData
504
+ > = new Signal(this);
505
+
506
+ private _connected: Signal<
507
+ ILSPDocumentConnectionManager,
508
+ IDocumentConnectionData
509
+ > = new Signal(this);
510
+
511
+ private _disconnected: Signal<
512
+ ILSPDocumentConnectionManager,
513
+ IDocumentConnectionData
514
+ > = new Signal(this);
515
+
516
+ private _closed: Signal<
517
+ ILSPDocumentConnectionManager,
518
+ IDocumentConnectionData
519
+ > = new Signal(this);
520
+
521
+ private _documentsChanged: Signal<
522
+ ILSPDocumentConnectionManager,
523
+ Map<VirtualDocument.uri, VirtualDocument>
524
+ > = new Signal(this);
525
+
526
+ /**
527
+ * Set of ignored languages
528
+ */
529
+ private _ignoredLanguages: Set<string>;
530
+ }
531
+
532
+ export namespace DocumentConnectionManager {
533
+ export interface IOptions {
534
+ /**
535
+ * The language server manager instance.
536
+ */
537
+ languageServerManager: ILanguageServerManager;
538
+ }
539
+
540
+ /**
541
+ * Generate the URI of a virtual document from input
542
+ *
543
+ * @param virtualDocument - the virtual document
544
+ * @param language - language of the document
545
+ */
546
+ export function solveUris(
547
+ virtualDocument: VirtualDocument,
548
+ language: string
549
+ ): IURIs | undefined {
550
+ const wsBase = PageConfig.getBaseUrl().replace(/^http/, 'ws');
551
+ const rootUri = PageConfig.getOption('rootUri');
552
+ const virtualDocumentsUri = PageConfig.getOption('virtualDocumentsUri');
553
+
554
+ const baseUri = virtualDocument.hasLspSupportedFile
555
+ ? rootUri
556
+ : virtualDocumentsUri;
557
+
558
+ // for now take the best match only
559
+ const matchingServers =
560
+ Private.getLanguageServerManager().getMatchingServers({
561
+ language
562
+ });
563
+ const languageServerId =
564
+ matchingServers.length === 0 ? null : matchingServers[0];
565
+
566
+ if (languageServerId === null) {
567
+ return;
568
+ }
569
+
570
+ // workaround url-parse bug(s) (see https://github.com/jupyter-lsp/jupyterlab-lsp/issues/595)
571
+ let documentUri = URLExt.join(baseUri, virtualDocument.uri);
572
+ if (
573
+ !documentUri.startsWith('file:///') &&
574
+ documentUri.startsWith('file://')
575
+ ) {
576
+ documentUri = documentUri.replace('file://', 'file:///');
577
+ if (
578
+ documentUri.startsWith('file:///users/') &&
579
+ baseUri.startsWith('file:///Users/')
580
+ ) {
581
+ documentUri = documentUri.replace('file:///users/', 'file:///Users/');
582
+ }
583
+ }
584
+
585
+ return {
586
+ base: baseUri,
587
+ document: documentUri,
588
+ server: URLExt.join('ws://jupyter-lsp', language),
589
+ socket: URLExt.join(wsBase, 'lsp', 'ws', languageServerId)
590
+ };
591
+ }
592
+
593
+ export interface IURIs {
594
+ /**
595
+ * The root URI set by server.
596
+ *
597
+ */
598
+ base: string;
599
+
600
+ /**
601
+ * The URI to the virtual document.
602
+ *
603
+ */
604
+ document: string;
605
+
606
+ /**
607
+ * Address of websocket endpoint for LSP services.
608
+ *
609
+ */
610
+ server: string;
611
+
612
+ /**
613
+ * Address of websocket endpoint for the language server.
614
+ *
615
+ */
616
+ socket: string;
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Namespace primarily for language-keyed cache of LSPConnections
622
+ */
623
+ namespace Private {
624
+ const _connections: Map<TLanguageServerId, LSPConnection> = new Map();
625
+ let _languageServerManager: ILanguageServerManager;
626
+
627
+ export function getLanguageServerManager(): ILanguageServerManager {
628
+ return _languageServerManager;
629
+ }
630
+ export function setLanguageServerManager(
631
+ languageServerManager: ILanguageServerManager
632
+ ): void {
633
+ _languageServerManager = languageServerManager;
634
+ }
635
+
636
+ export function disconnect(languageServerId: TLanguageServerId): void {
637
+ const connection = _connections.get(languageServerId);
638
+ if (connection) {
639
+ connection.close();
640
+ _connections.delete(languageServerId);
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Return (or create and initialize) the WebSocket associated with the language
646
+ */
647
+ export async function connection(
648
+ language: string,
649
+ languageServerId: TLanguageServerId,
650
+ uris: DocumentConnectionManager.IURIs,
651
+ onCreate: (connection: LSPConnection) => void,
652
+ capabilities: ClientCapabilities
653
+ ): Promise<LSPConnection> {
654
+ let connection = _connections.get(languageServerId);
655
+ if (!connection) {
656
+ const socket = new WebSocket(uris.socket);
657
+ const connection = new LSPConnection({
658
+ languageId: language,
659
+ serverUri: uris.server,
660
+ rootUri: uris.base,
661
+ serverIdentifier: languageServerId,
662
+ capabilities: capabilities
663
+ });
664
+
665
+ _connections.set(languageServerId, connection);
666
+ connection.connect(socket);
667
+ onCreate(connection);
668
+ }
669
+
670
+ connection = _connections.get(languageServerId)!;
671
+
672
+ return connection;
673
+ }
674
+
675
+ export function updateServerConfiguration(
676
+ languageServerId: TLanguageServerId,
677
+ settings: protocol.DidChangeConfigurationParams
678
+ ): void {
679
+ const connection = _connections.get(languageServerId);
680
+ if (connection) {
681
+ connection.sendConfigurationChange(settings);
682
+ }
683
+ }
684
+ }
@@ -0,0 +1,6 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ export * from './manager';
5
+ export * from './text_extractor';
6
+ export * from './types';