@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,686 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import mergeWith from 'lodash.mergewith';
5
+
6
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
7
+ import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry';
8
+ import {
9
+ ITranslator,
10
+ nullTranslator,
11
+ TranslationBundle
12
+ } from '@jupyterlab/translation';
13
+ import { JSONObject } from '@lumino/coreutils';
14
+ import { IDisposable } from '@lumino/disposable';
15
+ import { ISignal, Signal } from '@lumino/signaling';
16
+
17
+ import { ClientCapabilities, LanguageIdentifier } from '../lsp';
18
+ import { IVirtualPosition } from '../positioning';
19
+ import {
20
+ Document,
21
+ IDocumentConnectionData,
22
+ ILSPCodeExtractorsManager,
23
+ ILSPDocumentConnectionManager,
24
+ ILSPFeatureManager,
25
+ ISocketConnectionOptions
26
+ } from '../tokens';
27
+ import { VirtualDocument } from '../virtual/document';
28
+
29
+ type IButton = Dialog.IButton;
30
+ const createButton = Dialog.createButton;
31
+
32
+ /**
33
+ * The values should follow the https://microsoft.github.io/language-server-protocol/specification guidelines
34
+ */
35
+ const MIME_TYPE_LANGUAGE_MAP: JSONObject = {
36
+ 'text/x-rsrc': 'r',
37
+ 'text/x-r-source': 'r',
38
+ // currently there are no LSP servers for IPython we are aware of
39
+ 'text/x-ipython': 'python'
40
+ };
41
+
42
+ export interface IEditorChangedData {
43
+ /**
44
+ * The CM editor invoking the change event.
45
+ */
46
+ editor: Document.IEditor;
47
+ }
48
+
49
+ export interface IAdapterOptions {
50
+ /**
51
+ * The LSP document and connection manager instance.
52
+ */
53
+ connectionManager: ILSPDocumentConnectionManager;
54
+
55
+ /**
56
+ * The LSP feature manager instance.
57
+ */
58
+ featureManager: ILSPFeatureManager;
59
+
60
+ /**
61
+ * The LSP foreign code extractor manager.
62
+ */
63
+ foreignCodeExtractorsManager: ILSPCodeExtractorsManager;
64
+
65
+ /**
66
+ * The translator provider.
67
+ */
68
+ translator?: ITranslator;
69
+ }
70
+
71
+ /**
72
+ * Foreign code: low level adapter is not aware of the presence of foreign languages;
73
+ * it operates on the virtual document and must not attempt to infer the language dependencies
74
+ * as this would make the logic of inspections caching impossible to maintain, thus the WidgetAdapter
75
+ * has to handle that, keeping multiple connections and multiple virtual documents.
76
+ */
77
+ export abstract class WidgetLSPAdapter<T extends IDocumentWidget>
78
+ implements IDisposable
79
+ {
80
+ // note: it could be using namespace/IOptions pattern,
81
+ // but I do not know how to make it work with the generic type T
82
+ // (other than using 'any' in the IOptions interface)
83
+ constructor(public widget: T, protected options: IAdapterOptions) {
84
+ this._connectionManager = options.connectionManager;
85
+ this._isConnected = false;
86
+ this._trans = (options.translator || nullTranslator).load('jupyterlab');
87
+ // set up signal connections
88
+ this.widget.context.saveState.connect(this.onSaveState, this);
89
+ this.connectionManager.closed.connect(this.onConnectionClosed, this);
90
+ this.widget.disposed.connect(this.dispose, this);
91
+ }
92
+
93
+ /**
94
+ * Check if the adapter is disposed
95
+ */
96
+ get isDisposed(): boolean {
97
+ return this._isDisposed;
98
+ }
99
+ /**
100
+ * Check if the document contains multiple editors
101
+ */
102
+ get hasMultipleEditors(): boolean {
103
+ return this.editors.length > 1;
104
+ }
105
+ /**
106
+ * Get the ID of the internal widget.
107
+ */
108
+ get widgetId(): string {
109
+ return this.widget.id;
110
+ }
111
+
112
+ /**
113
+ * Get the language identifier of the document
114
+ */
115
+ get language(): LanguageIdentifier {
116
+ // the values should follow https://microsoft.github.io/language-server-protocol/specification guidelines,
117
+ // see the table in https://microsoft.github.io/language-server-protocol/specification#textDocumentItem
118
+ if (MIME_TYPE_LANGUAGE_MAP.hasOwnProperty(this.mimeType)) {
119
+ return MIME_TYPE_LANGUAGE_MAP[this.mimeType] as string;
120
+ } else {
121
+ let withoutParameters = this.mimeType.split(';')[0];
122
+ let [type, subtype] = withoutParameters.split('/');
123
+ if (type === 'application' || type === 'text') {
124
+ if (subtype.startsWith('x-')) {
125
+ return subtype.substring(2);
126
+ } else {
127
+ return subtype;
128
+ }
129
+ } else {
130
+ return this.mimeType;
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Signal emitted when the adapter is connected.
137
+ */
138
+ get adapterConnected(): ISignal<
139
+ WidgetLSPAdapter<T>,
140
+ IDocumentConnectionData
141
+ > {
142
+ return this._adapterConnected;
143
+ }
144
+
145
+ /**
146
+ * Signal emitted when the active editor have changed.
147
+ */
148
+ get activeEditorChanged(): ISignal<WidgetLSPAdapter<T>, IEditorChangedData> {
149
+ return this._activeEditorChanged;
150
+ }
151
+
152
+ /**
153
+ * Signal emitted when the adapter is disposed.
154
+ */
155
+ get disposed(): ISignal<WidgetLSPAdapter<T>, void> {
156
+ return this._disposed;
157
+ }
158
+
159
+ /**
160
+ * Signal emitted when the an editor is changed.
161
+ */
162
+ get editorAdded(): ISignal<WidgetLSPAdapter<T>, IEditorChangedData> {
163
+ return this._editorAdded;
164
+ }
165
+
166
+ /**
167
+ * Signal emitted when the an editor is removed.
168
+ */
169
+ get editorRemoved(): ISignal<WidgetLSPAdapter<T>, IEditorChangedData> {
170
+ return this._editorRemoved;
171
+ }
172
+
173
+ /**
174
+ * Get the inner HTMLElement of the document widget.
175
+ */
176
+ abstract get wrapperElement(): HTMLElement;
177
+
178
+ /**
179
+ * Get current path of the document.
180
+ */
181
+ abstract get documentPath(): string;
182
+
183
+ /**
184
+ * Get the mime type of the document.
185
+ */
186
+ abstract get mimeType(): string;
187
+
188
+ /**
189
+ * Get the file extension of the document.
190
+ */
191
+ abstract get languageFileExtension(): string | undefined;
192
+
193
+ /**
194
+ * Get the activated CM editor.
195
+ */
196
+ abstract get activeEditor(): Document.IEditor | undefined;
197
+
198
+ /**
199
+ * Get the list of CM editors in the document, there is only one editor
200
+ * in the case of file editor.
201
+ */
202
+ abstract get editors(): Document.ICodeBlockOptions[];
203
+
204
+ /**
205
+ * Promise that resolves once the adapter is initialized
206
+ */
207
+ abstract get ready(): Promise<void>;
208
+
209
+ /**
210
+ * The virtual document is connected or not
211
+ */
212
+ get isConnected(): boolean {
213
+ return this._isConnected;
214
+ }
215
+
216
+ /**
217
+ * The LSP document and connection manager instance.
218
+ */
219
+ get connectionManager(): ILSPDocumentConnectionManager {
220
+ return this._connectionManager;
221
+ }
222
+
223
+ /**
224
+ * The translator provider.
225
+ */
226
+ get trans(): TranslationBundle {
227
+ return this._trans;
228
+ }
229
+
230
+ /**
231
+ * Promise that resolves once the document is updated
232
+ */
233
+ get updateFinished(): Promise<void> {
234
+ return this._updateFinished;
235
+ }
236
+
237
+ /**
238
+ * Internal virtual document of the adapter.
239
+ */
240
+ get virtualDocument(): VirtualDocument | null {
241
+ return this._virtualDocument;
242
+ }
243
+
244
+ /**
245
+ * Callback on connection closed event.
246
+ */
247
+ onConnectionClosed(
248
+ _: ILSPDocumentConnectionManager,
249
+ { virtualDocument }: IDocumentConnectionData
250
+ ): void {
251
+ if (virtualDocument === this.virtualDocument) {
252
+ this.dispose();
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Dispose the adapter.
258
+ */
259
+ dispose(): void {
260
+ if (this._isDisposed) {
261
+ return;
262
+ }
263
+ this._isDisposed = true;
264
+ this.disconnect();
265
+ this._virtualDocument = null;
266
+ this._disposed.emit();
267
+ Signal.clearData(this);
268
+ }
269
+
270
+ /**
271
+ * Disconnect virtual document from the language server.
272
+ */
273
+ disconnect(): void {
274
+ const uri = this.virtualDocument?.uri;
275
+ const { model } = this.widget.context;
276
+ if (uri) {
277
+ this.connectionManager.unregisterDocument(uri);
278
+ }
279
+ model.contentChanged.disconnect(this._onContentChanged, this);
280
+
281
+ // pretend that all editors were removed to trigger the disconnection of even handlers
282
+ // they will be connected again on new connection
283
+ for (let { ceEditor: editor } of this.editors) {
284
+ this._editorRemoved.emit({
285
+ editor: editor
286
+ });
287
+ }
288
+
289
+ this.virtualDocument?.dispose();
290
+ }
291
+
292
+ /**
293
+ * Update the virtual document.
294
+ */
295
+ updateDocuments(): Promise<void> {
296
+ if (this._isDisposed) {
297
+ console.warn('Cannot update documents: adapter disposed');
298
+ return Promise.reject('Cannot update documents: adapter disposed');
299
+ }
300
+ return this.virtualDocument!.updateManager.updateDocuments(this.editors);
301
+ }
302
+
303
+ /**
304
+ * Callback called on the document changed event.
305
+ */
306
+ documentChanged(
307
+ virtualDocument: VirtualDocument,
308
+ document: VirtualDocument,
309
+ isInit = false
310
+ ): void {
311
+ if (this._isDisposed) {
312
+ console.warn('Cannot swap document: adapter disposed');
313
+ return;
314
+ }
315
+
316
+ // TODO only send the difference, using connection.sendSelectiveChange()
317
+ let connection = this.connectionManager.connections.get(
318
+ virtualDocument.uri
319
+ );
320
+
321
+ if (!connection?.isReady) {
322
+ console.log('Skipping document update signal: connection not ready');
323
+ return;
324
+ }
325
+
326
+ connection.sendFullTextChange(
327
+ virtualDocument.value,
328
+ virtualDocument.documentInfo
329
+ );
330
+ }
331
+
332
+ /**
333
+ * (re)create virtual document using current path and language
334
+ */
335
+ protected abstract createVirtualDocument(): VirtualDocument;
336
+
337
+ /**
338
+ * Get the index of editor from the cursor position in the virtual
339
+ * document. Since there is only one editor, this method always return
340
+ * 0
341
+ *
342
+ * @param position - the position of cursor in the virtual document.
343
+ * @return - index of the virtual editor
344
+ */
345
+ abstract getEditorIndexAt(position: IVirtualPosition): number;
346
+
347
+ /**
348
+ * Get the index of input editor
349
+ *
350
+ * @param ceEditor - instance of the code editor
351
+ */
352
+ abstract getEditorIndex(ceEditor: Document.IEditor): number;
353
+
354
+ /**
355
+ * Get the wrapper of input editor.
356
+ *
357
+ * @param ceEditor
358
+ */
359
+ abstract getEditorWrapper(ceEditor: Document.IEditor): HTMLElement;
360
+
361
+ // equivalent to triggering didClose and didOpen, as per syncing specification,
362
+ // but also reloads the connection; used during file rename (or when it was moved)
363
+ protected reloadConnection(): void {
364
+ // ignore premature calls (before the editor was initialized)
365
+ if (this.virtualDocument === null) {
366
+ return;
367
+ }
368
+
369
+ // disconnect all existing connections (and dispose adapters)
370
+ this.disconnect();
371
+
372
+ // recreate virtual document using current path and language
373
+ // as virtual editor assumes it gets the virtual document at init,
374
+ // just dispose virtual editor (which disposes virtual document too)
375
+ // and re-initialize both virtual editor and document
376
+ this.initVirtual();
377
+
378
+ // reconnect
379
+ this.connectDocument(this.virtualDocument, true).catch(console.warn);
380
+ }
381
+
382
+ /**
383
+ * Callback on document saved event.
384
+ */
385
+ protected onSaveState(
386
+ context: DocumentRegistry.IContext<DocumentRegistry.IModel>,
387
+ state: DocumentRegistry.SaveState
388
+ ): void {
389
+ // ignore premature calls (before the editor was initialized)
390
+ if (this.virtualDocument === null) {
391
+ return;
392
+ }
393
+
394
+ if (state === 'completed') {
395
+ // note: must only be send to the appropriate connections as
396
+ // some servers (Julia) break if they receive save notification
397
+ // for a document that was not opened before, see:
398
+ // https://github.com/jupyter-lsp/jupyterlab-lsp/issues/490
399
+ const documentsToSave = [this.virtualDocument];
400
+
401
+ for (let virtualDocument of documentsToSave) {
402
+ let connection = this.connectionManager.connections.get(
403
+ virtualDocument.uri
404
+ );
405
+ if (!connection) {
406
+ continue;
407
+ }
408
+ connection.sendSaved(virtualDocument.documentInfo);
409
+ for (let foreign of virtualDocument.foreignDocuments.values()) {
410
+ documentsToSave.push(foreign);
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Connect the virtual document with the language server.
418
+ */
419
+ protected async onConnected(data: IDocumentConnectionData): Promise<void> {
420
+ let { virtualDocument } = data;
421
+
422
+ this._adapterConnected.emit(data);
423
+ this._isConnected = true;
424
+
425
+ try {
426
+ await this.updateDocuments();
427
+ } catch (reason) {
428
+ console.warn('Could not update documents', reason);
429
+ return;
430
+ }
431
+
432
+ // refresh the document on the LSP server
433
+ this.documentChanged(virtualDocument, virtualDocument, true);
434
+
435
+ data.connection.serverNotifications['$/logTrace'].connect(
436
+ (connection, message) => {
437
+ console.log(
438
+ data.connection.serverIdentifier,
439
+ 'trace',
440
+ virtualDocument.uri,
441
+ message
442
+ );
443
+ }
444
+ );
445
+
446
+ data.connection.serverNotifications['window/logMessage'].connect(
447
+ (connection, message) => {
448
+ console.log(connection.serverIdentifier + ': ' + message.message);
449
+ }
450
+ );
451
+
452
+ data.connection.serverNotifications['window/showMessage'].connect(
453
+ (connection, message) => {
454
+ void showDialog({
455
+ title: this.trans.__('Message from ') + connection.serverIdentifier,
456
+ body: message.message
457
+ });
458
+ }
459
+ );
460
+
461
+ data.connection.serverRequests['window/showMessageRequest'].setHandler(
462
+ async params => {
463
+ const actionItems = params.actions;
464
+ const buttons = actionItems
465
+ ? actionItems.map(action => {
466
+ return createButton({
467
+ label: action.title
468
+ });
469
+ })
470
+ : [createButton({ label: this.trans.__('Dismiss') })];
471
+ const result = await showDialog<IButton>({
472
+ title:
473
+ this.trans.__('Message from ') + data.connection.serverIdentifier,
474
+ body: params.message,
475
+ buttons: buttons
476
+ });
477
+ const choice = buttons.indexOf(result.button);
478
+ if (choice === -1) {
479
+ return null;
480
+ }
481
+ if (actionItems) {
482
+ return actionItems[choice];
483
+ }
484
+ return null;
485
+ }
486
+ );
487
+ }
488
+
489
+ /**
490
+ * Opens a connection for the document. The connection may or may
491
+ * not be initialized, yet, and depending on when this is called, the client
492
+ * may not be fully connected.
493
+ *
494
+ * @param virtualDocument a VirtualDocument
495
+ * @param sendOpen whether to open the document immediately
496
+ */
497
+ protected async connectDocument(
498
+ virtualDocument: VirtualDocument,
499
+ sendOpen = false
500
+ ): Promise<void> {
501
+ virtualDocument.foreignDocumentOpened.connect(
502
+ this.onForeignDocumentOpened,
503
+ this
504
+ );
505
+ const connectionContext = await this._connect(virtualDocument).catch(
506
+ console.error
507
+ );
508
+
509
+ if (connectionContext && connectionContext.connection) {
510
+ virtualDocument.changed.connect(this.documentChanged, this);
511
+ if (sendOpen) {
512
+ connectionContext.connection.sendOpenWhenReady(
513
+ virtualDocument.documentInfo
514
+ );
515
+ }
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Create the virtual document using current path and language.
521
+ */
522
+ protected initVirtual(): void {
523
+ const { model } = this.widget.context;
524
+ this._virtualDocument?.dispose();
525
+ this._virtualDocument = this.createVirtualDocument();
526
+ model.contentChanged.connect(this._onContentChanged, this);
527
+ }
528
+
529
+ /**
530
+ * Handler for opening a document contained in a parent document. The assumption
531
+ * is that the editor already exists for this, and as such the document
532
+ * should be queued for immediate opening.
533
+ *
534
+ * @param host the VirtualDocument that contains the VirtualDocument in another language
535
+ * @param context information about the foreign VirtualDocument
536
+ */
537
+ protected async onForeignDocumentOpened(
538
+ _: VirtualDocument,
539
+ context: Document.IForeignContext
540
+ ): Promise<void> {
541
+ const { foreignDocument } = context;
542
+
543
+ await this.connectDocument(foreignDocument, true);
544
+
545
+ foreignDocument.foreignDocumentClosed.connect(
546
+ this._onForeignDocumentClosed,
547
+ this
548
+ );
549
+ }
550
+
551
+ /**
552
+ * Signal emitted when the adapter is connected.
553
+ */
554
+ protected _adapterConnected: Signal<
555
+ WidgetLSPAdapter<T>,
556
+ IDocumentConnectionData
557
+ > = new Signal(this);
558
+
559
+ /**
560
+ * Signal emitted when the active editor have changed.
561
+ */
562
+ protected _activeEditorChanged: Signal<
563
+ WidgetLSPAdapter<T>,
564
+ IEditorChangedData
565
+ > = new Signal(this);
566
+
567
+ /**
568
+ * Signal emitted when an editor is changed.
569
+ */
570
+ protected _editorAdded: Signal<WidgetLSPAdapter<T>, IEditorChangedData> =
571
+ new Signal(this);
572
+
573
+ /**
574
+ * Signal emitted when an editor is removed.
575
+ */
576
+ protected _editorRemoved: Signal<WidgetLSPAdapter<T>, IEditorChangedData> =
577
+ new Signal(this);
578
+
579
+ /**
580
+ * Signal emitted when the adapter is disposed.
581
+ */
582
+ protected _disposed: Signal<WidgetLSPAdapter<T>, void> = new Signal(this);
583
+
584
+ private _isDisposed = false;
585
+
586
+ private readonly _connectionManager: ILSPDocumentConnectionManager;
587
+ private readonly _trans: TranslationBundle;
588
+
589
+ private _isConnected: boolean;
590
+ private _updateFinished: Promise<void>;
591
+ private _virtualDocument: VirtualDocument | null = null;
592
+
593
+ /**
594
+ * Callback called when a foreign document is closed,
595
+ * the associated signals with this virtual document
596
+ * are disconnected.
597
+ */
598
+ private _onForeignDocumentClosed(
599
+ _: VirtualDocument,
600
+ context: Document.IForeignContext
601
+ ): void {
602
+ const { foreignDocument } = context;
603
+ foreignDocument.foreignDocumentClosed.disconnect(
604
+ this._onForeignDocumentClosed,
605
+ this
606
+ );
607
+ foreignDocument.foreignDocumentOpened.disconnect(
608
+ this.onForeignDocumentOpened,
609
+ this
610
+ );
611
+ foreignDocument.changed.disconnect(this.documentChanged, this);
612
+ }
613
+
614
+ /**
615
+ * Detect the capabilities for the document type then
616
+ * open the websocket connection with the language server.
617
+ */
618
+ private async _connect(virtualDocument: VirtualDocument) {
619
+ let language = virtualDocument.language;
620
+
621
+ let capabilities: ClientCapabilities = {
622
+ textDocument: {
623
+ synchronization: {
624
+ dynamicRegistration: true,
625
+ willSave: false,
626
+ didSave: true,
627
+ willSaveWaitUntil: false
628
+ }
629
+ },
630
+ workspace: {
631
+ didChangeConfiguration: {
632
+ dynamicRegistration: true
633
+ }
634
+ }
635
+ };
636
+ capabilities = mergeWith(
637
+ capabilities,
638
+ this.options.featureManager.clientCapabilities()
639
+ );
640
+
641
+ let options: ISocketConnectionOptions = {
642
+ capabilities,
643
+ virtualDocument,
644
+ language,
645
+ hasLspSupportedFile: virtualDocument.hasLspSupportedFile
646
+ };
647
+
648
+ let connection = await this.connectionManager.connect(options);
649
+
650
+ if (connection) {
651
+ await this.onConnected({ virtualDocument, connection });
652
+
653
+ return {
654
+ connection,
655
+ virtualDocument
656
+ };
657
+ } else {
658
+ return undefined;
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Handle content changes and update all virtual documents after a change.
664
+ *
665
+ * #### Notes
666
+ * Update to the state of a notebook may be done without a notice on the
667
+ * CodeMirror level, e.g. when a cell is deleted. Therefore a
668
+ * JupyterLab-specific signal is watched instead.
669
+ *
670
+ * While by not using the change event of CodeMirror editors we lose an easy
671
+ * way to send selective (range) updates this can be still implemented by
672
+ * comparison of before/after states of the virtual documents, which is
673
+ * more resilient and editor-independent.
674
+ */
675
+ private async _onContentChanged(_: unknown) {
676
+ // Update the virtual documents.
677
+ // Sending the updates to LSP is out of scope here.
678
+ const promise = this.updateDocuments();
679
+ if (!promise) {
680
+ console.warn('Could not update documents');
681
+ return;
682
+ }
683
+ this._updateFinished = promise.catch(console.warn);
684
+ await this.updateFinished;
685
+ }
686
+ }