@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.
- package/README.md +30 -30
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/notebook-frontend-module.d.ts.map +1 -1
- package/lib/browser/notebook-frontend-module.js +2 -0
- package/lib/browser/notebook-frontend-module.js.map +1 -1
- package/lib/browser/service/notebook-cell-editor-service.d.ts +16 -0
- package/lib/browser/service/notebook-cell-editor-service.d.ts.map +1 -0
- package/lib/browser/service/notebook-cell-editor-service.js +53 -0
- package/lib/browser/service/notebook-cell-editor-service.js.map +1 -0
- package/lib/browser/view/notebook-cell-editor.d.ts +2 -0
- package/lib/browser/view/notebook-cell-editor.d.ts.map +1 -1
- package/lib/browser/view/notebook-cell-editor.js +12 -1
- package/lib/browser/view/notebook-cell-editor.js.map +1 -1
- package/lib/browser/view/notebook-code-cell-view.d.ts +2 -0
- package/lib/browser/view/notebook-code-cell-view.d.ts.map +1 -1
- package/lib/browser/view/notebook-code-cell-view.js +6 -1
- package/lib/browser/view/notebook-code-cell-view.js.map +1 -1
- package/lib/browser/view/notebook-markdown-cell-view.d.ts +2 -0
- package/lib/browser/view/notebook-markdown-cell-view.d.ts.map +1 -1
- package/lib/browser/view/notebook-markdown-cell-view.js +8 -3
- package/lib/browser/view/notebook-markdown-cell-view.js.map +1 -1
- package/package.json +7 -7
- package/src/browser/contributions/cell-operations.ts +44 -44
- package/src/browser/contributions/notebook-actions-contribution.ts +379 -379
- package/src/browser/contributions/notebook-cell-actions-contribution.ts +525 -525
- package/src/browser/contributions/notebook-color-contribution.ts +268 -268
- package/src/browser/contributions/notebook-context-keys.ts +113 -113
- package/src/browser/contributions/notebook-label-provider-contribution.ts +85 -85
- package/src/browser/contributions/notebook-outline-contribution.ts +114 -114
- package/src/browser/contributions/notebook-output-action-contribution.ts +82 -82
- package/src/browser/contributions/notebook-preferences.ts +92 -92
- package/src/browser/contributions/notebook-status-bar-contribution.ts +77 -77
- package/src/browser/contributions/notebook-undo-redo-handler.ts +41 -41
- package/src/browser/index.ts +28 -27
- package/src/browser/notebook-cell-resource-resolver.ts +130 -130
- package/src/browser/notebook-editor-widget-factory.ts +82 -82
- package/src/browser/notebook-editor-widget.tsx +330 -330
- package/src/browser/notebook-frontend-module.ts +121 -119
- package/src/browser/notebook-open-handler.ts +120 -120
- package/src/browser/notebook-output-utils.ts +119 -119
- package/src/browser/notebook-renderer-registry.ts +85 -85
- package/src/browser/notebook-type-registry.ts +54 -54
- package/src/browser/notebook-types.ts +186 -186
- package/src/browser/renderers/cell-output-webview.ts +33 -33
- package/src/browser/service/notebook-cell-editor-service.ts +56 -0
- package/src/browser/service/notebook-clipboard-service.ts +43 -43
- package/src/browser/service/notebook-context-manager.ts +162 -162
- package/src/browser/service/notebook-editor-widget-service.ts +101 -101
- package/src/browser/service/notebook-execution-service.ts +139 -139
- package/src/browser/service/notebook-execution-state-service.ts +311 -311
- package/src/browser/service/notebook-kernel-history-service.ts +124 -124
- package/src/browser/service/notebook-kernel-quick-pick-service.ts +479 -479
- package/src/browser/service/notebook-kernel-service.ts +357 -357
- package/src/browser/service/notebook-model-resolver-service.ts +160 -160
- package/src/browser/service/notebook-monaco-text-model-service.ts +48 -48
- package/src/browser/service/notebook-options.ts +155 -155
- package/src/browser/service/notebook-renderer-messaging-service.ts +121 -121
- package/src/browser/service/notebook-service.ts +215 -215
- package/src/browser/style/index.css +484 -483
- package/src/browser/view/notebook-cell-editor.tsx +276 -263
- package/src/browser/view/notebook-cell-list-view.tsx +279 -279
- package/src/browser/view/notebook-cell-toolbar-factory.tsx +102 -102
- package/src/browser/view/notebook-cell-toolbar.tsx +74 -74
- package/src/browser/view/notebook-code-cell-view.tsx +355 -350
- package/src/browser/view/notebook-find-widget.tsx +335 -335
- package/src/browser/view/notebook-main-toolbar.tsx +235 -235
- package/src/browser/view/notebook-markdown-cell-view.tsx +215 -208
- package/src/browser/view/notebook-viewport-service.ts +61 -61
- package/src/browser/view-model/notebook-cell-model.ts +473 -473
- package/src/browser/view-model/notebook-cell-output-model.ts +100 -100
- package/src/browser/view-model/notebook-model.ts +550 -550
- package/src/common/index.ts +18 -18
- package/src/common/notebook-common.ts +337 -337
- package/src/common/notebook-protocol.ts +35 -35
- 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
|
+
}
|