@theia/notebook 1.53.0-next.4 → 1.53.0-next.55

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.
Files changed (99) hide show
  1. package/README.md +30 -30
  2. package/lib/browser/contributions/cell-operations.d.ts.map +1 -1
  3. package/lib/browser/contributions/cell-operations.js +8 -1
  4. package/lib/browser/contributions/cell-operations.js.map +1 -1
  5. package/lib/browser/contributions/notebook-actions-contribution.d.ts +1 -0
  6. package/lib/browser/contributions/notebook-actions-contribution.d.ts.map +1 -1
  7. package/lib/browser/contributions/notebook-actions-contribution.js +31 -6
  8. package/lib/browser/contributions/notebook-actions-contribution.js.map +1 -1
  9. package/lib/browser/contributions/notebook-cell-actions-contribution.d.ts +4 -0
  10. package/lib/browser/contributions/notebook-cell-actions-contribution.d.ts.map +1 -1
  11. package/lib/browser/contributions/notebook-cell-actions-contribution.js +49 -12
  12. package/lib/browser/contributions/notebook-cell-actions-contribution.js.map +1 -1
  13. package/lib/browser/contributions/notebook-status-bar-contribution.d.ts +14 -0
  14. package/lib/browser/contributions/notebook-status-bar-contribution.d.ts.map +1 -0
  15. package/lib/browser/contributions/notebook-status-bar-contribution.js +75 -0
  16. package/lib/browser/contributions/notebook-status-bar-contribution.js.map +1 -0
  17. package/lib/browser/notebook-editor-widget.d.ts.map +1 -1
  18. package/lib/browser/notebook-editor-widget.js +3 -5
  19. package/lib/browser/notebook-editor-widget.js.map +1 -1
  20. package/lib/browser/notebook-frontend-module.d.ts.map +1 -1
  21. package/lib/browser/notebook-frontend-module.js +3 -0
  22. package/lib/browser/notebook-frontend-module.js.map +1 -1
  23. package/lib/browser/notebook-open-handler.d.ts +3 -2
  24. package/lib/browser/notebook-open-handler.d.ts.map +1 -1
  25. package/lib/browser/notebook-open-handler.js +12 -5
  26. package/lib/browser/notebook-open-handler.js.map +1 -1
  27. package/lib/browser/service/notebook-options.d.ts +1 -0
  28. package/lib/browser/service/notebook-options.d.ts.map +1 -1
  29. package/lib/browser/service/notebook-options.js +1 -0
  30. package/lib/browser/service/notebook-options.js.map +1 -1
  31. package/lib/browser/service/notebook-service.d.ts +1 -0
  32. package/lib/browser/service/notebook-service.d.ts.map +1 -1
  33. package/lib/browser/service/notebook-service.js +7 -0
  34. package/lib/browser/service/notebook-service.js.map +1 -1
  35. package/lib/browser/view/notebook-cell-editor.d.ts +1 -0
  36. package/lib/browser/view/notebook-cell-editor.d.ts.map +1 -1
  37. package/lib/browser/view/notebook-cell-editor.js +30 -16
  38. package/lib/browser/view/notebook-cell-editor.js.map +1 -1
  39. package/lib/browser/view/notebook-cell-list-view.d.ts +7 -4
  40. package/lib/browser/view/notebook-cell-list-view.d.ts.map +1 -1
  41. package/lib/browser/view/notebook-cell-list-view.js +39 -29
  42. package/lib/browser/view/notebook-cell-list-view.js.map +1 -1
  43. package/lib/browser/view-model/notebook-cell-model.d.ts +3 -0
  44. package/lib/browser/view-model/notebook-cell-model.d.ts.map +1 -1
  45. package/lib/browser/view-model/notebook-cell-model.js +5 -0
  46. package/lib/browser/view-model/notebook-cell-model.js.map +1 -1
  47. package/package.json +8 -8
  48. package/src/browser/contributions/cell-operations.ts +44 -39
  49. package/src/browser/contributions/notebook-actions-contribution.ts +379 -351
  50. package/src/browser/contributions/notebook-cell-actions-contribution.ts +525 -485
  51. package/src/browser/contributions/notebook-color-contribution.ts +268 -268
  52. package/src/browser/contributions/notebook-context-keys.ts +113 -113
  53. package/src/browser/contributions/notebook-label-provider-contribution.ts +85 -85
  54. package/src/browser/contributions/notebook-outline-contribution.ts +114 -114
  55. package/src/browser/contributions/notebook-output-action-contribution.ts +82 -82
  56. package/src/browser/contributions/notebook-preferences.ts +92 -92
  57. package/src/browser/contributions/notebook-status-bar-contribution.ts +77 -0
  58. package/src/browser/contributions/notebook-undo-redo-handler.ts +41 -41
  59. package/src/browser/index.ts +27 -27
  60. package/src/browser/notebook-cell-resource-resolver.ts +130 -130
  61. package/src/browser/notebook-editor-widget-factory.ts +82 -82
  62. package/src/browser/notebook-editor-widget.tsx +330 -331
  63. package/src/browser/notebook-frontend-module.ts +119 -115
  64. package/src/browser/notebook-open-handler.ts +120 -114
  65. package/src/browser/notebook-output-utils.ts +119 -119
  66. package/src/browser/notebook-renderer-registry.ts +85 -85
  67. package/src/browser/notebook-type-registry.ts +54 -54
  68. package/src/browser/notebook-types.ts +186 -186
  69. package/src/browser/renderers/cell-output-webview.ts +33 -33
  70. package/src/browser/service/notebook-clipboard-service.ts +43 -43
  71. package/src/browser/service/notebook-context-manager.ts +162 -162
  72. package/src/browser/service/notebook-editor-widget-service.ts +101 -101
  73. package/src/browser/service/notebook-execution-service.ts +139 -139
  74. package/src/browser/service/notebook-execution-state-service.ts +311 -311
  75. package/src/browser/service/notebook-kernel-history-service.ts +124 -124
  76. package/src/browser/service/notebook-kernel-quick-pick-service.ts +479 -479
  77. package/src/browser/service/notebook-kernel-service.ts +357 -357
  78. package/src/browser/service/notebook-model-resolver-service.ts +160 -160
  79. package/src/browser/service/notebook-monaco-text-model-service.ts +48 -48
  80. package/src/browser/service/notebook-options.ts +155 -154
  81. package/src/browser/service/notebook-renderer-messaging-service.ts +121 -121
  82. package/src/browser/service/notebook-service.ts +215 -209
  83. package/src/browser/style/index.css +483 -467
  84. package/src/browser/view/notebook-cell-editor.tsx +263 -247
  85. package/src/browser/view/notebook-cell-list-view.tsx +279 -259
  86. package/src/browser/view/notebook-cell-toolbar-factory.tsx +102 -102
  87. package/src/browser/view/notebook-cell-toolbar.tsx +74 -74
  88. package/src/browser/view/notebook-code-cell-view.tsx +350 -350
  89. package/src/browser/view/notebook-find-widget.tsx +335 -335
  90. package/src/browser/view/notebook-main-toolbar.tsx +235 -235
  91. package/src/browser/view/notebook-markdown-cell-view.tsx +208 -208
  92. package/src/browser/view/notebook-viewport-service.ts +61 -61
  93. package/src/browser/view-model/notebook-cell-model.ts +473 -466
  94. package/src/browser/view-model/notebook-cell-output-model.ts +100 -100
  95. package/src/browser/view-model/notebook-model.ts +550 -550
  96. package/src/common/index.ts +18 -18
  97. package/src/common/notebook-common.ts +337 -337
  98. package/src/common/notebook-protocol.ts +35 -35
  99. package/src/common/notebook-range.ts +30 -30
