Skip to content

Commit 1409e5d

Browse files
committed
Create own notebook documents service
1 parent 4e96814 commit 1409e5d

File tree

3 files changed

+298
-5
lines changed

3 files changed

+298
-5
lines changed

packages/langium/src/lsp/default-lsp-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { DefaultNodeKindProvider } from './node-kind-provider.js';
2323
import { DefaultReferencesProvider } from './references-provider.js';
2424
import { DefaultRenameProvider } from './rename-provider.js';
2525
import { DefaultWorkspaceSymbolProvider } from './workspace-symbol-provider.js';
26-
import { NormalizedTextDocuments } from './normalized-text-documents.js';
26+
import { NormalizedNotebookDocuments, NormalizedTextDocuments } from './normalized-text-documents.js';
2727

2828
/**
2929
* Context required for creating the default language-specific dependency injection module.
@@ -96,7 +96,8 @@ export function createDefaultSharedLSPModule(context: DefaultSharedModuleContext
9696
FuzzyMatcher: () => new DefaultFuzzyMatcher(),
9797
},
9898
workspace: {
99-
TextDocuments: () => new NormalizedTextDocuments(TextDocument)
99+
TextDocuments: () => new NormalizedTextDocuments(TextDocument),
100+
NotebookDocuments: () => new NormalizedNotebookDocuments(TextDocument)
100101
}
101102
};
102103
}

packages/langium/src/lsp/lsp-services.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import type { SignatureHelpProvider } from './signature-help-provider.js';
3434
import type { TypeHierarchyProvider } from './type-hierarchy-provider.js';
3535
import type { TypeDefinitionProvider } from './type-provider.js';
3636
import type { WorkspaceSymbolProvider } from './workspace-symbol-provider.js';
37-
import type { TextDocuments } from './normalized-text-documents.js';
37+
import type { NotebookDocuments, TextDocuments } from './normalized-text-documents.js';
3838

3939
/**
4040
* Combined Core + LSP services of Langium (total services)
@@ -91,6 +91,7 @@ export type LangiumSharedLSPServices = {
9191
},
9292
readonly workspace: {
9393
readonly TextDocuments: TextDocuments<TextDocument>
94+
readonly NotebookDocuments: NotebookDocuments<TextDocument>
9495
}
9596
};
9697

packages/langium/src/lsp/normalized-text-documents.ts

Lines changed: 293 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,26 @@
66

77
import type {
88
Connection, DidOpenTextDocumentParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentsConfiguration, TextDocumentChangeEvent,
9-
TextDocumentWillSaveEvent, RequestHandler, TextEdit, Event, WillSaveTextDocumentParams, CancellationToken, DidSaveTextDocumentParams
9+
TextDocumentWillSaveEvent, RequestHandler, TextEdit, Event, WillSaveTextDocumentParams, CancellationToken, DidSaveTextDocumentParams,
10+
NotebookCell,
11+
NotebookDocument,
12+
DocumentUri,
13+
NotificationHandler
1014
} from 'vscode-languageserver';
1115
import { TextDocumentSyncKind, Disposable, Emitter } from 'vscode-languageserver';
1216
import type { URI } from '../utils/uri-utils.js';
1317
import { UriUtils } from '../utils/uri-utils.js';
18+
// For some reason, this isn't exported by vscode-languageserver
19+
import type { NotebookDocumentChangeEvent } from 'vscode-languageserver/lib/common/notebook.js';
20+
21+
export type TextDocumentConnection = Pick<Connection,
22+
'onDidOpenTextDocument' |
23+
'onDidChangeTextDocument' |
24+
'onDidCloseTextDocument' |
25+
'onWillSaveTextDocument' |
26+
'onWillSaveTextDocumentWaitUntil' |
27+
'onDidSaveTextDocument'
28+
>;
1429

1530
/**
1631
* A manager service that keeps track of all currently opened text documents.
@@ -91,7 +106,7 @@ export interface TextDocuments<T extends { uri: string }> {
91106
*
92107
* @param connection The connection to listen on.
93108
*/
94-
listen(connection: Connection): Disposable;
109+
listen(connection: TextDocumentConnection): Disposable;
95110
}
96111

