@theia/notebook 1.53.0-next.5 → 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 (88) hide show
  1. package/README.md +30 -30
  2. package/lib/browser/contributions/notebook-actions-contribution.d.ts +1 -0
  3. package/lib/browser/contributions/notebook-actions-contribution.d.ts.map +1 -1
  4. package/lib/browser/contributions/notebook-actions-contribution.js +31 -5
  5. package/lib/browser/contributions/notebook-actions-contribution.js.map +1 -1
  6. package/lib/browser/contributions/notebook-cell-actions-contribution.d.ts +2 -0
  7. package/lib/browser/contributions/notebook-cell-actions-contribution.d.ts.map +1 -1
  8. package/lib/browser/contributions/notebook-cell-actions-contribution.js +41 -7
  9. package/lib/browser/contributions/notebook-cell-actions-contribution.js.map +1 -1
  10. package/lib/browser/contributions/notebook-status-bar-contribution.d.ts +14 -0
  11. package/lib/browser/contributions/notebook-status-bar-contribution.d.ts.map +1 -0
  12. package/lib/browser/contributions/notebook-status-bar-contribution.js +75 -0
  13. package/lib/browser/contributions/notebook-status-bar-contribution.js.map +1 -0
  14. package/lib/browser/notebook-editor-widget.d.ts.map +1 -1
  15. package/lib/browser/notebook-editor-widget.js +3 -5
  16. package/lib/browser/notebook-editor-widget.js.map +1 -1
  17. package/lib/browser/notebook-frontend-module.d.ts.map +1 -1
  18. package/lib/browser/notebook-frontend-module.js +3 -0
  19. package/lib/browser/notebook-frontend-module.js.map +1 -1
  20. package/lib/browser/notebook-open-handler.d.ts +3 -2
  21. package/lib/browser/notebook-open-handler.d.ts.map +1 -1
  22. package/lib/browser/notebook-open-handler.js +12 -5
  23. package/lib/browser/notebook-open-handler.js.map +1 -1
  24. package/lib/browser/view/notebook-cell-editor.d.ts +1 -0
  25. package/lib/browser/view/notebook-cell-editor.d.ts.map +1 -1
  26. package/lib/browser/view/notebook-cell-editor.js +30 -16
  27. package/lib/browser/view/notebook-cell-editor.js.map +1 -1
  28. package/lib/browser/view/notebook-cell-list-view.d.ts +6 -4
  29. package/lib/browser/view/notebook-cell-list-view.d.ts.map +1 -1
  30. package/lib/browser/view/notebook-cell-list-view.js +20 -13
  31. package/lib/browser/view/notebook-cell-list-view.js.map +1 -1
  32. package/lib/browser/view-model/notebook-cell-model.d.ts +3 -0
  33. package/lib/browser/view-model/notebook-cell-model.d.ts.map +1 -1
  34. package/lib/browser/view-model/notebook-cell-model.js +5 -0
  35. package/lib/browser/view-model/notebook-cell-model.js.map +1 -1
  36. package/package.json +8 -8
  37. package/src/browser/contributions/cell-operations.ts +44 -44
  38. package/src/browser/contributions/notebook-actions-contribution.ts +379 -350
  39. package/src/browser/contributions/notebook-cell-actions-contribution.ts +525 -487
  40. package/src/browser/contributions/notebook-color-contribution.ts +268 -268
  41. package/src/browser/contributions/notebook-context-keys.ts +113 -113
  42. package/src/browser/contributions/notebook-label-provider-contribution.ts +85 -85
  43. package/src/browser/contributions/notebook-outline-contribution.ts +114 -114
  44. package/src/browser/contributions/notebook-output-action-contribution.ts +82 -82
  45. package/src/browser/contributions/notebook-preferences.ts +92 -92
  46. package/src/browser/contributions/notebook-status-bar-contribution.ts +77 -0
  47. package/src/browser/contributions/notebook-undo-redo-handler.ts +41 -41
  48. package/src/browser/index.ts +27 -27
  49. package/src/browser/notebook-cell-resource-resolver.ts +130 -130
  50. package/src/browser/notebook-editor-widget-factory.ts +82 -82
  51. package/src/browser/notebook-editor-widget.tsx +330 -331
  52. package/src/browser/notebook-frontend-module.ts +119 -115
  53. package/src/browser/notebook-open-handler.ts +120 -114
  54. package/src/browser/notebook-output-utils.ts +119 -119
  55. package/src/browser/notebook-renderer-registry.ts +85 -85
  56. package/src/browser/notebook-type-registry.ts +54 -54
  57. package/src/browser/notebook-types.ts +186 -186
  58. package/src/browser/renderers/cell-output-webview.ts +33 -33
  59. package/src/browser/service/notebook-clipboard-service.ts +43 -43
  60. package/src/browser/service/notebook-context-manager.ts +162 -162
  61. package/src/browser/service/notebook-editor-widget-service.ts +101 -101
  62. package/src/browser/service/notebook-execution-service.ts +139 -139
  63. package/src/browser/service/notebook-execution-state-service.ts +311 -311
  64. package/src/browser/service/notebook-kernel-history-service.ts +124 -124
  65. package/src/browser/service/notebook-kernel-quick-pick-service.ts +479 -479
  66. package/src/browser/service/notebook-kernel-service.ts +357 -357
  67. package/src/browser/service/notebook-model-resolver-service.ts +160 -160
  68. package/src/browser/service/notebook-monaco-text-model-service.ts +48 -48
  69. package/src/browser/service/notebook-options.ts +155 -155
  70. package/src/browser/service/notebook-renderer-messaging-service.ts +121 -121
  71. package/src/browser/service/notebook-service.ts +215 -215
  72. package/src/browser/style/index.css +483 -471
  73. package/src/browser/view/notebook-cell-editor.tsx +263 -247
  74. package/src/browser/view/notebook-cell-list-view.tsx +279 -262
  75. package/src/browser/view/notebook-cell-toolbar-factory.tsx +102 -102
  76. package/src/browser/view/notebook-cell-toolbar.tsx +74 -74
  77. package/src/browser/view/notebook-code-cell-view.tsx +350 -350
  78. package/src/browser/view/notebook-find-widget.tsx +335 -335
  79. package/src/browser/view/notebook-main-toolbar.tsx +235 -235
  80. package/src/browser/view/notebook-markdown-cell-view.tsx +208 -208
  81. package/src/browser/view/notebook-viewport-service.ts +61 -61
  82. package/src/browser/view-model/notebook-cell-model.ts +473 -466
  83. package/src/browser/view-model/notebook-cell-output-model.ts +100 -100
  84. package/src/browser/view-model/notebook-model.ts +550 -550
  85. package/src/common/index.ts +18 -18
  86. package/src/common/notebook-common.ts +337 -337
  87. package/src/common/notebook-protocol.ts +35 -35
  88. 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
+ }