1 | // Copyright (c) Jupyter Development Team.
|
2 | // Distributed under the terms of the Modified BSD License.
|
3 | import { ObservableMap } from '@jupyterlab/observables';
|
4 | import * as models from '@jupyter/ydoc';
|
5 | import { ArrayExt, ArrayIterator, each, toArray } from '@lumino/algorithm';
|
6 | import { Signal } from '@lumino/signaling';
|
7 | /**
|
8 | * A cell list object that supports undo/redo.
|
9 | */
|
10 | export class CellList {
|
11 | /**
|
12 | * Construct the cell list.
|
13 | */
|
14 | constructor(modelDB, factory, model) {
|
15 | /**
|
16 | * Prevents that the modeldb event handler is executed when the shared-model event handler is executed and vice-versa.
|
17 | */
|
18 | this._mutex = models.createMutex();
|
19 | this._isDisposed = false;
|
20 | this._changed = new Signal(this);
|
21 | this._factory = factory;
|
22 | this._cellOrder = modelDB.createList('cellOrder');
|
23 | this._cellMap = new ObservableMap();
|
24 | this._cellOrder.changed.connect(this._onOrderChanged, this);
|
25 | this.nbmodel = model;
|
26 | this.nbmodel.changed.connect(this.onSharedModelChanged, this);
|
27 | this.changed.connect(this.onModelDBChanged, this);
|
28 | }
|
29 | onModelDBChanged(self, change) {
|
30 | this._mutex(() => {
|
31 | const nbmodel = this.nbmodel;
|
32 | nbmodel.transact(() => {
|
33 | if (change.type === 'set' || change.type === 'remove') {
|
34 | nbmodel.deleteCellRange(change.oldIndex, change.oldIndex + change.oldValues.length);
|
35 | }
|
36 | if (change.type === 'set' ||
|
37 | change.type === 'add' ||
|
38 | change.type === 'move') {
|
39 | let insertLocation = change.newIndex;
|
40 | if (change.type === 'move' && insertLocation > change.oldIndex) {
|
41 | insertLocation += change.oldValues.length;
|
42 | }
|
43 | const cells = nbmodel.insertCells(insertLocation, change.newValues.map(cell => {
|
44 | return cell.sharedModel.toJSON();
|
45 | }));
|
46 | change.newValues.forEach((cell, index) => {
|
47 | cell.switchSharedModel(cells[index], false);
|
48 | });
|
49 | }
|
50 | if (change.type === 'move') {
|
51 | let from = change.oldIndex;
|
52 | if (from >= change.newIndex) {
|
53 | from += change.oldValues.length;
|
54 | }
|
55 | nbmodel.deleteCellRange(from, from + change.oldValues.length);
|
56 | }
|
57 | });
|
58 | });
|
59 | }
|
60 | onSharedModelChanged(self, change) {
|
61 | this._mutex(() => {
|
62 | var _a;
|
63 | let currpos = 0;
|
64 | (_a = change.cellsChange) === null || _a === void 0 ? void 0 : _a.forEach(delta => {
|
65 | if (delta.insert != null) {
|
66 | const cells = delta.insert.map(nbcell => {
|
67 | const cell = this._factory.createCell(nbcell.cell_type, { id: nbcell.id });
|
68 | cell.switchSharedModel(nbcell, true);
|
69 | return cell;
|
70 | });
|
71 | this.insertAll(currpos, cells);
|
72 | currpos += delta.insert.length;
|
73 | }
|
74 | else if (delta.delete != null) {
|
75 | this.removeRange(currpos, currpos + delta.delete);
|
76 | }
|
77 | else if (delta.retain != null) {
|
78 | currpos += delta.retain;
|
79 | }
|
80 | });
|
81 | });
|
82 | }
|
83 | /**
|
84 | * A signal emitted when the cell list has changed.
|
85 | */
|
86 | get changed() {
|
87 | return this._changed;
|
88 | }
|
89 | /**
|
90 | * Test whether the cell list has been disposed.
|
91 | */
|
92 | get isDisposed() {
|
93 | return this._isDisposed;
|
94 | }
|
95 | /**
|
96 | * Test whether the list is empty.
|
97 | *
|
98 | * @returns `true` if the cell list is empty, `false` otherwise.
|
99 | *
|
100 | * #### Notes
|
101 | * This is a read-only property.
|
102 | *
|
103 | * #### Complexity
|
104 | * Constant.
|
105 | *
|
106 | * #### Iterator Validity
|
107 | * No changes.
|
108 | */
|
109 | get isEmpty() {
|
110 | return this._cellOrder.length === 0;
|
111 | }
|
112 | /**
|
113 | * Get the length of the cell list.
|
114 | *
|
115 | * @return The number of cells in the cell list.
|
116 | *
|
117 | * #### Notes
|
118 | * This is a read-only property.
|
119 | *
|
120 | * #### Complexity
|
121 | * Constant.
|
122 | *
|
123 | * #### Iterator Validity
|
124 | * No changes.
|
125 | */
|
126 | get length() {
|
127 | return this._cellOrder.length;
|
128 | }
|
129 | /**
|
130 | * Create an iterator over the cells in the cell list.
|
131 | *
|
132 | * @returns A new iterator starting at the front of the cell list.
|
133 | *
|
134 | * #### Complexity
|
135 | * Constant.
|
136 | *
|
137 | * #### Iterator Validity
|
138 | * No changes.
|
139 | */
|
140 | iter() {
|
141 | const arr = [];
|
142 | for (const id of toArray(this._cellOrder)) {
|
143 | arr.push(this._cellMap.get(id));
|
144 | }
|
145 | return new ArrayIterator(arr);
|
146 | }
|
147 | /**
|
148 | * Dispose of the resources held by the cell list.
|
149 | */
|
150 | dispose() {
|
151 | if (this._isDisposed) {
|
152 | return;
|
153 | }
|
154 | this._isDisposed = true;
|
155 | Signal.clearData(this);
|
156 | // Clean up the cell map and cell order objects.
|
157 | for (const cell of this._cellMap.values()) {
|
158 | cell.dispose();
|
159 | }
|
160 | this._cellMap.dispose();
|
161 | this._cellOrder.dispose();
|
162 | }
|
163 | /**
|
164 | * Get the cell at the specified index.
|
165 | *
|
166 | * @param index - The positive integer index of interest.
|
167 | *
|
168 | * @returns The cell at the specified index.
|
169 | *
|
170 | * #### Complexity
|
171 | * Constant.
|
172 | *
|
173 | * #### Iterator Validity
|
174 | * No changes.
|
175 | *
|
176 | * #### Undefined Behavior
|
177 | * An `index` which is non-integral or out of range.
|
178 | */
|
179 | get(index) {
|
180 | return this._cellMap.get(this._cellOrder.get(index));
|
181 | }
|
182 | /**
|
183 | * Set the cell at the specified index.
|
184 | *
|
185 | * @param index - The positive integer index of interest.
|
186 | *
|
187 | * @param cell - The cell to set at the specified index.
|
188 | *
|
189 | * #### Complexity
|
190 | * Constant.
|
191 | *
|
192 | * #### Iterator Validity
|
193 | * No changes.
|
194 | *
|
195 | * #### Undefined Behavior
|
196 | * An `index` which is non-integral or out of range.
|
197 | *
|
198 | * #### Notes
|
199 | * This should be considered to transfer ownership of the
|
200 | * cell to the `CellList`. As such, `cell.dispose()` should
|
201 | * not be called by other actors.
|
202 | */
|
203 | set(index, cell) {
|
204 | // Set the internal data structures.
|
205 | this._cellMap.set(cell.id, cell);
|
206 | this._cellOrder.set(index, cell.id);
|
207 | }
|
208 | /**
|
209 | * Add a cell to the back of the cell list.
|
210 | *
|
211 | * @param cell - The cell to add to the back of the cell list.
|
212 | *
|
213 | * @returns The new length of the cell list.
|
214 | *
|
215 | * #### Complexity
|
216 | * Constant.
|
217 | *
|
218 | * #### Iterator Validity
|
219 | * No changes.
|
220 | *
|
221 | * #### Notes
|
222 | * This should be considered to transfer ownership of the
|
223 | * cell to the `CellList`. As such, `cell.dispose()` should
|
224 | * not be called by other actors.
|
225 | */
|
226 | push(cell) {
|
227 | // Set the internal data structures.
|
228 | this._cellMap.set(cell.id, cell);
|
229 | const num = this._cellOrder.push(cell.id);
|
230 | return num;
|
231 | }
|
232 | /**
|
233 | * Insert a cell into the cell list at a specific index.
|
234 | *
|
235 | * @param index - The index at which to insert the cell.
|
236 | *
|
237 | * @param cell - The cell to set at the specified index.
|
238 | *
|
239 | * @returns The new length of the cell list.
|
240 | *
|
241 | * #### Complexity
|
242 | * Linear.
|
243 | *
|
244 | * #### Iterator Validity
|
245 | * No changes.
|
246 | *
|
247 | * #### Notes
|
248 | * The `index` will be clamped to the bounds of the cell list.
|
249 | *
|
250 | * #### Undefined Behavior
|
251 | * An `index` which is non-integral.
|
252 | *
|
253 | * #### Notes
|
254 | * This should be considered to transfer ownership of the
|
255 | * cell to the `CellList`. As such, `cell.dispose()` should
|
256 | * not be called by other actors.
|
257 | */
|
258 | insert(index, cell) {
|
259 | // Set the internal data structures.
|
260 | this._cellMap.set(cell.id, cell);
|
261 | this._cellOrder.insert(index, cell.id);
|
262 | }
|
263 | /**
|
264 | * Remove the first occurrence of a cell from the cell list.
|
265 | *
|
266 | * @param cell - The cell of interest.
|
267 | *
|
268 | * @returns The index of the removed cell, or `-1` if the cell
|
269 | * is not contained in the cell list.
|
270 | *
|
271 | * #### Complexity
|
272 | * Linear.
|
273 | *
|
274 | * #### Iterator Validity
|
275 | * Iterators pointing at the removed cell and beyond are invalidated.
|
276 | */
|
277 | removeValue(cell) {
|
278 | const index = ArrayExt.findFirstIndex(toArray(this._cellOrder), id => this._cellMap.get(id) === cell);
|
279 | this.remove(index);
|
280 | return index;
|
281 | }
|
282 | /**
|
283 | * Remove and return the cell at a specific index.
|
284 | *
|
285 | * @param index - The index of the cell of interest.
|
286 | *
|
287 | * @returns The cell at the specified index, or `undefined` if the
|
288 | * index is out of range.
|
289 | *
|
290 | * #### Complexity
|
291 | * Constant.
|
292 | *
|
293 | * #### Iterator Validity
|
294 | * Iterators pointing at the removed cell and beyond are invalidated.
|
295 | *
|
296 | * #### Undefined Behavior
|
297 | * An `index` which is non-integral.
|
298 | */
|
299 | remove(index) {
|
300 | const id = this._cellOrder.get(index);
|
301 | this._cellOrder.remove(index);
|
302 | const cell = this._cellMap.get(id);
|
303 | return cell;
|
304 | }
|
305 | /**
|
306 | * Remove all cells from the cell list.
|
307 | *
|
308 | * #### Complexity
|
309 | * Linear.
|
310 | *
|
311 | * #### Iterator Validity
|
312 | * All current iterators are invalidated.
|
313 | */
|
314 | clear() {
|
315 | this._cellOrder.clear();
|
316 | }
|
317 | /**
|
318 | * Move a cell from one index to another.
|
319 | *
|
320 | * @parm fromIndex - The index of the element to move.
|
321 | *
|
322 | * @param toIndex - The index to move the element to.
|
323 | *
|
324 | * #### Complexity
|
325 | * Constant.
|
326 | *
|
327 | * #### Iterator Validity
|
328 | * Iterators pointing at the lesser of the `fromIndex` and the `toIndex`
|
329 | * and beyond are invalidated.
|
330 | *
|
331 | * #### Undefined Behavior
|
332 | * A `fromIndex` or a `toIndex` which is non-integral.
|
333 | */
|
334 | move(fromIndex, toIndex) {
|
335 | this._cellOrder.move(fromIndex, toIndex);
|
336 | }
|
337 | /**
|
338 | * Push a set of cells to the back of the cell list.
|
339 | *
|
340 | * @param cells - An iterable or array-like set of cells to add.
|
341 | *
|
342 | * @returns The new length of the cell list.
|
343 | *
|
344 | * #### Complexity
|
345 | * Linear.
|
346 | *
|
347 | * #### Iterator Validity
|
348 | * No changes.
|
349 | *
|
350 | * #### Notes
|
351 | * This should be considered to transfer ownership of the
|
352 | * cells to the `CellList`. As such, `cell.dispose()` should
|
353 | * not be called by other actors.
|
354 | */
|
355 | pushAll(cells) {
|
356 | const newValues = toArray(cells);
|
357 | each(newValues, cell => {
|
358 | // Set the internal data structures.
|
359 | this._cellMap.set(cell.id, cell);
|
360 | this._cellOrder.push(cell.id);
|
361 | });
|
362 | return this.length;
|
363 | }
|
364 | /**
|
365 | * Insert a set of items into the cell list at the specified index.
|
366 | *
|
367 | * @param index - The index at which to insert the cells.
|
368 | *
|
369 | * @param cells - The cells to insert at the specified index.
|
370 | *
|
371 | * @returns The new length of the cell list.
|
372 | *
|
373 | * #### Complexity.
|
374 | * Linear.
|
375 | *
|
376 | * #### Iterator Validity
|
377 | * No changes.
|
378 | *
|
379 | * #### Notes
|
380 | * The `index` will be clamped to the bounds of the cell list.
|
381 | *
|
382 | * #### Undefined Behavior.
|
383 | * An `index` which is non-integral.
|
384 | *
|
385 | * #### Notes
|
386 | * This should be considered to transfer ownership of the
|
387 | * cells to the `CellList`. As such, `cell.dispose()` should
|
388 | * not be called by other actors.
|
389 | */
|
390 | insertAll(index, cells) {
|
391 | const newValues = toArray(cells);
|
392 | each(newValues, cell => {
|
393 | this._cellMap.set(cell.id, cell);
|
394 | // @todo it looks like this compound operation should start before the `each` loop.
|
395 | this._cellOrder.beginCompoundOperation();
|
396 | this._cellOrder.insert(index++, cell.id);
|
397 | this._cellOrder.endCompoundOperation();
|
398 | });
|
399 | return this.length;
|
400 | }
|
401 | /**
|
402 | * Remove a range of items from the cell list.
|
403 | *
|
404 | * @param startIndex - The start index of the range to remove (inclusive).
|
405 | *
|
406 | * @param endIndex - The end index of the range to remove (exclusive).
|
407 | *
|
408 | * @returns The new length of the cell list.
|
409 | *
|
410 | * #### Complexity
|
411 | * Linear.
|
412 | *
|
413 | * #### Iterator Validity
|
414 | * Iterators pointing to the first removed cell and beyond are invalid.
|
415 | *
|
416 | * #### Undefined Behavior
|
417 | * A `startIndex` or `endIndex` which is non-integral.
|
418 | */
|
419 | removeRange(startIndex, endIndex) {
|
420 | this._cellOrder.removeRange(startIndex, endIndex);
|
421 | return this.length;
|
422 | }
|
423 | /**
|
424 | * Whether the object can redo changes.
|
425 | */
|
426 | get canRedo() {
|
427 | return this.nbmodel.canRedo();
|
428 | }
|
429 | /**
|
430 | * Whether the object can undo changes.
|
431 | */
|
432 | get canUndo() {
|
433 | return this.nbmodel.canUndo();
|
434 | }
|
435 | /**
|
436 | * Begin a compound operation.
|
437 | *
|
438 | * @param isUndoAble - Whether the operation is undoable.
|
439 | * The default is `true`.
|
440 | */
|
441 | beginCompoundOperation(isUndoAble) {
|
442 | this._cellOrder.beginCompoundOperation(isUndoAble);
|
443 | }
|
444 | /**
|
445 | * End a compound operation.
|
446 | */
|
447 | endCompoundOperation() {
|
448 | this._cellOrder.endCompoundOperation();
|
449 | }
|
450 | /**
|
451 | * Undo an operation.
|
452 | */
|
453 | undo() {
|
454 | this.nbmodel.undo();
|
455 | }
|
456 | /**
|
457 | * Redo an operation.
|
458 | */
|
459 | redo() {
|
460 | this.nbmodel.redo();
|
461 | }
|
462 | /**
|
463 | * Clear the change stack.
|
464 | */
|
465 | clearUndo() {
|
466 | this.nbmodel.clearUndoHistory();
|
467 | }
|
468 | _onOrderChanged(order, change) {
|
469 | if (change.type === 'add' || change.type === 'set') {
|
470 | each(change.newValues, id => {
|
471 | const existingCell = this._cellMap.get(id);
|
472 | if (existingCell == null) {
|
473 | const cellDB = this._factory.modelDB;
|
474 | const cellType = cellDB.createValue(id + '.type');
|
475 | let cell;
|
476 | switch (cellType.get()) {
|
477 | case 'code':
|
478 | cell = this._factory.createCodeCell({ id: id });
|
479 | break;
|
480 | case 'markdown':
|
481 | cell = this._factory.createMarkdownCell({ id: id });
|
482 | break;
|
483 | default:
|
484 | cell = this._factory.createRawCell({ id: id });
|
485 | break;
|
486 | }
|
487 | this._cellMap.set(id, cell);
|
488 | }
|
489 | else if (!existingCell.sharedModel.isStandalone) {
|
490 | this._mutex(() => {
|
491 | // it does already exist, probably because it was deleted previously and we introduced it
|
492 | // copy it to a fresh codecell instance
|
493 | const cell = existingCell.toJSON();
|
494 | let freshCell = null;
|
495 | switch (cell.cell_type) {
|
496 | case 'code':
|
497 | freshCell = this._factory.createCodeCell({
|
498 | cell,
|
499 | id: cell.id
|
500 | });
|
501 | break;
|
502 | case 'markdown':
|
503 | freshCell = this._factory.createMarkdownCell({
|
504 | cell,
|
505 | id: cell.id
|
506 | });
|
507 | break;
|
508 | default:
|
509 | freshCell = this._factory.createRawCell({
|
510 | cell,
|
511 | id: cell.id
|
512 | });
|
513 | break;
|
514 | }
|
515 | this._cellMap.set(id, freshCell);
|
516 | });
|
517 | }
|
518 | });
|
519 | }
|
520 | const newValues = [];
|
521 | const oldValues = [];
|
522 | each(change.newValues, id => {
|
523 | newValues.push(this._cellMap.get(id));
|
524 | });
|
525 | each(change.oldValues, id => {
|
526 | oldValues.push(this._cellMap.get(id));
|
527 | });
|
528 | this._changed.emit({
|
529 | type: change.type,
|
530 | oldIndex: change.oldIndex,
|
531 | newIndex: change.newIndex,
|
532 | oldValues,
|
533 | newValues
|
534 | });
|
535 | }
|
536 | }
|
537 | //# sourceMappingURL=celllist.js.map |
\ | No newline at end of file |