@@ -1,550 +1,550 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2023 Typefox and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import { Disposable, Emitter, Event, QueueableEmitter, Resource, URI } from '@theia/core';
18
- import { Saveable, SaveOptions } from '@theia/core/lib/browser';
19
- import {
20
- CellData, CellEditType, CellUri, NotebookCellInternalMetadata,
21
- NotebookCellMetadata,
22
- NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData,
23
- NotebookDocumentMetadata,
24
- } from '../../common';
25
- import {
26
- NotebookContentChangedEvent, NotebookModelWillAddRemoveEvent,
27
- CellEditOperation, NullablePartialNotebookCellInternalMetadata,
28
- NullablePartialNotebookCellMetadata
29
- } from '../notebook-types';
30
- import { NotebookSerializer } from '../service/notebook-service';
31
- import { FileService } from '@theia/filesystem/lib/browser/file-service';
32
- import { NotebookCellModel, NotebookCellModelFactory, NotebookCodeEditorFindMatch } from './notebook-cell-model';
33
- import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
34
- import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
35
- import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
36
- import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service';
37
- import { BinaryBuffer } from '@theia/core/lib/common/buffer';
38
- import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from '../view/notebook-find-widget';
39
-
40
- export const NotebookModelFactory = Symbol('NotebookModelFactory');
41
-
42
- export function createNotebookModelContainer(parent: interfaces.Container, props: NotebookModelProps): interfaces.Container {
43
- const child = parent.createChild();
44
-
45
- child.bind(NotebookModelProps).toConstantValue(props);
46
- child.bind(NotebookModel).toSelf();
47
-
48
- return child;
49
- }
50
-
51
- export const NotebookModelResolverServiceProxy = Symbol('NotebookModelResolverServiceProxy');
52
-
53
- const NotebookModelProps = Symbol('NotebookModelProps');
54
- export interface NotebookModelProps {
55
- data: NotebookData;
56
- resource: Resource;
57
- viewType: string;
58
- serializer: NotebookSerializer;
59
- }
60
-
61
- export interface SelectedCellChangeEvent {
62
- cell: NotebookCellModel | undefined;
63
- scrollIntoView: boolean;
64
- }
65
-
66
- @injectable()
67
- export class NotebookModel implements Saveable, Disposable {
68
-
69
- protected readonly onDirtyChangedEmitter = new Emitter<void>();
70
- readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
71
-
72
- protected readonly onDidSaveNotebookEmitter = new Emitter<void>();
73
- readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event;
74
-
75
- protected readonly onDidAddOrRemoveCellEmitter = new Emitter<NotebookModelWillAddRemoveEvent>();
76
- readonly onDidAddOrRemoveCell = this.onDidAddOrRemoveCellEmitter.event;
77
-
78
- protected readonly onDidChangeContentEmitter = new QueueableEmitter<NotebookContentChangedEvent>();
79
- readonly onDidChangeContent = this.onDidChangeContentEmitter.event;
80
-
81
- protected readonly onContentChangedEmitter = new Emitter<void>();
82
- readonly onContentChanged = this.onContentChangedEmitter.event;
83
-
84
- protected readonly onDidChangeSelectedCellEmitter = new Emitter<SelectedCellChangeEvent>();
85
- readonly onDidChangeSelectedCell = this.onDidChangeSelectedCellEmitter.event;
86
-
87
- protected readonly onDidDisposeEmitter = new Emitter<void>();
88
- readonly onDidDispose = this.onDidDisposeEmitter.event;
89
-
90
- get onDidChangeReadOnly(): Event<boolean | MarkdownString> {
91
- return this.props.resource.onDidChangeReadOnly ?? Event.None;
92
- }
93
-
94
- @inject(FileService)
95
- protected readonly fileService: FileService;
96
-
97
- @inject(UndoRedoService)
98
- protected readonly undoRedoService: UndoRedoService;
99
-
100
- @inject(NotebookModelProps)
101
- protected props: NotebookModelProps;
102
-
103
- @inject(NotebookCellModelFactory)
104
- protected cellModelFactory: NotebookCellModelFactory;
105
-
106
- @inject(NotebookModelResolverServiceProxy)
107
- protected modelResolverService: NotebookModelResolverService;
108
-
109
- protected nextHandle: number = 0;
110
-
111
- protected _dirty = false;
112
-
113
- set dirty(dirty: boolean) {
114
- const oldState = this._dirty;
115
- this._dirty = dirty;
116
- if (oldState !== dirty) {
117
- this.onDirtyChangedEmitter.fire();
118
- }
119
- }
120
-
121
- get dirty(): boolean {
122
- return this._dirty;
123
- }
124
-
125
- get readOnly(): boolean | MarkdownString {
126
- return this.props.resource.readOnly ?? false;
127
- }
128
-
129
- protected _selectedText = '';
130
-
131
- set selectedText(value: string) {
132
- this._selectedText = value;
133
- }
134
-
135
- get selectedText(): string {
136
- return this._selectedText;
137
- }
138
-
139
- selectedCell?: NotebookCellModel;
140
- protected dirtyCells: NotebookCellModel[] = [];
141
-
142
- cells: NotebookCellModel[];
143
-
144
- get uri(): URI {
145
- return this.props.resource.uri;
146
- }
147
-
148
- get viewType(): string {
149
- return this.props.viewType;
150
- }
151
-
152
- metadata: NotebookDocumentMetadata = {};
153
-
154
- @postConstruct()
155
- initialize(): void {
156
- this.dirty = false;
157
-
158
- this.cells = this.props.data.cells.map((cell, index) => this.cellModelFactory({
159
- uri: CellUri.generate(this.props.resource.uri, index),
160
- handle: index,
161
- source: cell.source,
162
- language: cell.language,
163
- cellKind: cell.cellKind,
164
- outputs: cell.outputs,
165
- metadata: cell.metadata,
166
- internalMetadata: cell.internalMetadata,
167
- collapseState: cell.collapseState
168
- }));
169
-
170
- this.addCellOutputListeners(this.cells);
171
-
172
- this.metadata = this.props.data.metadata;
173
-
174
- this.nextHandle = this.cells.length;
175
- }
176
-
177
- dispose(): void {
178
- this.onDirtyChangedEmitter.dispose();
179
- this.onDidSaveNotebookEmitter.dispose();
180
- this.onDidAddOrRemoveCellEmitter.dispose();
181
- this.onDidChangeContentEmitter.dispose();
182
- this.onDidChangeSelectedCellEmitter.dispose();
183
- this.cells.forEach(cell => cell.dispose());
184
- this.onDidDisposeEmitter.fire();
185
- }
186
-
187
- async save(options?: SaveOptions): Promise<void> {
188
- this.dirtyCells = [];
189
- this.dirty = false;
190
-
191
- const serializedNotebook = await this.serialize();
192
- this.fileService.writeFile(this.uri, serializedNotebook);
193
-
194
- this.onDidSaveNotebookEmitter.fire();
195
- }
196
-
197
- createSnapshot(): Saveable.Snapshot {
198
- return {
199
- read: () => JSON.stringify(this.getData())
200
- };
201
- }
202
-
203
- serialize(): Promise<BinaryBuffer> {
204
- return this.props.serializer.fromNotebook(this.getData());
205
- }
206
-
207
- async applySnapshot(snapshot: Saveable.Snapshot): Promise<void> {
208
- const rawData = Saveable.Snapshot.read(snapshot);
209
- if (!rawData) {
210
- throw new Error('could not read notebook snapshot');
211
- }
212
- const data = JSON.parse(rawData) as NotebookData;
213
- this.setData(data);
214
- }
215
-
216
- async revert(options?: Saveable.RevertOptions): Promise<void> {
217
- if (!options?.soft) {
218
- // Load the data from the file again
219
- try {
220
- const data = await this.modelResolverService.resolveExistingNotebookData(this.props.resource, this.props.viewType);
221
- this.setData(data, false);
222
- } catch (err) {
223
- console.error('Failed to revert notebook', err);
224
- }
225
- }
226
- this.dirty = false;
227
- }
228
-
229
- isDirty(): boolean {
230
- return this.dirty;
231
- }
232
-
233
- cellDirtyChanged(cell: NotebookCellModel, dirtyState: boolean): void {
234
- if (dirtyState) {
235
- this.dirtyCells.push(cell);
236
- } else {
237
- this.dirtyCells.splice(this.dirtyCells.indexOf(cell), 1);
238
- }
239
-
240
- this.dirty = this.dirtyCells.length > 0;
241
- // Only fire `onContentChangedEmitter` here, because `onDidChangeContentEmitter` is used for model level changes only
242
- // However, this event indicates that the content of a cell has changed
243
- this.onContentChangedEmitter.fire();
244
- }
245
-
246
- setData(data: NotebookData, markDirty = true): void {
247
- // Replace all cells in the model
248
- this.dirtyCells = [];
249
- this.replaceCells(0, this.cells.length, data.cells, false, false);
250
- this.metadata = data.metadata;
251
- this.dirty = markDirty;
252
- this.onDidChangeContentEmitter.fire();
253
- }
254
-
255
- getData(): NotebookData {
256
- return {
257
- cells: this.cells.map(cell => cell.getData()),
258
- metadata: this.metadata
259
- };
260
- }
261
-
262
- undo(): void {
263
- if (!this.readOnly) {
264
- this.undoRedoService.undo(this.uri);
265
- }
266
- }
267
-
268
- redo(): void {
269
- if (!this.readOnly) {
270
- this.undoRedoService.redo(this.uri);
271
- }
272
- }
273
-
274
- setSelectedCell(cell: NotebookCellModel, scrollIntoView?: boolean): void {
275
- if (this.selectedCell !== cell) {
276
- this.selectedCell = cell;
277
- this.onDidChangeSelectedCellEmitter.fire({ cell, scrollIntoView: scrollIntoView ?? true });
278
- }
279
- }
280
-
281
- private addCellOutputListeners(cells: NotebookCellModel[]): void {
282
- for (const cell of cells) {
283
- cell.onDidChangeOutputs(() => {
284
- this.dirty = true;
285
- });
286
- cell.onDidRequestCellEditChange(() => {
287
- this.onContentChangedEmitter.fire();
288
- });
289
- }
290
- }
291
-
292
- applyEdits(rawEdits: CellEditOperation[], computeUndoRedo: boolean): void {
293
- const editsWithDetails = rawEdits.map((edit, index) => {
294
- let cellIndex: number = -1;
295
- if ('index' in edit) {
296
- cellIndex = edit.index;
297
- } else if ('handle' in edit) {
298
- cellIndex = this.getCellIndexByHandle(edit.handle);
299
- } else if ('outputId' in edit) {
300
- cellIndex = this.cells.findIndex(cell => cell.outputs.some(output => output.outputId === edit.outputId));
301
- }
302
-
303
- return {
304
- edit,
305
- cellIndex,
306
- end: edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex,
307
- originalIndex: index
308
- };
309
- });
310
-
311
- for (const { edit, cellIndex } of editsWithDetails) {
312
- const cell = this.cells[cellIndex];
313
- if (cell) {
314
- this.cellDirtyChanged(cell, true);
315
- }
316
-
317
- let scrollIntoView = true;
318
- switch (edit.editType) {
319
- case CellEditType.Replace:
320
- this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo, true);
321
- scrollIntoView = edit.cells.length > 0;
322
- break;
323
- case CellEditType.Output: {
324
- if (edit.append) {
325
- cell.spliceNotebookCellOutputs({ deleteCount: 0, newOutputs: edit.outputs, start: cell.outputs.length });
326
- } else {
327
- // could definitely be more efficient. See vscode __spliceNotebookCellOutputs2
328
- // For now, just replace the whole existing output with the new output
329
- cell.spliceNotebookCellOutputs({ start: 0, deleteCount: edit.deleteCount ?? cell.outputs.length, newOutputs: edit.outputs });
330
- }
331
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Output, index: cellIndex, outputs: cell.outputs, append: edit.append ?? false });
332
- break;
333
- }
334
- case CellEditType.OutputItems:
335
- cell.changeOutputItems(edit.outputId, !!edit.append, edit.items);
336
- this.onDidChangeContentEmitter.queue({
337
- kind: NotebookCellsChangeType.OutputItem, index: cellIndex, outputItems: edit.items,
338
- outputId: edit.outputId, append: edit.append ?? false
339
- });
340
-
341
- break;
342
- case CellEditType.Metadata:
343
- this.changeCellMetadata(this.cells[cellIndex], edit.metadata, false);
344
- break;
345
- case CellEditType.PartialMetadata:
346
- this.changeCellMetadataPartial(this.cells[cellIndex], edit.metadata, false);
347
- break;
348
- case CellEditType.PartialInternalMetadata:
349
- this.changeCellInternalMetadataPartial(this.cells[cellIndex], edit.internalMetadata);
350
- break;
351
- case CellEditType.CellLanguage:
352
- this.changeCellLanguage(this.cells[cellIndex], edit.language, computeUndoRedo);
353
- break;
354
- case CellEditType.DocumentMetadata:
355
- this.updateNotebookMetadata(edit.metadata, false);
356
- break;
357
- case CellEditType.Move:
358
- this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo);
359
- break;
360
- }
361
-
362
- // if selected cell is affected update it because it can potentially have been replaced
363
- if (cell === this.selectedCell) {
364
- this.setSelectedCell(this.cells[Math.min(cellIndex, this.cells.length - 1)], scrollIntoView);
365
- }
366
- }
367
-
368
- this.fireContentChange();
369
- }
370
-
371
- protected fireContentChange(): void {
372
- this.onDidChangeContentEmitter.fire();
373
- this.onContentChangedEmitter.fire();
374
- }
375
-
376
- protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean, requestEdit: boolean): void {
377
- const cells = newCells.map(cell => {
378
- const handle = this.nextHandle++;
379
- return this.cellModelFactory({
380
- uri: CellUri.generate(this.uri, handle),
381
- handle: handle,
382
- source: cell.source,
383
- language: cell.language,
384
- cellKind: cell.cellKind,
385
- outputs: cell.outputs,
386
- metadata: cell.metadata,
387
- internalMetadata: cell.internalMetadata,
388
- collapseState: cell.collapseState
389
- });
390
- });
391
- this.addCellOutputListeners(cells);
392
-
393
- const changes: NotebookCellTextModelSplice<NotebookCellModel>[] = [{ start, deleteCount, newItems: cells }];
394
-
395
- const deletedCells = this.cells.splice(start, deleteCount, ...cells);
396
-
397
- for (const cell of deletedCells) {
398
- cell.dispose();
399
- }
400
-
401
- if (computeUndoRedo) {
402
- this.undoRedoService.pushElement(this.uri,
403
- async () => {
404
- this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false, false);
405
- this.fireContentChange();
406
- },
407
- async () => {
408
- this.replaceCells(start, deleteCount, newCells, false, false);
409
- this.fireContentChange();
410
- }
411
- );
412
- }
413
-
414
- this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle) });
415
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ModelChange, changes });
416
- if (cells.length > 0 && requestEdit) {
417
- this.setSelectedCell(cells[cells.length - 1]);
418
- cells[cells.length - 1].requestEdit();
419
- }
420
- }
421
-
422
- protected changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void {
423
- const newInternalMetadata: NotebookCellInternalMetadata = {
424
- ...cell.internalMetadata
425
- };
426
- let k: keyof NotebookCellInternalMetadata;
427
- // eslint-disable-next-line guard-for-in
428
- for (k in internalMetadata) {
429
- newInternalMetadata[k] = (internalMetadata[k] ?? undefined) as never;
430
- }
431
-
432
- cell.internalMetadata = newInternalMetadata;
433
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this.cells.indexOf(cell), internalMetadata: newInternalMetadata });
434
- }
435
-
436
- protected updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void {
437
- const oldMetadata = this.metadata;
438
- if (computeUndoRedo) {
439
- this.undoRedoService.pushElement(this.uri,
440
- async () => this.updateNotebookMetadata(oldMetadata, false),
441
- async () => this.updateNotebookMetadata(metadata, false)
442
- );
443
- }
444
-
445
- this.metadata = metadata;
446
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata });
447
- }
448
-
449
- protected changeCellMetadataPartial(cell: NotebookCellModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean): void {
450
- const newMetadata: NotebookCellMetadata = {
451
- ...cell.metadata
452
- };
453
- let k: keyof NullablePartialNotebookCellMetadata;
454
- // eslint-disable-next-line guard-for-in
455
- for (k in metadata) {
456
- const value = metadata[k] ?? undefined;
457
- newMetadata[k] = value as unknown;
458
- }
459
-
460
- this.changeCellMetadata(cell, newMetadata, computeUndoRedo);
461
- }
462
-
463
- protected changeCellMetadata(cell: NotebookCellModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean): void {
464
- const triggerDirtyChange = this.isCellMetadataChanged(cell.metadata, metadata);
465
-
466
- if (triggerDirtyChange) {
467
- if (computeUndoRedo) {
468
- const oldMetadata = cell.metadata;
469
- cell.metadata = metadata;
470
- this.undoRedoService.pushElement(this.uri,
471
- async () => { cell.metadata = oldMetadata; },
472
- async () => { cell.metadata = metadata; }
473
- );
474
- }
475
- }
476
-
477
- cell.metadata = metadata;
478
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellMetadata, index: this.cells.indexOf(cell), metadata: cell.metadata });
479
- }
480
-
481
- protected changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void {
482
- if (cell.language === languageId) {
483
- return;
484
- }
485
-
486
- cell.language = languageId;
487
-
488
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId });
489
- }
490
-
491
- protected moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean {
492
- if (computeUndoRedo) {
493
- this.undoRedoService.pushElement(this.uri,
494
- async () => {
495
- this.moveCellToIndex(toIndex, length, fromIndex, false);
496
- this.fireContentChange();
497
- },
498
- async () => {
499
- this.moveCellToIndex(fromIndex, length, toIndex, false);
500
- this.fireContentChange();
501
- }
502
- );
503
- }
504
-
505
- const cells = this.cells.splice(fromIndex, length);
506
- this.cells.splice(toIndex, 0, ...cells);
507
- this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Move, index: fromIndex, length, newIdx: toIndex, cells });
508
-
509
- return true;
510
- }
511
-
512
- protected getCellIndexByHandle(handle: number): number {
513
- return this.cells.findIndex(c => c.handle === handle);
514
- }
515
-
516
- protected isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata): boolean {
517
- const keys = new Set<keyof NotebookCellMetadata>([...Object.keys(a || {}), ...Object.keys(b || {})]);
518
- for (const key of keys) {
519
- if (a[key] !== b[key]) {
520
- return true;
521
- }
522
- }
523
-
524
- return false;
525
- }
526
-
527
- findMatches(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] {
528
- const matches: NotebookEditorFindMatch[] = [];
529
- for (const cell of this.cells) {
530
- matches.push(...cell.findMatches(options));
531
- }
532
- return matches;
533
- }
534
-
535
- replaceAll(matches: NotebookEditorFindMatch[], text: string): void {
536
- const matchMap = new Map<NotebookCellModel, NotebookCodeEditorFindMatch[]>();
537
- for (const match of matches) {
538
- if (match instanceof NotebookCodeEditorFindMatch) {
539
- if (!matchMap.has(match.cell)) {
540
- matchMap.set(match.cell, []);
541
- }
542
- matchMap.get(match.cell)?.push(match);
543
- }
544
- }
545
- for (const [cell, cellMatches] of matchMap) {
546
- cell.replaceAll(cellMatches, text);
547
- }
548
- }
549
-
550
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2023 Typefox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { Disposable, Emitter, Event, QueueableEmitter, Resource, URI } from '@theia/core';
18
+ import { Saveable, SaveOptions } from '@theia/core/lib/browser';
19
+ import {
20
+ CellData, CellEditType, CellUri, NotebookCellInternalMetadata,
21
+ NotebookCellMetadata,
22
+ NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData,
23
+ NotebookDocumentMetadata,
24
+ } from '../../common';
25
+ import {
26
+ NotebookContentChangedEvent, NotebookModelWillAddRemoveEvent,
27
+ CellEditOperation, NullablePartialNotebookCellInternalMetadata,
28
+ NullablePartialNotebookCellMetadata
29
+ } from '../notebook-types';
30
+ import { NotebookSerializer } from '../service/notebook-service';
31
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
32
+ import { NotebookCellModel, NotebookCellModelFactory, NotebookCodeEditorFindMatch } from './notebook-cell-model';
33
+ import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
34
+ import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
35
+ import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
36
+ import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service';
37
+ import { BinaryBuffer } from '@theia/core/lib/common/buffer';
38
+ import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from '../view/notebook-find-widget';
39
+
40
+ export const NotebookModelFactory = Symbol('NotebookModelFactory');
41
+
42
+ export function createNotebookModelContainer(parent: interfaces.Container, props: NotebookModelProps): interfaces.Container {
43
+ const child = parent.createChild();
44
+
45
+ child.bind(NotebookModelProps).toConstantValue(props);
46
+ child.bind(NotebookModel).toSelf();
47
+
48
+ return child;
49
+ }
50
+
51
+ export const NotebookModelResolverServiceProxy = Symbol('NotebookModelResolverServiceProxy');
52
+
53
+ const NotebookModelProps = Symbol('NotebookModelProps');
54
+ export interface NotebookModelProps {
55
+ data: NotebookData;
56
+ resource: Resource;
57
+ viewType: string;
58
+ serializer: NotebookSerializer;
59
+ }
60
+
61
+ export interface SelectedCellChangeEvent {
62
+ cell: NotebookCellModel | undefined;
63
+ scrollIntoView: boolean;
64
+ }
65
+
66
+ @injectable()
67
+ export class NotebookModel implements Saveable, Disposable {
68
+
69
+ protected readonly onDirtyChangedEmitter = new Emitter<void>();
70
+ readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
71
+
72
+ protected readonly onDidSaveNotebookEmitter = new Emitter<void>();
73
+ readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event;
74
+
75
+ protected readonly onDidAddOrRemoveCellEmitter = new Emitter<NotebookModelWillAddRemoveEvent>();
76
+ readonly onDidAddOrRemoveCell = this.onDidAddOrRemoveCellEmitter.event;
77
+
78
+ protected readonly onDidChangeContentEmitter = new QueueableEmitter<NotebookContentChangedEvent>();
79
+ readonly onDidChangeContent = this.onDidChangeContentEmitter.event;
80
+
81
+ protected readonly onContentChangedEmitter = new Emitter<void>();
82
+ readonly onContentChanged = this.onContentChangedEmitter.event;
83
+
84
+ protected readonly onDidChangeSelectedCellEmitter = new Emitter<SelectedCellChangeEvent>();
85
+ readonly onDidChangeSelectedCell = this.onDidChangeSelectedCellEmitter.event;
86
+
87
+ protected readonly onDidDisposeEmitter = new Emitter<void>();
88
+ readonly onDidDispose = this.onDidDisposeEmitter.event;
89
+
90
+ get onDidChangeReadOnly(): Event<boolean | MarkdownString> {
91
+ return this.props.resource.onDidChangeReadOnly ?? Event.None;
92
+ }
93
+
94
+ @inject(FileService)
95
+ protected readonly fileService: FileService;
96
+
97
+ @inject(UndoRedoService)
98
+ protected readonly undoRedoService: UndoRedoService;
99
+
100
+ @inject(NotebookModelProps)
101
+ protected props: NotebookModelProps;
102
+
103
+ @inject(NotebookCellModelFactory)
104
+ protected cellModelFactory: NotebookCellModelFactory;
105
+
106
+ @inject(NotebookModelResolverServiceProxy)
107
+ protected modelResolverService: NotebookModelResolverService;
108
+
109
+ protected nextHandle: number = 0;
110
+
111
+ protected _dirty = false;
112
+
113
+ set dirty(dirty: boolean) {
114
+ const oldState = this._dirty;
115
+ this._dirty = dirty;
116
+ if (oldState !== dirty) {
117
+ this.onDirtyChangedEmitter.fire();
118
+ }
119
+ }
120
+
121
+ get dirty(): boolean {
122
+ return this._dirty;
123
+ }
124
+
125
+ get readOnly(): boolean | MarkdownString {
126
+ return this.props.resource.readOnly ?? false;
127
+ }
128
+
129
+ protected _selectedText = '';
130
+
131
+ set selectedText(value: string) {
132
+ this._selectedText = value;
133
+ }
134
+
135
+ get selectedText(): string {
136
+ return this._selectedText;
137
+ }
138
+
139
+ selectedCell?: NotebookCellModel;
140
+ protected dirtyCells: NotebookCellModel[] = [];
141
+
142
+ cells: NotebookCellModel[];
143
+
144
+ get uri(): URI {
145
+ return this.props.resource.uri;
146
+ }
147
+
148
+ get viewType(): string {
149
+ return this.props.viewType;
150
+ }
151
+
152
+ metadata: NotebookDocumentMetadata = {};
153
+
154
+ @postConstruct()
155
+ initialize(): void {
156
+ this.dirty = false;
157
+
158
+ this.cells = this.props.data.cells.map((cell, index) => this.cellModelFactory({
159
+ uri: CellUri.generate(this.props.resource.uri, index),
160
+ handle: index,
161
+ source: cell.source,
162
+ language: cell.language,
163
+ cellKind: cell.cellKind,
164
+ outputs: cell.outputs,
165
+ metadata: cell.metadata,
166
+ internalMetadata: cell.internalMetadata,
167
+ collapseState: cell.collapseState
168
+ }));
169
+
170
+ this.addCellOutputListeners(this.cells);
171
+
172
+ this.metadata = this.props.data.metadata;
173
+
174
+ this.nextHandle = this.cells.length;
175
+ }
176
+
177
+ dispose(): void {
178
+ this.onDirtyChangedEmitter.dispose();
179
+ this.onDidSaveNotebookEmitter.dispose();
180
+ this.onDidAddOrRemoveCellEmitter.dispose();
181
+ this.onDidChangeContentEmitter.dispose();
182
+ this.onDidChangeSelectedCellEmitter.dispose();
183
+ this.cells.forEach(cell => cell.dispose());
184
+ this.onDidDisposeEmitter.fire();
185
+ }
186
+
187
+ async save(options?: SaveOptions): Promise<void> {
188
+ this.dirtyCells = [];
189
+ this.dirty = false;
190
+
191
+ const serializedNotebook = await this.serialize();
192
+ this.fileService.writeFile(this.uri, serializedNotebook);
193
+
194
+ this.onDidSaveNotebookEmitter.fire();
195
+ }
196
+
197
+ createSnapshot(): Saveable.Snapshot {
198
+ return {
199
+ read: () => JSON.stringify(this.getData())
200
+ };
201
+ }
202
+
203
+ serialize(): Promise<BinaryBuffer> {
204
+ return this.props.serializer.fromNotebook(this.getData());
205
+ }
206
+
207
+ async applySnapshot(snapshot: Saveable.Snapshot): Promise<void> {
208
+ const rawData = Saveable.Snapshot.read(snapshot);
209
+ if (!rawData) {
210
+ throw new Error('could not read notebook snapshot');
211
+ }
212
+ const data = JSON.parse(rawData) as NotebookData;
213
+ this.setData(data);
214
+ }
215
+
216
+ async revert(options?: Saveable.RevertOptions): Promise<void> {
217
+ if (!options?.soft) {
218
+ // Load the data from the file again
219
+ try {
220
+ const data = await this.modelResolverService.resolveExistingNotebookData(this.props.resource, this.props.viewType);
221
+ this.setData(data, false);
222
+ } catch (err) {
223
+ console.error('Failed to revert notebook', err);
224
+ }
225
+ }
226
+ this.dirty = false;
227
+ }
228
+
229
+ isDirty(): boolean {
230
+ return this.dirty;
231
+ }
232
+
233
+ cellDirtyChanged(cell: NotebookCellModel, dirtyState: boolean): void {
234
+ if (dirtyState) {
235
+ this.dirtyCells.push(cell);
236
+ } else {
237
+ this.dirtyCells.splice(this.dirtyCells.indexOf(cell), 1);
238
+ }
239
+
240
+ this.dirty = this.dirtyCells.length > 0;
241
+ // Only fire `onContentChangedEmitter` here, because `onDidChangeContentEmitter` is used for model level changes only
242
+ // However, this event indicates that the content of a cell has changed
243
+ this.onContentChangedEmitter.fire();
244
+ }
245
+
246
+ setData(data: NotebookData, markDirty = true): void {
247
+ // Replace all cells in the model
248
+ this.dirtyCells = [];
249
+ this.replaceCells(0, this.cells.length, data.cells, false, false);
250
+ this.metadata = data.metadata;
251
+ this.dirty = markDirty;
252
+ this.onDidChangeContentEmitter.fire();
253
+ }
254
+
255
+ getData(): NotebookData {
256
+ return {
257
+ cells: this.cells.map(cell => cell.getData()),
258
+ metadata: this.metadata
259
+ };
260
+ }
261
+
262
+ undo(): void {
263
+ if (!this.readOnly) {
264
+ this.undoRedoService.undo(this.uri);
265
+ }
266
+ }
267
+
268
+ redo(): void {
269
+ if (!this.readOnly) {
270
+ this.undoRedoService.redo(this.uri);
271
+ }
272
+ }
273
+
274
+ setSelectedCell(cell: NotebookCellModel, scrollIntoView?: boolean): void {
275
+ if (this.selectedCell !== cell) {
276
+ this.selectedCell = cell;
277
+ this.onDidChangeSelectedCellEmitter.fire({ cell, scrollIntoView: scrollIntoView ?? true });
278
+ }
279
+ }
280
+
281
+ private addCellOutputListeners(cells: NotebookCellModel[]): void {
282
+ for (const cell of cells) {
283
+ cell.onDidChangeOutputs(() => {
284
+ this.dirty = true;
285
+ });
286
+ cell.onDidRequestCellEditChange(() => {
287
+ this.onContentChangedEmitter.fire();
288
+ });
289
+ }
290
+ }
291
+
292
+ applyEdits(rawEdits: CellEditOperation[], computeUndoRedo: boolean): void {
293
+ const editsWithDetails = rawEdits.map((edit, index) => {
294
+ let cellIndex: number = -1;
295
+ if ('index' in edit) {
296
+ cellIndex = edit.index;
297
+ } else if ('handle' in edit) {
298
+ cellIndex = this.getCellIndexByHandle(edit.handle);
299
+ } else if ('outputId' in edit) {
300
+ cellIndex = this.cells.findIndex(cell => cell.outputs.some(output => output.outputId === edit.outputId));
301
+ }
302
+
303
+ return {
304
+ edit,
305
+ cellIndex,
306
+ end: edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex,
307
+ originalIndex: index
308
+ };
309
+ });
310
+
311
+ for (const { edit, cellIndex } of editsWithDetails) {
312
+ const cell = this.cells[cellIndex];
313
+ if (cell) {
314
+ this.cellDirtyChanged(cell, true);
315
+ }
316
+
317
+ let scrollIntoView = true;
318
+ switch (edit.editType) {
319
+ case CellEditType.Replace:
320
+ this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo, true);
321
+ scrollIntoView = edit.cells.length > 0;
322
+ break;
323
+ case CellEditType.Output: {
324
+ if (edit.append) {
325
+ cell.spliceNotebookCellOutputs({ deleteCount: 0, newOutputs: edit.outputs, start: cell.outputs.length });
326
+ } else {
327
+ // could definitely be more efficient. See vscode __spliceNotebookCellOutputs2
328
+ // For now, just replace the whole existing output with the new output
329
+ cell.spliceNotebookCellOutputs({ start: 0, deleteCount: edit.deleteCount ?? cell.outputs.length, newOutputs: edit.outputs });
330
+ }
331
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Output, index: cellIndex, outputs: cell.outputs, append: edit.append ?? false });
332
+ break;
333
+ }
334
+ case CellEditType.OutputItems:
335
+ cell.changeOutputItems(edit.outputId, !!edit.append, edit.items);
336
+ this.onDidChangeContentEmitter.queue({
337
+ kind: NotebookCellsChangeType.OutputItem, index: cellIndex, outputItems: edit.items,
338
+ outputId: edit.outputId, append: edit.append ?? false
339
+ });
340
+
341
+ break;
342
+ case CellEditType.Metadata:
343
+ this.changeCellMetadata(this.cells[cellIndex], edit.metadata, false);
344
+ break;
345
+ case CellEditType.PartialMetadata:
346
+ this.changeCellMetadataPartial(this.cells[cellIndex], edit.metadata, false);
347
+ break;
348
+ case CellEditType.PartialInternalMetadata:
349
+ this.changeCellInternalMetadataPartial(this.cells[cellIndex], edit.internalMetadata);
350
+ break;
351
+ case CellEditType.CellLanguage:
352
+ this.changeCellLanguage(this.cells[cellIndex], edit.language, computeUndoRedo);
353
+ break;
354
+ case CellEditType.DocumentMetadata:
355
+ this.updateNotebookMetadata(edit.metadata, false);
356
+ break;
357
+ case CellEditType.Move:
358
+ this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo);
359
+ break;
360
+ }
361
+
362
+ // if selected cell is affected update it because it can potentially have been replaced
363
+ if (cell === this.selectedCell) {
364
+ this.setSelectedCell(this.cells[Math.min(cellIndex, this.cells.length - 1)], scrollIntoView);
365
+ }
366
+ }
367
+
368
+ this.fireContentChange();
369
+ }
370
+
371
+ protected fireContentChange(): void {
372
+ this.onDidChangeContentEmitter.fire();
373
+ this.onContentChangedEmitter.fire();
374
+ }
375
+
376
+ protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean, requestEdit: boolean): void {
377
+ const cells = newCells.map(cell => {
378
+ const handle = this.nextHandle++;
379
+ return this.cellModelFactory({
380
+ uri: CellUri.generate(this.uri, handle),
381
+ handle: handle,
382
+ source: cell.source,
383
+ language: cell.language,
384
+ cellKind: cell.cellKind,
385
+ outputs: cell.outputs,
386
+ metadata: cell.metadata,
387
+ internalMetadata: cell.internalMetadata,
388
+ collapseState: cell.collapseState
389
+ });
390
+ });
391
+ this.addCellOutputListeners(cells);
392
+
393
+ const changes: NotebookCellTextModelSplice<NotebookCellModel>[] = [{ start, deleteCount, newItems: cells }];
394
+
395
+ const deletedCells = this.cells.splice(start, deleteCount, ...cells);
396
+
397
+ for (const cell of deletedCells) {
398
+ cell.dispose();
399
+ }
400
+
401
+ if (computeUndoRedo) {
402
+ this.undoRedoService.pushElement(this.uri,
403
+ async () => {
404
+ this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false, false);
405
+ this.fireContentChange();
406
+ },
407
+ async () => {
408
+ this.replaceCells(start, deleteCount, newCells, false, false);
409
+ this.fireContentChange();
410
+ }
411
+ );
412
+ }
413
+
414
+ this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle) });
415
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ModelChange, changes });
416
+ if (cells.length > 0 && requestEdit) {
417
+ this.setSelectedCell(cells[cells.length - 1]);
418
+ cells[cells.length - 1].requestEdit();
419
+ }
420
+ }
421
+
422
+ protected changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void {
423
+ const newInternalMetadata: NotebookCellInternalMetadata = {
424
+ ...cell.internalMetadata
425
+ };
426
+ let k: keyof NotebookCellInternalMetadata;
427
+ // eslint-disable-next-line guard-for-in
428
+ for (k in internalMetadata) {
429
+ newInternalMetadata[k] = (internalMetadata[k] ?? undefined) as never;
430
+ }
431
+
432
+ cell.internalMetadata = newInternalMetadata;
433
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this.cells.indexOf(cell), internalMetadata: newInternalMetadata });
434
+ }
435
+
436
+ protected updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void {
437
+ const oldMetadata = this.metadata;
438
+ if (computeUndoRedo) {
439
+ this.undoRedoService.pushElement(this.uri,
440
+ async () => this.updateNotebookMetadata(oldMetadata, false),
441
+ async () => this.updateNotebookMetadata(metadata, false)
442
+ );
443
+ }
444
+
445
+ this.metadata = metadata;
446
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata });
447
+ }
448
+
449
+ protected changeCellMetadataPartial(cell: NotebookCellModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean): void {
450
+ const newMetadata: NotebookCellMetadata = {
451
+ ...cell.metadata
452
+ };
453
+ let k: keyof NullablePartialNotebookCellMetadata;
454
+ // eslint-disable-next-line guard-for-in
455
+ for (k in metadata) {
456
+ const value = metadata[k] ?? undefined;
457
+ newMetadata[k] = value as unknown;
458
+ }
459
+
460
+ this.changeCellMetadata(cell, newMetadata, computeUndoRedo);
461
+ }
462
+
463
+ protected changeCellMetadata(cell: NotebookCellModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean): void {
464
+ const triggerDirtyChange = this.isCellMetadataChanged(cell.metadata, metadata);
465
+
466
+ if (triggerDirtyChange) {
467
+ if (computeUndoRedo) {
468
+ const oldMetadata = cell.metadata;
469
+ cell.metadata = metadata;
470
+ this.undoRedoService.pushElement(this.uri,
471
+ async () => { cell.metadata = oldMetadata; },
472
+ async () => { cell.metadata = metadata; }
473
+ );
474
+ }
475
+ }
476
+
477
+ cell.metadata = metadata;
478
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellMetadata, index: this.cells.indexOf(cell), metadata: cell.metadata });
479
+ }
480
+
481
+ protected changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void {
482
+ if (cell.language === languageId) {
483
+ return;
484
+ }
485
+
486
+ cell.language = languageId;
487
+
488
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId });
489
+ }
490
+
491
+ protected moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean {
492
+ if (computeUndoRedo) {
493
+ this.undoRedoService.pushElement(this.uri,
494
+ async () => {
495
+ this.moveCellToIndex(toIndex, length, fromIndex, false);
496
+ this.fireContentChange();
497
+ },
498
+ async () => {
499
+ this.moveCellToIndex(fromIndex, length, toIndex, false);
500
+ this.fireContentChange();
501
+ }
502
+ );
503
+ }
504
+
505
+ const cells = this.cells.splice(fromIndex, length);
506
+ this.cells.splice(toIndex, 0, ...cells);
507
+ this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Move, index: fromIndex, length, newIdx: toIndex, cells });
508
+
509
+ return true;
510
+ }
511
+
512
+ protected getCellIndexByHandle(handle: number): number {
513
+ return this.cells.findIndex(c => c.handle === handle);
514
+ }
515
+
516
+ protected isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata): boolean {
517
+ const keys = new Set<keyof NotebookCellMetadata>([...Object.keys(a || {}), ...Object.keys(b || {})]);
518
+ for (const key of keys) {
519
+ if (a[key] !== b[key]) {
520
+ return true;
521
+ }
522
+ }
523
+
524
+ return false;
525
+ }
526
+
527
+ findMatches(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] {
528
+ const matches: NotebookEditorFindMatch[] = [];
529
+ for (const cell of this.cells) {
530
+ matches.push(...cell.findMatches(options));
531
+ }
532
+ return matches;
533
+ }
534
+
535
+ replaceAll(matches: NotebookEditorFindMatch[], text: string): void {
536
+ const matchMap = new Map<NotebookCellModel, NotebookCodeEditorFindMatch[]>();
537
+ for (const match of matches) {
538
+ if (match instanceof NotebookCodeEditorFindMatch) {
539
+ if (!matchMap.has(match.cell)) {
540
+ matchMap.set(match.cell, []);
541
+ }
542
+ matchMap.get(match.cell)?.push(match);
543
+ }
544
+ }
545
+ for (const [cell, cellMatches] of matchMap) {
546
+ cell.replaceAll(cellMatches, text);
547
+ }
548
+ }
549
+
550
+ }