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