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

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