UNPKG

16.7 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { ObservableMap } from '@jupyterlab/observables';
4import * as models from '@jupyter/ydoc';
5import { ArrayExt, ArrayIterator, each, toArray } from '@lumino/algorithm';
6import { Signal } from '@lumino/signaling';
7/**
8 * A cell list object that supports undo/redo.
9 */
10export 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