@difizen/libro-shared-model 0.0.2-alpha.0
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/LICENSE +21 -0
- package/README.md +1 -0
- package/es/api.d.ts +639 -0
- package/es/api.d.ts.map +1 -0
- package/es/api.js +91 -0
- package/es/index.d.ts +4 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +3 -0
- package/es/utils.d.ts +18 -0
- package/es/utils.d.ts.map +1 -0
- package/es/utils.js +41 -0
- package/es/ymodels.d.ts +656 -0
- package/es/ymodels.d.ts.map +1 -0
- package/es/ymodels.js +1778 -0
- package/package.json +57 -0
- package/src/api.ts +743 -0
- package/src/index.ts +3 -0
- package/src/utils.ts +46 -0
- package/src/ymodels.ts +1603 -0
package/src/ymodels.ts
ADDED
|
@@ -0,0 +1,1603 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IAttachments,
|
|
3
|
+
IBaseCellMetadata,
|
|
4
|
+
PartialJSONValue,
|
|
5
|
+
IBaseCell,
|
|
6
|
+
IOutput,
|
|
7
|
+
ICodeCell,
|
|
8
|
+
IRawCell,
|
|
9
|
+
IMarkdownCell,
|
|
10
|
+
INotebookMetadata,
|
|
11
|
+
CellType,
|
|
12
|
+
} from '@difizen/libro-common';
|
|
13
|
+
import { deepCopy, deepEqual } from '@difizen/libro-common';
|
|
14
|
+
import type { Event } from '@difizen/mana-common';
|
|
15
|
+
import { Emitter } from '@difizen/mana-common';
|
|
16
|
+
import { v4 } from 'uuid';
|
|
17
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
18
|
+
import * as Y from 'yjs';
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
CellChange,
|
|
22
|
+
CellTypeAdaptor,
|
|
23
|
+
Delta,
|
|
24
|
+
DocumentChange,
|
|
25
|
+
FileChange,
|
|
26
|
+
IListChange,
|
|
27
|
+
IMapChange,
|
|
28
|
+
ISharedAttachmentsCell,
|
|
29
|
+
ISharedBaseCell,
|
|
30
|
+
ISharedCell,
|
|
31
|
+
ISharedCodeCell,
|
|
32
|
+
ISharedDocument,
|
|
33
|
+
ISharedFile,
|
|
34
|
+
ISharedMarkdownCell,
|
|
35
|
+
ISharedNotebook,
|
|
36
|
+
ISharedRawCell,
|
|
37
|
+
ISharedText,
|
|
38
|
+
NotebookChange,
|
|
39
|
+
SharedCell,
|
|
40
|
+
StateChange,
|
|
41
|
+
} from './api.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Abstract interface to define Shared Models that can be bound to a text editor using any existing
|
|
45
|
+
* Yjs-based editor binding.
|
|
46
|
+
*/
|
|
47
|
+
export interface IYText extends ISharedText {
|
|
48
|
+
/**
|
|
49
|
+
* Shareable text
|
|
50
|
+
*/
|
|
51
|
+
readonly ysource: Y.Text;
|
|
52
|
+
/**
|
|
53
|
+
* Shareable awareness
|
|
54
|
+
*/
|
|
55
|
+
readonly awareness: Awareness | null;
|
|
56
|
+
/**
|
|
57
|
+
* Undo manager
|
|
58
|
+
*/
|
|
59
|
+
readonly undoManager: Y.UndoManager | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generic shareable document.
|
|
64
|
+
*/
|
|
65
|
+
export class YDocument<T extends DocumentChange> implements ISharedDocument {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.ystate.observe(this.onStateChanged);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* YJS document
|
|
72
|
+
*/
|
|
73
|
+
readonly ydoc = new Y.Doc();
|
|
74
|
+
/**
|
|
75
|
+
* Shared state
|
|
76
|
+
*/
|
|
77
|
+
readonly ystate: Y.Map<any> = this.ydoc.getMap('state');
|
|
78
|
+
/**
|
|
79
|
+
* YJS document undo manager
|
|
80
|
+
*/
|
|
81
|
+
readonly undoManager = new Y.UndoManager([], {
|
|
82
|
+
trackedOrigins: new Set([this]),
|
|
83
|
+
doc: this.ydoc,
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Shared awareness
|
|
87
|
+
*/
|
|
88
|
+
readonly awareness = new Awareness(this.ydoc);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The changed signal.
|
|
92
|
+
*/
|
|
93
|
+
get changed(): Event<T> {
|
|
94
|
+
return this._changed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether the document is disposed or not.
|
|
99
|
+
*/
|
|
100
|
+
get isDisposed(): boolean {
|
|
101
|
+
return this._isDisposed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether the object can undo changes.
|
|
106
|
+
*/
|
|
107
|
+
canUndo(): boolean {
|
|
108
|
+
return this.undoManager.undoStack.length > 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether the object can redo changes.
|
|
113
|
+
*/
|
|
114
|
+
canRedo(): boolean {
|
|
115
|
+
return this.undoManager.redoStack.length > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Dispose of the resources.
|
|
120
|
+
*/
|
|
121
|
+
dispose(): void {
|
|
122
|
+
if (this._isDisposed) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this._isDisposed = true;
|
|
126
|
+
this.ystate.unobserve(this.onStateChanged);
|
|
127
|
+
this.awareness.destroy();
|
|
128
|
+
this.undoManager.destroy();
|
|
129
|
+
this.ydoc.destroy();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Undo an operation.
|
|
134
|
+
*/
|
|
135
|
+
undo(): void {
|
|
136
|
+
this.undoManager.undo();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Redo an operation.
|
|
141
|
+
*/
|
|
142
|
+
redo(): void {
|
|
143
|
+
this.undoManager.redo();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear the change stack.
|
|
148
|
+
*/
|
|
149
|
+
clearUndoHistory(): void {
|
|
150
|
+
this.undoManager.clear();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Perform a transaction. While the function f is called, all changes to the shared
|
|
155
|
+
* document are bundled into a single event.
|
|
156
|
+
*/
|
|
157
|
+
transact(f: () => void, undoable = true): void {
|
|
158
|
+
this.ydoc.transact(f, undoable ? this : null);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle a change to the ystate.
|
|
163
|
+
*/
|
|
164
|
+
protected onStateChanged = (event: Y.YMapEvent<any>): void => {
|
|
165
|
+
const stateChange = new Array<StateChange<any>>();
|
|
166
|
+
event.keysChanged.forEach((key) => {
|
|
167
|
+
const change = event.changes.keys.get(key);
|
|
168
|
+
if (change) {
|
|
169
|
+
stateChange.push({
|
|
170
|
+
name: key,
|
|
171
|
+
oldValue: change.oldValue,
|
|
172
|
+
newValue: this.ystate.get(key),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this._changedEmitter.fire({ stateChange } as any);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
protected _changedEmitter = new Emitter<T>();
|
|
181
|
+
protected _changed = this._changedEmitter.event;
|
|
182
|
+
private _isDisposed = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Shareable text file.
|
|
187
|
+
*/
|
|
188
|
+
export class YFile
|
|
189
|
+
extends YDocument<FileChange>
|
|
190
|
+
implements ISharedFile, ISharedText, IYText
|
|
191
|
+
{
|
|
192
|
+
/**
|
|
193
|
+
* Instantiate a new shareable file.
|
|
194
|
+
*
|
|
195
|
+
* @param source The initial file content
|
|
196
|
+
*
|
|
197
|
+
* @returns The file model
|
|
198
|
+
*/
|
|
199
|
+
static create(source?: string): YFile {
|
|
200
|
+
const model = new YFile();
|
|
201
|
+
if (source) {
|
|
202
|
+
model.source = source;
|
|
203
|
+
}
|
|
204
|
+
return model;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
constructor() {
|
|
208
|
+
super();
|
|
209
|
+
this.undoManager.addToScope(this.ysource);
|
|
210
|
+
this.ysource.observe(this._modelObserver);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* YJS file text.
|
|
215
|
+
*/
|
|
216
|
+
readonly ysource = this.ydoc.getText('source');
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* File text
|
|
220
|
+
*/
|
|
221
|
+
get source(): string {
|
|
222
|
+
return this.getSource();
|
|
223
|
+
}
|
|
224
|
+
set source(v: string) {
|
|
225
|
+
this.setSource(v);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Dispose of the resources.
|
|
230
|
+
*/
|
|
231
|
+
override dispose(): void {
|
|
232
|
+
if (this.isDisposed) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.ysource.unobserve(this._modelObserver);
|
|
236
|
+
super.dispose();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get the file text.
|
|
241
|
+
*
|
|
242
|
+
* @returns File text.
|
|
243
|
+
*/
|
|
244
|
+
getSource(): string {
|
|
245
|
+
return this.ysource.toString();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Set the file text.
|
|
250
|
+
*
|
|
251
|
+
* @param value New text
|
|
252
|
+
*/
|
|
253
|
+
setSource(value: string): void {
|
|
254
|
+
this.transact(() => {
|
|
255
|
+
const ytext = this.ysource;
|
|
256
|
+
ytext.delete(0, ytext.length);
|
|
257
|
+
ytext.insert(0, value);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Replace content from `start' to `end` with `value`.
|
|
263
|
+
*
|
|
264
|
+
* @param start: The start index of the range to replace (inclusive).
|
|
265
|
+
* @param end: The end index of the range to replace (exclusive).
|
|
266
|
+
* @param value: New source (optional).
|
|
267
|
+
*/
|
|
268
|
+
updateSource(start: number, end: number, value = ''): void {
|
|
269
|
+
this.transact(() => {
|
|
270
|
+
const ysource = this.ysource;
|
|
271
|
+
// insert and then delete.
|
|
272
|
+
// This ensures that the cursor position is adjusted after the replaced content.
|
|
273
|
+
ysource.insert(start, value);
|
|
274
|
+
ysource.delete(start + value.length, end - start);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle a change to the ymodel.
|
|
280
|
+
*/
|
|
281
|
+
private _modelObserver = (event: Y.YTextEvent) => {
|
|
282
|
+
this._changedEmitter.fire({ sourceChange: event.changes.delta as Delta<string> });
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const defaultCellTypeAdaptor: CellTypeAdaptor = (cell_Type: CellType) =>
|
|
287
|
+
cell_Type as 'code' | 'markdown' | 'raw';
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create a new shared cell model given the YJS shared type.
|
|
291
|
+
*/
|
|
292
|
+
const createCellModelFromSharedType = (
|
|
293
|
+
type: Y.Map<any>,
|
|
294
|
+
options: SharedCell.IOptions = {},
|
|
295
|
+
cellTypeAdaptor = defaultCellTypeAdaptor,
|
|
296
|
+
): YCellType => {
|
|
297
|
+
switch (cellTypeAdaptor(type.get('cell_type'))) {
|
|
298
|
+
case 'code':
|
|
299
|
+
return new YCodeCell(type, type.get('source'), type.get('outputs'), options);
|
|
300
|
+
case 'markdown':
|
|
301
|
+
return new YMarkdownCell(type, type.get('source'), options);
|
|
302
|
+
case 'raw':
|
|
303
|
+
return new YRawCell(type, type.get('source'), options);
|
|
304
|
+
default:
|
|
305
|
+
throw new Error('Found unknown cell type');
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a new cell that can be inserted in an existing shared model.
|
|
311
|
+
*
|
|
312
|
+
* If no notebook is specified the cell will be standalone.
|
|
313
|
+
*
|
|
314
|
+
* @param cell Cell JSON representation
|
|
315
|
+
* @param notebook Notebook to which the cell will be added
|
|
316
|
+
*/
|
|
317
|
+
const createCell = (
|
|
318
|
+
cell: SharedCell.Cell,
|
|
319
|
+
notebook?: YNotebook,
|
|
320
|
+
cellTypeAdaptor = defaultCellTypeAdaptor,
|
|
321
|
+
): YCodeCell | YMarkdownCell | YRawCell => {
|
|
322
|
+
const ymodel = new Y.Map();
|
|
323
|
+
const ysource = new Y.Text();
|
|
324
|
+
ymodel.set('source', ysource);
|
|
325
|
+
ymodel.set('metadata', {});
|
|
326
|
+
ymodel.set('cell_type', cell.cell_type);
|
|
327
|
+
ymodel.set('id', cell.id ?? v4());
|
|
328
|
+
|
|
329
|
+
let ycell: YCellType;
|
|
330
|
+
switch (cellTypeAdaptor(cell.cell_type)) {
|
|
331
|
+
case 'markdown': {
|
|
332
|
+
ycell = new YMarkdownCell(ymodel, ysource, { notebook });
|
|
333
|
+
if (cell.attachments !== null) {
|
|
334
|
+
ycell.setAttachments(cell.attachments as IAttachments);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case 'code': {
|
|
339
|
+
const youtputs = new Y.Array();
|
|
340
|
+
ymodel.set('outputs', youtputs);
|
|
341
|
+
ycell = new YCodeCell(ymodel, ysource, youtputs, {
|
|
342
|
+
notebook,
|
|
343
|
+
});
|
|
344
|
+
const cCell = cell as Partial<ICodeCell>;
|
|
345
|
+
ycell.execution_count = cCell.execution_count ?? null;
|
|
346
|
+
if (cCell.outputs) {
|
|
347
|
+
ycell.setOutputs(cCell.outputs);
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
default: {
|
|
352
|
+
// raw
|
|
353
|
+
ycell = new YRawCell(ymodel, ysource, { notebook });
|
|
354
|
+
if (cell.attachments) {
|
|
355
|
+
ycell.setAttachments(cell.attachments as IAttachments);
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (cell.metadata !== null) {
|
|
362
|
+
ycell.setMetadata(cell.metadata);
|
|
363
|
+
}
|
|
364
|
+
if (cell.source !== null) {
|
|
365
|
+
ycell.setSource(
|
|
366
|
+
typeof cell.source === 'string' ? cell.source : cell.source.join('\n'),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return ycell;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Create a new cell that cannot be inserted in an existing shared model.
|
|
374
|
+
*
|
|
375
|
+
* @param cell Cell JSON representation
|
|
376
|
+
*/
|
|
377
|
+
export const createStandaloneCell = (
|
|
378
|
+
cell: SharedCell.Cell,
|
|
379
|
+
cellTypeAdaptor?: CellTypeAdaptor,
|
|
380
|
+
): YCellType => createCell(cell, undefined, cellTypeAdaptor);
|
|
381
|
+
|
|
382
|
+
export class YBaseCell<Metadata extends IBaseCellMetadata>
|
|
383
|
+
implements ISharedBaseCell<Metadata>, IYText
|
|
384
|
+
{
|
|
385
|
+
/**
|
|
386
|
+
* Create a new YCell that works standalone. It cannot be
|
|
387
|
+
* inserted into a YNotebook because the Yjs model is already
|
|
388
|
+
* attached to an anonymous Y.Doc instance.
|
|
389
|
+
*/
|
|
390
|
+
static createStandalone(id?: string): YBaseCell<any> {
|
|
391
|
+
const cell = createCell({
|
|
392
|
+
id,
|
|
393
|
+
cell_type: this.prototype.cell_type,
|
|
394
|
+
source: '',
|
|
395
|
+
metadata: {},
|
|
396
|
+
});
|
|
397
|
+
return cell;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Base cell constructor
|
|
402
|
+
*
|
|
403
|
+
* ### Notes
|
|
404
|
+
* Don't use the constructor directly - prefer using ``YNotebook.insertCell``
|
|
405
|
+
*
|
|
406
|
+
* The ``ysource`` is needed because ``ymodel.get('source')`` will
|
|
407
|
+
* not return the real source if the model is not yet attached to
|
|
408
|
+
* a document. Requesting it explicitly allows to introspect a non-empty
|
|
409
|
+
* source before the cell is attached to the document.
|
|
410
|
+
*
|
|
411
|
+
* @param ymodel Cell map
|
|
412
|
+
* @param ysource Cell source
|
|
413
|
+
* @param options { notebook?: The notebook the cell is attached to }
|
|
414
|
+
*/
|
|
415
|
+
constructor(ymodel: Y.Map<any>, ysource: Y.Text, options: SharedCell.IOptions = {}) {
|
|
416
|
+
this.ymodel = ymodel;
|
|
417
|
+
this._ysource = ysource;
|
|
418
|
+
this._prevSourceLength = ysource ? ysource.length : 0;
|
|
419
|
+
this._notebook = null;
|
|
420
|
+
this._awareness = null;
|
|
421
|
+
this._undoManager = null;
|
|
422
|
+
if (options.notebook) {
|
|
423
|
+
this._notebook = options.notebook as YNotebook;
|
|
424
|
+
// We cannot create a undo manager with the cell not yet attached in the notebook
|
|
425
|
+
// so we defer that to the notebook insertCell method
|
|
426
|
+
} else {
|
|
427
|
+
// Standalone cell
|
|
428
|
+
const doc = new Y.Doc();
|
|
429
|
+
doc.getArray().insert(0, [this.ymodel]);
|
|
430
|
+
this._awareness = new Awareness(doc);
|
|
431
|
+
this._undoManager = new Y.UndoManager([this.ymodel], {
|
|
432
|
+
trackedOrigins: new Set([this]),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
this.ymodel.observeDeep(this._modelObserver);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Cell notebook awareness or null if the cell is standalone.
|
|
441
|
+
*/
|
|
442
|
+
get awareness(): Awareness | null {
|
|
443
|
+
return this._awareness ?? this.notebook?.awareness ?? null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The type of the cell.
|
|
448
|
+
*/
|
|
449
|
+
get cell_type(): any {
|
|
450
|
+
throw new Error('A YBaseCell must not be constructed');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The changed signal.
|
|
455
|
+
*/
|
|
456
|
+
get changed(): Event<CellChange<Metadata>> {
|
|
457
|
+
return this._changed;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Cell id
|
|
462
|
+
*/
|
|
463
|
+
get id(): string {
|
|
464
|
+
return this.getId();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Whether the model has been disposed or not.
|
|
469
|
+
*/
|
|
470
|
+
get isDisposed(): boolean {
|
|
471
|
+
return this._isDisposed;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Whether the cell is standalone or not.
|
|
476
|
+
*
|
|
477
|
+
* If the cell is standalone. It cannot be
|
|
478
|
+
* inserted into a YNotebook because the Yjs model is already
|
|
479
|
+
* attached to an anonymous Y.Doc instance.
|
|
480
|
+
*/
|
|
481
|
+
get isStandalone(): boolean {
|
|
482
|
+
return this._notebook !== null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Cell metadata.
|
|
487
|
+
*/
|
|
488
|
+
get metadata(): Partial<Metadata> {
|
|
489
|
+
return this.getMetadata();
|
|
490
|
+
}
|
|
491
|
+
set metadata(v: Partial<Metadata>) {
|
|
492
|
+
this.setMetadata(v);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Signal triggered when the cell metadata changes.
|
|
497
|
+
*/
|
|
498
|
+
get metadataChanged(): Event<IMapChange> {
|
|
499
|
+
return this._metadataChanged;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The notebook that this cell belongs to.
|
|
504
|
+
*/
|
|
505
|
+
get notebook(): YNotebook | null {
|
|
506
|
+
return this._notebook;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Cell input content.
|
|
511
|
+
*/
|
|
512
|
+
get source(): string {
|
|
513
|
+
return this.getSource();
|
|
514
|
+
}
|
|
515
|
+
set source(v: string) {
|
|
516
|
+
this.setSource(v);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The cell undo manager.
|
|
521
|
+
*/
|
|
522
|
+
get undoManager(): Y.UndoManager | null {
|
|
523
|
+
if (!this.notebook) {
|
|
524
|
+
return this._undoManager;
|
|
525
|
+
}
|
|
526
|
+
return this.notebook?.disableDocumentWideUndoRedo
|
|
527
|
+
? this._undoManager
|
|
528
|
+
: this.notebook.undoManager;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Defer setting the undo manager as it requires the
|
|
533
|
+
* cell to be attached to the notebook Y document.
|
|
534
|
+
*/
|
|
535
|
+
setUndoManager(): void {
|
|
536
|
+
if (this._undoManager) {
|
|
537
|
+
throw new Error('The cell undo manager is already set.');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (this._notebook && this._notebook.disableDocumentWideUndoRedo) {
|
|
541
|
+
this._undoManager = new Y.UndoManager([this.ymodel], {
|
|
542
|
+
trackedOrigins: new Set([this]),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
readonly ymodel: Y.Map<any>;
|
|
548
|
+
|
|
549
|
+
get ysource(): Y.Text {
|
|
550
|
+
return this._ysource;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Whether the object can undo changes.
|
|
555
|
+
*/
|
|
556
|
+
canUndo(): boolean {
|
|
557
|
+
return !!this.undoManager && this.undoManager.undoStack.length > 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Whether the object can redo changes.
|
|
562
|
+
*/
|
|
563
|
+
canRedo(): boolean {
|
|
564
|
+
return !!this.undoManager && this.undoManager.redoStack.length > 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Clear the change stack.
|
|
569
|
+
*/
|
|
570
|
+
clearUndoHistory(): void {
|
|
571
|
+
this.undoManager?.clear();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Undo an operation.
|
|
576
|
+
*/
|
|
577
|
+
undo(): void {
|
|
578
|
+
this.undoManager?.undo();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Redo an operation.
|
|
583
|
+
*/
|
|
584
|
+
redo(): void {
|
|
585
|
+
this.undoManager?.redo();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Dispose of the resources.
|
|
590
|
+
*/
|
|
591
|
+
dispose(): void {
|
|
592
|
+
if (this._isDisposed) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
this._isDisposed = true;
|
|
596
|
+
this.ymodel.unobserveDeep(this._modelObserver);
|
|
597
|
+
|
|
598
|
+
if (this._awareness) {
|
|
599
|
+
// A new document is created for standalone cell.
|
|
600
|
+
const doc = this._awareness.doc;
|
|
601
|
+
this._awareness.destroy();
|
|
602
|
+
doc.destroy();
|
|
603
|
+
}
|
|
604
|
+
if (this._undoManager) {
|
|
605
|
+
// Be sure to not destroy the document undo manager.
|
|
606
|
+
if (this._undoManager === this.notebook?.undoManager) {
|
|
607
|
+
this._undoManager = null;
|
|
608
|
+
} else {
|
|
609
|
+
this._undoManager.destroy();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Get cell id.
|
|
616
|
+
*
|
|
617
|
+
* @returns Cell id
|
|
618
|
+
*/
|
|
619
|
+
getId(): string {
|
|
620
|
+
return this.ymodel.get('id');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Gets cell's source.
|
|
625
|
+
*
|
|
626
|
+
* @returns Cell's source.
|
|
627
|
+
*/
|
|
628
|
+
getSource(): string {
|
|
629
|
+
return this.ysource.toString();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Sets cell's source.
|
|
634
|
+
*
|
|
635
|
+
* @param value: New source.
|
|
636
|
+
*/
|
|
637
|
+
setSource(value: string): void {
|
|
638
|
+
this.transact(() => {
|
|
639
|
+
this.ysource.delete(0, this.ysource.length);
|
|
640
|
+
this.ysource.insert(0, value);
|
|
641
|
+
});
|
|
642
|
+
// @todo Do we need proper replace semantic? This leads to issues in editor bindings because they don't switch source.
|
|
643
|
+
// this.ymodel.set('source', new Y.Text(value));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Replace content from `start' to `end` with `value`.
|
|
648
|
+
*
|
|
649
|
+
* @param start: The start index of the range to replace (inclusive).
|
|
650
|
+
*
|
|
651
|
+
* @param end: The end index of the range to replace (exclusive).
|
|
652
|
+
*
|
|
653
|
+
* @param value: New source (optional).
|
|
654
|
+
*/
|
|
655
|
+
updateSource(start: number, end: number, value = ''): void {
|
|
656
|
+
this.transact(() => {
|
|
657
|
+
const ysource = this.ysource;
|
|
658
|
+
// insert and then delete.
|
|
659
|
+
// This ensures that the cursor position is adjusted after the replaced content.
|
|
660
|
+
ysource.insert(start, value);
|
|
661
|
+
ysource.delete(start + value.length, end - start);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Delete a metadata cell.
|
|
667
|
+
*
|
|
668
|
+
* @param key The key to delete
|
|
669
|
+
*/
|
|
670
|
+
deleteMetadata(key: string): void {
|
|
671
|
+
const allMetadata = deepCopy(this.ymodel.get('metadata'));
|
|
672
|
+
delete allMetadata[key];
|
|
673
|
+
this.setMetadata(allMetadata);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Returns the metadata associated with the cell.
|
|
678
|
+
*
|
|
679
|
+
* @param key
|
|
680
|
+
* @returns Cell metadata.
|
|
681
|
+
*/
|
|
682
|
+
getMetadata(key?: string): Partial<Metadata> {
|
|
683
|
+
const metadata = this.ymodel.get('metadata');
|
|
684
|
+
|
|
685
|
+
if (typeof key === 'string') {
|
|
686
|
+
return deepCopy(metadata[key]);
|
|
687
|
+
} else {
|
|
688
|
+
return deepCopy(metadata);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Sets some cell metadata.
|
|
694
|
+
*
|
|
695
|
+
* If only one argument is provided, it will override all cell metadata.
|
|
696
|
+
* Otherwise a single key will be set to a new value.
|
|
697
|
+
*
|
|
698
|
+
* @param metadata Cell's metadata or key.
|
|
699
|
+
* @param value Metadata value
|
|
700
|
+
*/
|
|
701
|
+
setMetadata(metadata: Partial<Metadata> | string, value?: PartialJSONValue): void {
|
|
702
|
+
if (typeof metadata === 'string') {
|
|
703
|
+
if (typeof value === 'undefined') {
|
|
704
|
+
throw new TypeError(
|
|
705
|
+
`Metadata value for ${metadata} cannot be 'undefined'; use deleteMetadata.`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const key = metadata;
|
|
709
|
+
// eslint-disable-next-line no-param-reassign
|
|
710
|
+
metadata = this.getMetadata();
|
|
711
|
+
// @ts-expect-error metadata type is changed at runtime.
|
|
712
|
+
metadata[key] = value;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const clone = deepCopy(metadata) as any;
|
|
716
|
+
if (clone.collapsed !== null) {
|
|
717
|
+
clone.jupyter = clone.jupyter || {};
|
|
718
|
+
clone.jupyter.outputs_hidden = clone.collapsed;
|
|
719
|
+
} else if (clone?.jupyter?.outputs_hidden !== null) {
|
|
720
|
+
clone.collapsed = clone.jupyter.outputs_hidden;
|
|
721
|
+
}
|
|
722
|
+
if (this.ymodel.doc === null || !deepEqual(clone, this.getMetadata())) {
|
|
723
|
+
this.transact(() => {
|
|
724
|
+
this.ymodel.set('metadata', clone);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Serialize the model to JSON.
|
|
731
|
+
*/
|
|
732
|
+
toJSON(): IBaseCell {
|
|
733
|
+
return {
|
|
734
|
+
id: this.getId(),
|
|
735
|
+
cell_type: this.cell_type,
|
|
736
|
+
source: this.getSource(),
|
|
737
|
+
metadata: this.getMetadata(),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Perform a transaction. While the function f is called, all changes to the shared
|
|
743
|
+
* document are bundled into a single event.
|
|
744
|
+
*/
|
|
745
|
+
transact(f: () => void, undoable = true): void {
|
|
746
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
747
|
+
this.notebook && undoable
|
|
748
|
+
? this.notebook.transact(f)
|
|
749
|
+
: this.ymodel.doc === null
|
|
750
|
+
? f()
|
|
751
|
+
: this.ymodel.doc.transact(f, this);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Extract changes from YJS events
|
|
756
|
+
*
|
|
757
|
+
* @param events YJS events
|
|
758
|
+
* @returns Cell changes
|
|
759
|
+
*/
|
|
760
|
+
protected getChanges(events: Y.YEvent<any>[]): Partial<CellChange<Metadata>> {
|
|
761
|
+
const changes: CellChange<Metadata> = {};
|
|
762
|
+
|
|
763
|
+
const sourceEvent = events.find(
|
|
764
|
+
(event) => event.target === this.ymodel.get('source'),
|
|
765
|
+
);
|
|
766
|
+
if (sourceEvent) {
|
|
767
|
+
changes.sourceChange = sourceEvent.changes.delta as any;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const modelEvent = events.find((event) => event.target === this.ymodel) as
|
|
771
|
+
| undefined
|
|
772
|
+
| Y.YMapEvent<any>;
|
|
773
|
+
if (modelEvent && modelEvent.keysChanged.has('metadata')) {
|
|
774
|
+
const change = modelEvent.changes.keys.get('metadata');
|
|
775
|
+
const metadataChange = (changes.metadataChange = {
|
|
776
|
+
oldValue: change?.oldValue ? change.oldValue : undefined,
|
|
777
|
+
newValue: this.getMetadata(),
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
const oldValue = metadataChange.oldValue ?? {};
|
|
781
|
+
const oldKeys = Object.keys(oldValue);
|
|
782
|
+
const newKeys = Object.keys(metadataChange.newValue);
|
|
783
|
+
for (const key of new Set(oldKeys.concat(newKeys))) {
|
|
784
|
+
if (!oldKeys.includes(key)) {
|
|
785
|
+
this._metadataChangedEmitter.fire({
|
|
786
|
+
key,
|
|
787
|
+
newValue: metadataChange.newValue[key],
|
|
788
|
+
type: 'add',
|
|
789
|
+
});
|
|
790
|
+
} else if (!newKeys.includes(key)) {
|
|
791
|
+
this._metadataChangedEmitter.fire({
|
|
792
|
+
key,
|
|
793
|
+
oldValue: metadataChange.oldValue[key],
|
|
794
|
+
type: 'remove',
|
|
795
|
+
});
|
|
796
|
+
} else if (!deepEqual(oldValue[key], metadataChange.newValue[key]!)) {
|
|
797
|
+
this._metadataChangedEmitter.fire({
|
|
798
|
+
key,
|
|
799
|
+
newValue: metadataChange.newValue[key],
|
|
800
|
+
oldValue: metadataChange.oldValue[key],
|
|
801
|
+
type: 'change',
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// The model allows us to replace the complete source with a new string. We express this in the Delta format
|
|
808
|
+
// as a replace of the complete string.
|
|
809
|
+
const ysource = this.ymodel.get('source');
|
|
810
|
+
if (modelEvent && modelEvent.keysChanged.has('source')) {
|
|
811
|
+
changes.sourceChange = [
|
|
812
|
+
{ delete: this._prevSourceLength },
|
|
813
|
+
{ insert: ysource.toString() },
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
this._prevSourceLength = ysource.length;
|
|
817
|
+
|
|
818
|
+
return changes;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Handle a change to the ymodel.
|
|
823
|
+
*/
|
|
824
|
+
private _modelObserver = (events: Y.YEvent<any>[]) => {
|
|
825
|
+
this._changedEmitter.fire(this.getChanges(events));
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
protected _metadataChangedEmitter = new Emitter<IMapChange>();
|
|
829
|
+
protected _metadataChanged = this._metadataChangedEmitter.event;
|
|
830
|
+
/**
|
|
831
|
+
* The notebook that this cell belongs to.
|
|
832
|
+
*/
|
|
833
|
+
protected _notebook: YNotebook | null = null;
|
|
834
|
+
private _awareness: Awareness | null;
|
|
835
|
+
private _changedEmitter = new Emitter<CellChange<Metadata>>();
|
|
836
|
+
private _changed = this._changedEmitter.event;
|
|
837
|
+
private _isDisposed = false;
|
|
838
|
+
private _prevSourceLength: number;
|
|
839
|
+
private _undoManager: Y.UndoManager | null = null;
|
|
840
|
+
private _ysource: Y.Text;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Shareable code cell.
|
|
845
|
+
*/
|
|
846
|
+
export class YCodeCell extends YBaseCell<IBaseCellMetadata> implements ISharedCodeCell {
|
|
847
|
+
/**
|
|
848
|
+
* Create a new YCodeCell that works standalone. It cannot be
|
|
849
|
+
* inserted into a YNotebook because the Yjs model is already
|
|
850
|
+
* attached to an anonymous Y.Doc instance.
|
|
851
|
+
*/
|
|
852
|
+
static override createStandalone(id?: string): YCodeCell {
|
|
853
|
+
return super.createStandalone(id) as YCodeCell;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Code cell constructor
|
|
858
|
+
*
|
|
859
|
+
* ### Notes
|
|
860
|
+
* Don't use the constructor directly - prefer using ``YNotebook.insertCell``
|
|
861
|
+
*
|
|
862
|
+
* The ``ysource`` is needed because ``ymodel.get('source')`` will
|
|
863
|
+
* not return the real source if the model is not yet attached to
|
|
864
|
+
* a document. Requesting it explicitly allows to introspect a non-empty
|
|
865
|
+
* source before the cell is attached to the document.
|
|
866
|
+
*
|
|
867
|
+
* @param ymodel Cell map
|
|
868
|
+
* @param ysource Cell source
|
|
869
|
+
* @param youtputs Code cell outputs
|
|
870
|
+
* @param options { notebook?: The notebook the cell is attached to }
|
|
871
|
+
*/
|
|
872
|
+
constructor(
|
|
873
|
+
ymodel: Y.Map<any>,
|
|
874
|
+
ysource: Y.Text,
|
|
875
|
+
youtputs: Y.Array<any>,
|
|
876
|
+
options: SharedCell.IOptions = {},
|
|
877
|
+
) {
|
|
878
|
+
super(ymodel, ysource, options);
|
|
879
|
+
this._youtputs = youtputs;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* The type of the cell.
|
|
884
|
+
*/
|
|
885
|
+
override get cell_type(): string {
|
|
886
|
+
return this.ymodel.get('cell_type');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The code cell's prompt number. Will be null if the cell has not been run.
|
|
891
|
+
*/
|
|
892
|
+
get execution_count(): number | null {
|
|
893
|
+
return this.ymodel.get('execution_count') || null;
|
|
894
|
+
}
|
|
895
|
+
set execution_count(count: number | null) {
|
|
896
|
+
// Do not use `this.execution_count`. When initializing the
|
|
897
|
+
// cell, we need to set execution_count to `null` if we compare
|
|
898
|
+
// using `this.execution_count` it will return `null` and we will
|
|
899
|
+
// never initialize it
|
|
900
|
+
if (this.ymodel.get('execution_count') !== count) {
|
|
901
|
+
this.transact(() => {
|
|
902
|
+
this.ymodel.set('execution_count', count);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Cell outputs.
|
|
909
|
+
*/
|
|
910
|
+
get outputs(): IOutput[] {
|
|
911
|
+
return this.getOutputs();
|
|
912
|
+
}
|
|
913
|
+
set outputs(v: IOutput[]) {
|
|
914
|
+
this.setOutputs(v);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Execution, display, or stream outputs.
|
|
919
|
+
*/
|
|
920
|
+
getOutputs(): IOutput[] {
|
|
921
|
+
return deepCopy(this._youtputs.toArray());
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Replace all outputs.
|
|
926
|
+
*/
|
|
927
|
+
setOutputs(outputs: IOutput[]): void {
|
|
928
|
+
this.transact(() => {
|
|
929
|
+
this._youtputs.delete(0, this._youtputs.length);
|
|
930
|
+
this._youtputs.insert(0, outputs);
|
|
931
|
+
}, false);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Replace content from `start' to `end` with `outputs`.
|
|
936
|
+
*
|
|
937
|
+
* @param start: The start index of the range to replace (inclusive).
|
|
938
|
+
*
|
|
939
|
+
* @param end: The end index of the range to replace (exclusive).
|
|
940
|
+
*
|
|
941
|
+
* @param outputs: New outputs (optional).
|
|
942
|
+
*/
|
|
943
|
+
updateOutputs(start: number, end: number, outputs: IOutput[] = []): void {
|
|
944
|
+
const fin =
|
|
945
|
+
end < this._youtputs.length ? end - start : this._youtputs.length - start;
|
|
946
|
+
this.transact(() => {
|
|
947
|
+
this._youtputs.delete(start, fin);
|
|
948
|
+
this._youtputs.insert(start, outputs);
|
|
949
|
+
}, false);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Serialize the model to JSON.
|
|
954
|
+
*/
|
|
955
|
+
override toJSON(): ICodeCell {
|
|
956
|
+
return {
|
|
957
|
+
...(super.toJSON() as ICodeCell),
|
|
958
|
+
outputs: this.getOutputs(),
|
|
959
|
+
execution_count: this.execution_count,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Extract changes from YJS events
|
|
965
|
+
*
|
|
966
|
+
* @param events YJS events
|
|
967
|
+
* @returns Cell changes
|
|
968
|
+
*/
|
|
969
|
+
protected override getChanges(
|
|
970
|
+
events: Y.YEvent<any>[],
|
|
971
|
+
): Partial<CellChange<IBaseCellMetadata>> {
|
|
972
|
+
const changes = super.getChanges(events);
|
|
973
|
+
|
|
974
|
+
const outputEvent = events.find(
|
|
975
|
+
(event) => event.target === this.ymodel.get('outputs'),
|
|
976
|
+
);
|
|
977
|
+
if (outputEvent) {
|
|
978
|
+
changes.outputsChange = outputEvent.changes.delta as any;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const modelEvent = events.find((event) => event.target === this.ymodel) as
|
|
982
|
+
| undefined
|
|
983
|
+
| Y.YMapEvent<any>;
|
|
984
|
+
|
|
985
|
+
if (modelEvent && modelEvent.keysChanged.has('execution_count')) {
|
|
986
|
+
const change = modelEvent.changes.keys.get('execution_count');
|
|
987
|
+
changes.executionCountChange = {
|
|
988
|
+
oldValue: change!.oldValue,
|
|
989
|
+
newValue: this.ymodel.get('execution_count'),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return changes;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private _youtputs: Y.Array<IOutput>;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
class YAttachmentCell
|
|
1000
|
+
extends YBaseCell<IBaseCellMetadata>
|
|
1001
|
+
implements ISharedAttachmentsCell
|
|
1002
|
+
{
|
|
1003
|
+
/**
|
|
1004
|
+
* Cell attachments
|
|
1005
|
+
*/
|
|
1006
|
+
get attachments(): IAttachments | undefined {
|
|
1007
|
+
return this.getAttachments();
|
|
1008
|
+
}
|
|
1009
|
+
set attachments(v: IAttachments | undefined) {
|
|
1010
|
+
this.setAttachments(v);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Gets the cell attachments.
|
|
1015
|
+
*
|
|
1016
|
+
* @returns The cell attachments.
|
|
1017
|
+
*/
|
|
1018
|
+
getAttachments(): IAttachments | undefined {
|
|
1019
|
+
return this.ymodel.get('attachments');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Sets the cell attachments
|
|
1024
|
+
*
|
|
1025
|
+
* @param attachments: The cell attachments.
|
|
1026
|
+
*/
|
|
1027
|
+
setAttachments(attachments: IAttachments | undefined): void {
|
|
1028
|
+
this.transact(() => {
|
|
1029
|
+
if (attachments === null) {
|
|
1030
|
+
this.ymodel.delete('attachments');
|
|
1031
|
+
} else {
|
|
1032
|
+
this.ymodel.set('attachments', attachments);
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Extract changes from YJS events
|
|
1039
|
+
*
|
|
1040
|
+
* @param events YJS events
|
|
1041
|
+
* @returns Cell changes
|
|
1042
|
+
*/
|
|
1043
|
+
protected override getChanges(
|
|
1044
|
+
events: Y.YEvent<any>[],
|
|
1045
|
+
): Partial<CellChange<IBaseCellMetadata>> {
|
|
1046
|
+
const changes = super.getChanges(events);
|
|
1047
|
+
|
|
1048
|
+
const modelEvent = events.find((event) => event.target === this.ymodel) as
|
|
1049
|
+
| undefined
|
|
1050
|
+
| Y.YMapEvent<any>;
|
|
1051
|
+
|
|
1052
|
+
if (modelEvent && modelEvent.keysChanged.has('attachments')) {
|
|
1053
|
+
const change = modelEvent.changes.keys.get('attachments');
|
|
1054
|
+
changes.executionCountChange = {
|
|
1055
|
+
oldValue: change!.oldValue,
|
|
1056
|
+
newValue: this.ymodel.get('attachments'),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return changes;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Shareable raw cell.
|
|
1066
|
+
*/
|
|
1067
|
+
export class YRawCell extends YAttachmentCell implements ISharedRawCell {
|
|
1068
|
+
/**
|
|
1069
|
+
* Create a new YRawCell that works standalone. It cannot be
|
|
1070
|
+
* inserted into a YNotebook because the Yjs model is already
|
|
1071
|
+
* attached to an anonymous Y.Doc instance.
|
|
1072
|
+
*/
|
|
1073
|
+
static override createStandalone(id?: string): YRawCell {
|
|
1074
|
+
return super.createStandalone(id) as YRawCell;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* String identifying the type of cell.
|
|
1079
|
+
*/
|
|
1080
|
+
override get cell_type(): 'raw' {
|
|
1081
|
+
return 'raw';
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Serialize the model to JSON.
|
|
1086
|
+
*/
|
|
1087
|
+
override toJSON(): IRawCell {
|
|
1088
|
+
return {
|
|
1089
|
+
id: this.getId(),
|
|
1090
|
+
cell_type: 'raw',
|
|
1091
|
+
source: this.getSource(),
|
|
1092
|
+
metadata: this.getMetadata(),
|
|
1093
|
+
attachments: this.getAttachments(),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Shareable markdown cell.
|
|
1100
|
+
*/
|
|
1101
|
+
export class YMarkdownCell extends YAttachmentCell implements ISharedMarkdownCell {
|
|
1102
|
+
/**
|
|
1103
|
+
* Create a new YMarkdownCell that works standalone. It cannot be
|
|
1104
|
+
* inserted into a YNotebook because the Yjs model is already
|
|
1105
|
+
* attached to an anonymous Y.Doc instance.
|
|
1106
|
+
*/
|
|
1107
|
+
static override createStandalone(id?: string): YMarkdownCell {
|
|
1108
|
+
return super.createStandalone(id) as YMarkdownCell;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* String identifying the type of cell.
|
|
1113
|
+
*/
|
|
1114
|
+
override get cell_type(): 'markdown' {
|
|
1115
|
+
return 'markdown';
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Serialize the model to JSON.
|
|
1120
|
+
*/
|
|
1121
|
+
override toJSON(): IMarkdownCell {
|
|
1122
|
+
return {
|
|
1123
|
+
id: this.getId(),
|
|
1124
|
+
cell_type: 'markdown',
|
|
1125
|
+
source: this.getSource(),
|
|
1126
|
+
metadata: this.getMetadata(),
|
|
1127
|
+
attachments: this.getAttachments(),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Cell type.
|
|
1134
|
+
*/
|
|
1135
|
+
export type YCellType = YRawCell | YCodeCell | YMarkdownCell;
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Shared implementation of the Shared Document types.
|
|
1139
|
+
*
|
|
1140
|
+
* Shared cells can be inserted into a SharedNotebook.
|
|
1141
|
+
* Shared cells only start emitting events when they are connected to a SharedNotebook.
|
|
1142
|
+
*
|
|
1143
|
+
* "Standalone" cells must not be inserted into a (Shared)Notebook.
|
|
1144
|
+
* Standalone cells emit events immediately after they have been created, but they must not
|
|
1145
|
+
* be included into a (Shared)Notebook.
|
|
1146
|
+
*/
|
|
1147
|
+
export class YNotebook extends YDocument<NotebookChange> implements ISharedNotebook {
|
|
1148
|
+
protected _undoChangedEmitter = new Emitter<boolean>();
|
|
1149
|
+
/**
|
|
1150
|
+
* Signal triggered when a undo stack changed.
|
|
1151
|
+
*/
|
|
1152
|
+
get undoChanged(): Event<boolean> {
|
|
1153
|
+
return this._undoChangedEmitter.event;
|
|
1154
|
+
}
|
|
1155
|
+
protected _redoChangedEmitter = new Emitter<boolean>();
|
|
1156
|
+
/**
|
|
1157
|
+
* Signal triggered when a undo stack changed.
|
|
1158
|
+
*/
|
|
1159
|
+
get redoChanged(): Event<boolean> {
|
|
1160
|
+
return this._redoChangedEmitter.event;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Create a new YNotebook.
|
|
1165
|
+
*/
|
|
1166
|
+
static create(options: ISharedNotebook.IOptions = {}): ISharedNotebook {
|
|
1167
|
+
return new YNotebook(options);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
protected _canRedo = false;
|
|
1171
|
+
protected _canUndo = false;
|
|
1172
|
+
constructor(options: ISharedNotebook.IOptions = {}) {
|
|
1173
|
+
super();
|
|
1174
|
+
this._disableDocumentWideUndoRedo = options.disableDocumentWideUndoRedo ?? false;
|
|
1175
|
+
this.cellTypeAdaptor = options.cellTypeAdaptor ?? defaultCellTypeAdaptor;
|
|
1176
|
+
this.cells = this._ycells.toArray().map((ycell) => {
|
|
1177
|
+
if (!this._ycellMapping.has(ycell)) {
|
|
1178
|
+
this._ycellMapping.set(
|
|
1179
|
+
ycell,
|
|
1180
|
+
createCellModelFromSharedType(
|
|
1181
|
+
ycell,
|
|
1182
|
+
{ notebook: this },
|
|
1183
|
+
this.cellTypeAdaptor,
|
|
1184
|
+
),
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return this._ycellMapping.get(ycell) as YCellType;
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
this.undoManager.addToScope(this._ycells);
|
|
1191
|
+
this._ycells.observe(this._onYCellsChanged);
|
|
1192
|
+
this.ymeta.observe(this._onMetaChanged);
|
|
1193
|
+
this.undoManager.on('stack-item-updated', this.handleUndoChanged);
|
|
1194
|
+
this.undoManager.on('stack-item-added', this.handleUndoChanged);
|
|
1195
|
+
this.undoManager.on('stack-item-popped', this.handleUndoChanged);
|
|
1196
|
+
this.undoManager.on('stack-cleared', this.handleUndoChanged);
|
|
1197
|
+
this._canRedo = this.undoManager.canRedo();
|
|
1198
|
+
this._canUndo = this.undoManager.canUndo();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
protected handleUndoChanged = () => {
|
|
1202
|
+
const canRedo = this.undoManager.canRedo();
|
|
1203
|
+
if (this._canRedo !== canRedo) {
|
|
1204
|
+
this._canRedo = canRedo;
|
|
1205
|
+
this._redoChangedEmitter.fire(canRedo);
|
|
1206
|
+
}
|
|
1207
|
+
const canUndo = this.undoManager.canUndo();
|
|
1208
|
+
if (this._canUndo !== canUndo) {
|
|
1209
|
+
this._canUndo = canUndo;
|
|
1210
|
+
this._undoChangedEmitter.fire(canUndo);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
cellTypeAdaptor: CellTypeAdaptor = defaultCellTypeAdaptor;
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* YJS map for the notebook metadata
|
|
1218
|
+
*/
|
|
1219
|
+
readonly ymeta: Y.Map<any> = this.ydoc.getMap('meta');
|
|
1220
|
+
/**
|
|
1221
|
+
* Cells list
|
|
1222
|
+
*/
|
|
1223
|
+
readonly cells: YCellType[];
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Signal triggered when the cells list changes.
|
|
1227
|
+
*/
|
|
1228
|
+
get cellsChanged(): Event<IListChange> {
|
|
1229
|
+
return this._cellsChanged;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Wether the undo/redo logic should be
|
|
1234
|
+
* considered on the full document across all cells.
|
|
1235
|
+
*
|
|
1236
|
+
* Default: false
|
|
1237
|
+
*/
|
|
1238
|
+
get disableDocumentWideUndoRedo(): boolean {
|
|
1239
|
+
return this._disableDocumentWideUndoRedo;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Notebook metadata
|
|
1244
|
+
*/
|
|
1245
|
+
get metadata(): INotebookMetadata {
|
|
1246
|
+
return this.getMetadata();
|
|
1247
|
+
}
|
|
1248
|
+
set metadata(v: INotebookMetadata) {
|
|
1249
|
+
this.setMetadata(v);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Signal triggered when a metadata changes.
|
|
1254
|
+
*/
|
|
1255
|
+
get metadataChanged(): Event<IMapChange> {
|
|
1256
|
+
return this._metadataChanged;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* nbformat major version
|
|
1261
|
+
*/
|
|
1262
|
+
get nbformat(): number {
|
|
1263
|
+
return this.ymeta.get('nbformat');
|
|
1264
|
+
}
|
|
1265
|
+
set nbformat(value: number) {
|
|
1266
|
+
this.transact(() => {
|
|
1267
|
+
this.ymeta.set('nbformat', value);
|
|
1268
|
+
}, false);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* nbformat minor version
|
|
1273
|
+
*/
|
|
1274
|
+
get nbformat_minor(): number {
|
|
1275
|
+
return this.ymeta.get('nbformat_minor');
|
|
1276
|
+
}
|
|
1277
|
+
set nbformat_minor(value: number) {
|
|
1278
|
+
this.transact(() => {
|
|
1279
|
+
this.ymeta.set('nbformat_minor', value);
|
|
1280
|
+
}, false);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Dispose of the resources.
|
|
1285
|
+
*/
|
|
1286
|
+
override dispose(): void {
|
|
1287
|
+
if (this.isDisposed) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
this._ycells.unobserve(this._onYCellsChanged);
|
|
1291
|
+
this.ymeta.unobserve(this._onMetaChanged);
|
|
1292
|
+
this.undoManager.off('stack-item-updated', this.handleUndoChanged);
|
|
1293
|
+
this.undoManager.off('stack-item-added', this.handleUndoChanged);
|
|
1294
|
+
this.undoManager.off('stack-item-popped', this.handleUndoChanged);
|
|
1295
|
+
this.undoManager.off('stack-cleared', this.handleUndoChanged);
|
|
1296
|
+
super.dispose();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Get a shared cell by index.
|
|
1301
|
+
*
|
|
1302
|
+
* @param index: Cell's position.
|
|
1303
|
+
*
|
|
1304
|
+
* @returns The requested shared cell.
|
|
1305
|
+
*/
|
|
1306
|
+
getCell(index: number): YCellType {
|
|
1307
|
+
return this.cells[index];
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Add a shared cell at the notebook bottom.
|
|
1312
|
+
*
|
|
1313
|
+
* @param cell Cell to add.
|
|
1314
|
+
*
|
|
1315
|
+
* @returns The added cell.
|
|
1316
|
+
*/
|
|
1317
|
+
addCell(cell: SharedCell.Cell): YBaseCell<IBaseCellMetadata> {
|
|
1318
|
+
return this.insertCell(this._ycells.length, cell);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Insert a shared cell into a specific position.
|
|
1323
|
+
*
|
|
1324
|
+
* @param index: Cell's position.
|
|
1325
|
+
* @param cell: Cell to insert.
|
|
1326
|
+
*
|
|
1327
|
+
* @returns The inserted cell.
|
|
1328
|
+
*/
|
|
1329
|
+
insertCell(index: number, cell: SharedCell.Cell): YBaseCell<IBaseCellMetadata> {
|
|
1330
|
+
return this.insertCells(index, [cell])[0];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Insert a list of shared cells into a specific position.
|
|
1335
|
+
*
|
|
1336
|
+
* @param index: Position to insert the cells.
|
|
1337
|
+
* @param cells: Array of shared cells to insert.
|
|
1338
|
+
*
|
|
1339
|
+
* @returns The inserted cells.
|
|
1340
|
+
*/
|
|
1341
|
+
insertCells(index: number, cells: SharedCell.Cell[]): YBaseCell<IBaseCellMetadata>[] {
|
|
1342
|
+
const yCells = cells.map((c) => {
|
|
1343
|
+
const cell = createCell(c, this);
|
|
1344
|
+
this._ycellMapping.set(cell.ymodel, cell);
|
|
1345
|
+
return cell;
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
this.transact(() => {
|
|
1349
|
+
this._ycells.insert(
|
|
1350
|
+
index,
|
|
1351
|
+
yCells.map((cell) => cell.ymodel),
|
|
1352
|
+
);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
yCells.forEach((c) => {
|
|
1356
|
+
c.setUndoManager();
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
return yCells;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Move a cell.
|
|
1364
|
+
*
|
|
1365
|
+
* @param fromIndex: Index of the cell to move.
|
|
1366
|
+
* @param toIndex: New position of the cell.
|
|
1367
|
+
*/
|
|
1368
|
+
moveCell(fromIndex: number, toIndex: number): void {
|
|
1369
|
+
this.transact(() => {
|
|
1370
|
+
// FIXME we need to use yjs move feature to preserve undo history
|
|
1371
|
+
const clone = createCell(this.getCell(fromIndex).toJSON(), this);
|
|
1372
|
+
this._ycells.delete(fromIndex, 1);
|
|
1373
|
+
this._ycells.insert(toIndex, [clone.ymodel]);
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Remove a cell.
|
|
1379
|
+
*
|
|
1380
|
+
* @param index: Index of the cell to remove.
|
|
1381
|
+
*/
|
|
1382
|
+
deleteCell(index: number): void {
|
|
1383
|
+
this.deleteCellRange(index, index + 1);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Remove a range of cells.
|
|
1388
|
+
*
|
|
1389
|
+
* @param from: The start index of the range to remove (inclusive).
|
|
1390
|
+
* @param to: The end index of the range to remove (exclusive).
|
|
1391
|
+
*/
|
|
1392
|
+
deleteCellRange(from: number, to: number): void {
|
|
1393
|
+
// Cells will be removed from the mapping in the model event listener.
|
|
1394
|
+
this.transact(() => {
|
|
1395
|
+
this._ycells.delete(from, to - from);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Delete a metadata notebook.
|
|
1401
|
+
*
|
|
1402
|
+
* @param key The key to delete
|
|
1403
|
+
*/
|
|
1404
|
+
deleteMetadata(key: string): void {
|
|
1405
|
+
const allMetadata = deepCopy(this.ymeta.get('metadata'));
|
|
1406
|
+
delete allMetadata[key];
|
|
1407
|
+
this.setMetadata(allMetadata);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Returns some metadata associated with the notebook.
|
|
1412
|
+
*
|
|
1413
|
+
* If no `key` is provided, it will return all metadata.
|
|
1414
|
+
* Else it will return the value for that key.
|
|
1415
|
+
*
|
|
1416
|
+
* @param key Key to get from the metadata
|
|
1417
|
+
* @returns Notebook's metadata.
|
|
1418
|
+
*/
|
|
1419
|
+
getMetadata(key?: string): INotebookMetadata {
|
|
1420
|
+
const meta = this.ymeta.get('metadata');
|
|
1421
|
+
|
|
1422
|
+
if (typeof key === 'string') {
|
|
1423
|
+
return deepCopy(meta[key]);
|
|
1424
|
+
} else {
|
|
1425
|
+
return deepCopy(meta ?? {});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Sets some metadata associated with the notebook.
|
|
1431
|
+
*
|
|
1432
|
+
* If only one argument is provided, it will override all notebook metadata.
|
|
1433
|
+
* Otherwise a single key will be set to a new value.
|
|
1434
|
+
*
|
|
1435
|
+
* @param metadata All Notebook's metadata or the key to set.
|
|
1436
|
+
* @param value New metadata value
|
|
1437
|
+
*/
|
|
1438
|
+
setMetadata(metadata: INotebookMetadata | string, value?: PartialJSONValue): void {
|
|
1439
|
+
if (typeof metadata === 'string') {
|
|
1440
|
+
if (typeof value === 'undefined') {
|
|
1441
|
+
throw new TypeError(
|
|
1442
|
+
`Metadata value for ${metadata} cannot be 'undefined'; use deleteMetadata.`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
const update: Partial<INotebookMetadata> = {};
|
|
1446
|
+
update[metadata] = value;
|
|
1447
|
+
this.updateMetadata(update);
|
|
1448
|
+
} else {
|
|
1449
|
+
this.ymeta.set('metadata', deepCopy(metadata));
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Updates the metadata associated with the notebook.
|
|
1455
|
+
*
|
|
1456
|
+
* @param value: Metadata's attribute to update.
|
|
1457
|
+
*/
|
|
1458
|
+
updateMetadata(value: Partial<INotebookMetadata>): void {
|
|
1459
|
+
// TODO: Maybe modify only attributes instead of replacing the whole metadata?
|
|
1460
|
+
this.ymeta.set('metadata', { ...this.getMetadata(), ...value });
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Handle a change to the ystate.
|
|
1465
|
+
*/
|
|
1466
|
+
private _onMetaChanged = (event: Y.YMapEvent<any>) => {
|
|
1467
|
+
if (event.keysChanged.has('metadata')) {
|
|
1468
|
+
const change = event.changes.keys.get('metadata');
|
|
1469
|
+
const metadataChange = {
|
|
1470
|
+
oldValue: change?.oldValue ? change.oldValue : undefined,
|
|
1471
|
+
newValue: this.getMetadata(),
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
const oldValue = metadataChange.oldValue ?? {};
|
|
1475
|
+
const oldKeys = Object.keys(oldValue);
|
|
1476
|
+
const newKeys = Object.keys(metadataChange.newValue);
|
|
1477
|
+
for (const key of new Set(oldKeys.concat(newKeys))) {
|
|
1478
|
+
if (!oldKeys.includes(key)) {
|
|
1479
|
+
this._metadataChangedEmitter.fire({
|
|
1480
|
+
key,
|
|
1481
|
+
newValue: metadataChange.newValue[key],
|
|
1482
|
+
type: 'add',
|
|
1483
|
+
});
|
|
1484
|
+
} else if (!newKeys.includes(key)) {
|
|
1485
|
+
this._metadataChangedEmitter.fire({
|
|
1486
|
+
key,
|
|
1487
|
+
oldValue: metadataChange.oldValue[key],
|
|
1488
|
+
type: 'remove',
|
|
1489
|
+
});
|
|
1490
|
+
} else if (!deepEqual(oldValue[key], metadataChange.newValue[key]!)) {
|
|
1491
|
+
this._metadataChangedEmitter.fire({
|
|
1492
|
+
key,
|
|
1493
|
+
newValue: metadataChange.newValue[key],
|
|
1494
|
+
oldValue: metadataChange.oldValue[key],
|
|
1495
|
+
type: 'change',
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
this._changedEmitter.fire({ metadataChange });
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (event.keysChanged.has('nbformat')) {
|
|
1504
|
+
const change = event.changes.keys.get('nbformat');
|
|
1505
|
+
const nbformatChanged = {
|
|
1506
|
+
key: 'nbformat',
|
|
1507
|
+
oldValue: change?.oldValue ? change.oldValue : undefined,
|
|
1508
|
+
newValue: this.nbformat,
|
|
1509
|
+
};
|
|
1510
|
+
this._changedEmitter.fire({ nbformatChanged });
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (event.keysChanged.has('nbformat_minor')) {
|
|
1514
|
+
const change = event.changes.keys.get('nbformat_minor');
|
|
1515
|
+
const nbformatChanged = {
|
|
1516
|
+
key: 'nbformat_minor',
|
|
1517
|
+
oldValue: change?.oldValue ? change.oldValue : undefined,
|
|
1518
|
+
newValue: this.nbformat_minor,
|
|
1519
|
+
};
|
|
1520
|
+
this._changedEmitter.fire({ nbformatChanged });
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Handle a change to the list of cells.
|
|
1526
|
+
*/
|
|
1527
|
+
private _onYCellsChanged = (event: Y.YArrayEvent<Y.Map<any>>) => {
|
|
1528
|
+
// update the type cell mapping by iterating through the added/removed types
|
|
1529
|
+
event.changes.added.forEach((item) => {
|
|
1530
|
+
const type = (item.content as Y.ContentType).type as Y.Map<any>;
|
|
1531
|
+
if (!this._ycellMapping.has(type)) {
|
|
1532
|
+
const c = createCellModelFromSharedType(
|
|
1533
|
+
type,
|
|
1534
|
+
{ notebook: this },
|
|
1535
|
+
this.cellTypeAdaptor,
|
|
1536
|
+
);
|
|
1537
|
+
c.setUndoManager();
|
|
1538
|
+
this._ycellMapping.set(type, c);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
event.changes.deleted.forEach((item) => {
|
|
1542
|
+
const type = (item.content as Y.ContentType).type as Y.Map<any>;
|
|
1543
|
+
const model = this._ycellMapping.get(type);
|
|
1544
|
+
if (model) {
|
|
1545
|
+
model.dispose();
|
|
1546
|
+
this._ycellMapping.delete(type);
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
let index = 0;
|
|
1550
|
+
|
|
1551
|
+
// this reflects the event.changes.delta, but replaces the content of delta.insert with ycells
|
|
1552
|
+
const cellsChange: Delta<ISharedCell[]> = [];
|
|
1553
|
+
event.changes.delta.forEach((d: any) => {
|
|
1554
|
+
if (d.insert !== null) {
|
|
1555
|
+
const insertedCells = d.insert.map((ycell: Y.Map<any>) =>
|
|
1556
|
+
this._ycellMapping.get(ycell),
|
|
1557
|
+
);
|
|
1558
|
+
cellsChange.push({ insert: insertedCells });
|
|
1559
|
+
this.cells.splice(index, 0, ...insertedCells);
|
|
1560
|
+
|
|
1561
|
+
this._cellsChangedEmitter.fire({
|
|
1562
|
+
type: 'add',
|
|
1563
|
+
newIndex: index,
|
|
1564
|
+
newValues: insertedCells,
|
|
1565
|
+
oldIndex: -2,
|
|
1566
|
+
oldValues: [],
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
index += d.insert.length;
|
|
1570
|
+
} else if (d.delete !== null) {
|
|
1571
|
+
cellsChange.push(d);
|
|
1572
|
+
const oldValues = this.cells.splice(index, d.delete);
|
|
1573
|
+
|
|
1574
|
+
this._cellsChangedEmitter.fire({
|
|
1575
|
+
type: 'remove',
|
|
1576
|
+
newIndex: -1,
|
|
1577
|
+
newValues: [],
|
|
1578
|
+
oldIndex: index,
|
|
1579
|
+
oldValues,
|
|
1580
|
+
});
|
|
1581
|
+
} else if (d.retain !== null) {
|
|
1582
|
+
cellsChange.push(d);
|
|
1583
|
+
index += d.retain;
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
this._changedEmitter.fire({
|
|
1588
|
+
cellsChange: cellsChange,
|
|
1589
|
+
});
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
protected _cellsChangedEmitter = new Emitter<IListChange>();
|
|
1593
|
+
protected _cellsChanged = this._cellsChangedEmitter.event;
|
|
1594
|
+
protected _metadataChangedEmitter = new Emitter<IMapChange>();
|
|
1595
|
+
protected _metadataChanged = this._metadataChangedEmitter.event;
|
|
1596
|
+
/**
|
|
1597
|
+
* Internal Yjs cells list
|
|
1598
|
+
*/
|
|
1599
|
+
protected readonly _ycells: Y.Array<Y.Map<any>> = this.ydoc.getArray('cells');
|
|
1600
|
+
|
|
1601
|
+
private _disableDocumentWideUndoRedo: boolean;
|
|
1602
|
+
private _ycellMapping: WeakMap<Y.Map<any>, YCellType> = new WeakMap();
|
|
1603
|
+
}
|