97112
// Adapted from:
@@ -253,3 +268,279 @@ export class NormalizedTextDocuments<T extends { uri: string }> implements TextD
253268
return Disposable.create(() => { disposables.forEach(disposable => disposable.dispose()); });
254269
}
255270
}
271+
272+
export interface NotebookDocuments<T extends { uri: string }> {
273+
get cellTextDocuments(): TextDocuments<T>;
274+
getCellTextDocument(cell: NotebookCell): T | undefined;
275+
getNotebookDocument(uri: string | URI): NotebookDocument | undefined;
276+
getNotebookCell(uri: string | URI): NotebookCell | undefined;
277+
findNotebookDocumentForCell(cell: string | URI | NotebookCell): NotebookDocument | undefined;
278+
get onDidOpen(): Event<NotebookDocument>;
279+
get onDidSave(): Event<NotebookDocument>;
280+
get onDidChange(): Event<NotebookDocumentChangeEvent>;
281+
get onDidClose(): Event<NotebookDocument>;
282+
/**
283+
* Listens for `low level` notification on the given connection to
284+
* update the notebook documents managed by this instance.
285+
*
286+
* Please note that the connection only provides handlers not an event model. Therefore
287+
* listening on a connection will overwrite the following handlers on a connection:
288+
* `onDidOpenNotebookDocument`, `onDidChangeNotebookDocument`, `onDidSaveNotebookDocument`,
289+
* and `onDidCloseNotebookDocument`.
290+
*
291+
* @param connection The connection to listen on.
292+
*/
293+
listen(connection: Connection): Disposable;
294+
}
295+
296+
export class NormalizedNotebookDocuments<T extends { uri: DocumentUri }> implements NotebookDocuments<T> {
297+
298+
private readonly notebookDocuments = new Map<DocumentUri, NotebookDocument>();
299+
private readonly notebookCellMap = new Map<DocumentUri, [NotebookCell, NotebookDocument]>();
300+
301+
private readonly _onDidOpen = new Emitter<NotebookDocument>();
302+
private readonly _onDidSave = new Emitter<NotebookDocument>();
303+
private readonly _onDidChange = new Emitter<NotebookDocumentChangeEvent>();
304+
private readonly _onDidClose = new Emitter<NotebookDocument>();
305+
306+
private readonly _cellTextDocuments: TextDocuments<T>;
307+
308+
constructor(configurationOrTextDocuments: TextDocumentsConfiguration<T> | TextDocuments<T>) {
309+
if ('listen' in configurationOrTextDocuments) {
310+
this._cellTextDocuments = configurationOrTextDocuments;
311+
} else {
312+
this._cellTextDocuments = new NormalizedTextDocuments<T>(configurationOrTextDocuments);
313+
}
314+
}
315+
316+
get cellTextDocuments(): TextDocuments<T> {
317+
return this._cellTextDocuments;
318+
}
319+
320+
getCellTextDocument(cell: NotebookCell): T | undefined {
321+
return this._cellTextDocuments.get(cell.document);
322+
}
323+
324+
getNotebookDocument(uri: string | URI): NotebookDocument | undefined {
325+
return this.notebookDocuments.get(UriUtils.normalize(uri));
326+
}
327+
328+
getNotebookCell(uri: DocumentUri): NotebookCell | undefined {
329+
const value = this.notebookCellMap.get(uri);
330+
return value && value[0];
331+
}
332+
333+
findNotebookDocumentForCell(cell: string | URI | NotebookCell): NotebookDocument | undefined {
334+
const key = typeof cell === 'string' || 'scheme' in cell ? cell : cell.document;
335+
const value = this.notebookCellMap.get(UriUtils.normalize(key));
336+
return value && value[1];
337+
}
338+
339+
get onDidOpen(): Event<NotebookDocument> {
340+
return this._onDidOpen.event;
341+
}
342+
343+
get onDidSave(): Event<NotebookDocument> {
344+
return this._onDidSave.event;
345+
}
346+
347+
get onDidChange(): Event<NotebookDocumentChangeEvent> {
348+
return this._onDidChange.event;
349+
}
350+
351+
get onDidClose(): Event<NotebookDocument> {
352+
return this._onDidClose.event;
353+
}
354+
355+
/**
356+
* Listens for `low level` notification on the given connection to
357+
* update the notebook documents managed by this instance.
358+
*
359+
* Please note that the connection only provides handlers not an event model. Therefore
360+
* listening on a connection will overwrite the following handlers on a connection:
361+
* `onDidOpenNotebookDocument`, `onDidChangeNotebookDocument`, `onDidSaveNotebookDocument`,
362+
* and `onDidCloseNotebookDocument`.
363+
*
364+
* @param connection The connection to listen on.
365+
*/
366+
listen(connection: Connection): Disposable {
367+
const cellTextDocumentConnection = new CellTextDocumentConnection();
368+
const disposables: Disposable[] = [];
369+
370+
disposables.push(this.cellTextDocuments.listen(cellTextDocumentConnection));
371+
disposables.push(connection.notebooks.synchronization.onDidOpenNotebookDocument((params) => {
372+
const uri = UriUtils.normalize(params.notebookDocument.uri);
373+
this.notebookDocuments.set(uri, params.notebookDocument);
374+
for (const cellTextDocument of params.cellTextDocuments) {
375+
cellTextDocumentConnection.openTextDocument({ textDocument: cellTextDocument });
376+
}
377+
this.updateCellMap(params.notebookDocument);
378+
this._onDidOpen.fire(params.notebookDocument);
379+
}));
380+
disposables.push(connection.notebooks.synchronization.onDidChangeNotebookDocument((params) => {
381+
const uri = UriUtils.normalize(params.notebookDocument.uri);
382+
const notebookDocument = this.notebookDocuments.get(uri);
383+
if (notebookDocument === undefined) {
384+
return;
385+
}
386+
notebookDocument.version = params.notebookDocument.version;
387+
const oldMetadata = notebookDocument.metadata;
388+
let metadataChanged: boolean = false;
389+
const change = params.change;
390+
if (change.metadata !== undefined) {
391+
metadataChanged = true;
392+
notebookDocument.metadata = change.metadata;
393+
}
394+
395+
const opened: DocumentUri[] = [];
396+
const closed: DocumentUri[] = [];
397+
const data: Required<Required<Required<NotebookDocumentChangeEvent>['cells']>['changed']>['data'] = [];
398+
const text: DocumentUri[] = [];
399+
if (change.cells !== undefined) {
400+
const changedCells = change.cells;
401+
if (changedCells.structure !== undefined) {
402+
const array = changedCells.structure.array;
403+
notebookDocument.cells.splice(array.start, array.deleteCount, ...(array.cells !== undefined ? array.cells : []));
404+
// Additional open cell text documents.
405+
if (changedCells.structure.didOpen !== undefined) {
406+
for (const open of changedCells.structure.didOpen) {
407+
cellTextDocumentConnection.openTextDocument({ textDocument: open });
408+
opened.push(open.uri);
409+
}
410+
}
411+
// Additional closed cell test documents.
412+
if (changedCells.structure.didClose) {
413+
for (const close of changedCells.structure.didClose) {
414+
cellTextDocumentConnection.closeTextDocument({ textDocument: close });
415+
closed.push(close.uri);
416+
}
417+
}
418+
}
419+
if (changedCells.data !== undefined) {
420+
const cellUpdates: Map<string, NotebookCell> = new Map(changedCells.data.map(cell => [cell.document, cell]));
421+
for (let i = 0; i <= notebookDocument.cells.length; i++) {
422+
const change = cellUpdates.get(notebookDocument.cells[i].document);
423+
if (change !== undefined) {
424+
const old = notebookDocument.cells.splice(i, 1, change);
425+
data.push({ old: old[0], new: change });
426+
cellUpdates.delete(change.document);
427+
if (cellUpdates.size === 0) {
428+
break;
429+
}
430+
}
431+
}
432+
}
433+
if (changedCells.textContent !== undefined) {
434+
for (const cellTextDocument of changedCells.textContent) {
435+
cellTextDocumentConnection.changeTextDocument({ textDocument: cellTextDocument.document, contentChanges: cellTextDocument.changes });
436+
text.push(cellTextDocument.document.uri);
437+
}
438+
}
439+
}
440+
441+
// Update internal data structure.
442+
this.updateCellMap(notebookDocument);
443+
444+
const changeEvent: NotebookDocumentChangeEvent = { notebookDocument };
445+
if (metadataChanged) {
446+
changeEvent.metadata = { old: oldMetadata, new: notebookDocument.metadata };
447+
}
448+
449+
const added: NotebookCell[] = [];
450+
for (const open of opened) {
451+
added.push(this.getNotebookCell(open)!);
452+
}
453+
const removed: NotebookCell[] = [];
454+
for (const close of closed) {
455+
removed.push(this.getNotebookCell(close)!);
456+
}
457+
const textContent: NotebookCell[] = [];
458+
for (const change of text) {
459+
textContent.push(this.getNotebookCell(change)!);
460+
}
461+
if (added.length > 0 || removed.length > 0 || data.length > 0 || textContent.length > 0) {
462+
changeEvent.cells = { added, removed, changed: { data, textContent } };
463+
}
464+
if (changeEvent.metadata !== undefined || changeEvent.cells !== undefined) {
465+
this._onDidChange.fire(changeEvent);
466+
}
467+
}));
468+
disposables.push(connection.notebooks.synchronization.onDidSaveNotebookDocument((params) => {
469+
const notebookDocument = this.getNotebookDocument(params.notebookDocument.uri);
470+
if (notebookDocument === undefined) {
471+
return;
472+
}
473+
this._onDidSave.fire(notebookDocument);
474+
}));
475+
disposables.push(connection.notebooks.synchronization.onDidCloseNotebookDocument((params) => {
476+
const uri = UriUtils.normalize(params.notebookDocument.uri);
477+
const notebookDocument = this.notebookDocuments.get(uri);
478+
if (notebookDocument === undefined) {
479+
return;
480+
}
481+
this._onDidClose.fire(notebookDocument);
482+
for (const cellTextDocument of params.cellTextDocuments) {
483+
cellTextDocumentConnection.closeTextDocument({ textDocument: cellTextDocument });
484+
}
485+
this.notebookDocuments.delete(uri);
486+
for (const cell of notebookDocument.cells) {
487+
this.notebookCellMap.delete(cell.document);
488+
}
489+
}));
490+
return Disposable.create(() => { disposables.forEach(disposable => disposable.dispose()); });
491+
}
492+
493+
private updateCellMap(notebookDocument: NotebookDocument): void {
494+
for (const cell of notebookDocument.cells) {
495+
this.notebookCellMap.set(cell.document, [cell, notebookDocument]);
496+
}
497+
}
498+
}
499+
500+
class CellTextDocumentConnection implements TextDocumentConnection {
501+
502+
private static readonly NULL_DISPOSE = Object.freeze({ dispose: () => { } });
503+
504+
private openHandler: NotificationHandler<DidOpenTextDocumentParams> | undefined;
505+
private changeHandler: NotificationHandler<DidChangeTextDocumentParams> | undefined;
506+
private closeHandler: NotificationHandler<DidCloseTextDocumentParams> | undefined;
507+
508+
onDidOpenTextDocument(handler: NotificationHandler<DidOpenTextDocumentParams>): Disposable {
509+
this.openHandler = handler;
510+
return Disposable.create(() => { this.openHandler = undefined; });
511+
}
512+
513+
openTextDocument(params: DidOpenTextDocumentParams): void {
514+
this.openHandler && this.openHandler(params);
515+
}
516+
517+
onDidChangeTextDocument(handler: NotificationHandler<DidChangeTextDocumentParams>): Disposable {
518+
this.changeHandler = handler;
519+
return Disposable.create(() => { this.changeHandler = handler; });
520+
}
521+
522+
changeTextDocument(params: DidChangeTextDocumentParams): void {
523+
this.changeHandler && this.changeHandler(params);
524+
}
525+
526+
onDidCloseTextDocument(handler: NotificationHandler<DidCloseTextDocumentParams>): Disposable {
527+
this.closeHandler = handler;
528+
return Disposable.create(() => { this.closeHandler = undefined; });
529+
}
530+
531+
closeTextDocument(params: DidCloseTextDocumentParams): void {
532+
this.closeHandler && this.closeHandler(params);
533+
}
534+
535+
onWillSaveTextDocument(): Disposable {
536+
return CellTextDocumentConnection.NULL_DISPOSE;
537+
}
538+
539+
onWillSaveTextDocumentWaitUntil(): Disposable {
540+
return CellTextDocumentConnection.NULL_DISPOSE;
541+
}
542+
543+
onDidSaveTextDocument(): Disposable {
544+
return CellTextDocumentConnection.NULL_DISPOSE;
545+
}
546+
}

0 commit comments

Comments
 (0)