ymodels.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import { ISignal, Signal } from '@lumino/signaling';
  6. import { UUID } from '@lumino/coreutils';
  7. import * as Y from 'yjs';
  8. import * as nbformat from '@jupyterlab/nbformat';
  9. import * as models from './api';
  10. import { Delta } from './api';
  11. import { Awareness } from 'y-protocols/awareness';
  12. const deepCopy = (o: any) => JSON.parse(JSON.stringify(o));
  13. /**
  14. * Abstract interface to define Shared Models that can be bound to a text editor using any existing
  15. * Yjs-based editor binding.
  16. */
  17. export interface IYText extends models.ISharedText {
  18. readonly ysource: Y.Text;
  19. readonly awareness: Awareness | null;
  20. readonly undoManager: Y.UndoManager | null;
  21. }
  22. export type YCellType = YRawCell | YCodeCell | YMarkdownCell;
  23. export class YDocument<T> implements models.ISharedDocument {
  24. /**
  25. * Perform a transaction. While the function f is called, all changes to the shared
  26. * document are bundled into a single event.
  27. */
  28. transact(f: () => void, undoable = true): void {
  29. this.ydoc.transact(f, undoable ? this : null);
  30. }
  31. /**
  32. * Dispose of the resources.
  33. */
  34. dispose(): void {
  35. this.isDisposed = true;
  36. this.ydoc.destroy();
  37. }
  38. /**
  39. * Whether the object can undo changes.
  40. */
  41. canUndo(): boolean {
  42. return this.undoManager.undoStack.length > 0;
  43. }
  44. /**
  45. * Whether the object can redo changes.
  46. */
  47. canRedo(): boolean {
  48. return this.undoManager.redoStack.length > 0;
  49. }
  50. /**
  51. * Undo an operation.
  52. */
  53. undo(): void {
  54. this.undoManager.undo();
  55. }
  56. /**
  57. * Redo an operation.
  58. */
  59. redo(): void {
  60. this.undoManager.redo();
  61. }
  62. /**
  63. * Clear the change stack.
  64. */
  65. clearUndoHistory(): void {
  66. this.undoManager.clear();
  67. }
  68. /**
  69. * The changed signal.
  70. */
  71. get changed(): ISignal<this, T> {
  72. return this._changed;
  73. }
  74. public isDisposed = false;
  75. public ydoc = new Y.Doc();
  76. public source = this.ydoc.getText('source');
  77. public undoManager = new Y.UndoManager([this.source], {
  78. trackedOrigins: new Set([this])
  79. });
  80. public awareness = new Awareness(this.ydoc);
  81. protected _changed = new Signal<this, T>(this);
  82. }
  83. export class YFile
  84. extends YDocument<models.FileChange>
  85. implements models.ISharedFile, models.ISharedText, IYText {
  86. constructor() {
  87. super();
  88. this.ysource.observe(this._modelObserver);
  89. }
  90. /**
  91. * Handle a change to the ymodel.
  92. */
  93. private _modelObserver = (event: Y.YTextEvent) => {
  94. const changes: models.FileChange = {};
  95. changes.sourceChange = event.changes.delta as any;
  96. this._changed.emit(changes);
  97. };
  98. public static create(): YFile {
  99. return new YFile();
  100. }
  101. /**
  102. * Gets cell's source.
  103. *
  104. * @returns Cell's source.
  105. */
  106. public getSource(): string {
  107. return this.ysource.toString();
  108. }
  109. /**
  110. * Sets cell's source.
  111. *
  112. * @param value: New source.
  113. */
  114. public setSource(value: string): void {
  115. this.transact(() => {
  116. const ytext = this.ysource;
  117. ytext.delete(0, ytext.length);
  118. ytext.insert(0, value);
  119. });
  120. }
  121. /**
  122. * Replace content from `start' to `end` with `value`.
  123. *
  124. * @param start: The start index of the range to replace (inclusive).
  125. *
  126. * @param end: The end index of the range to replace (exclusive).
  127. *
  128. * @param value: New source (optional).
  129. */
  130. public updateSource(start: number, end: number, value = ''): void {
  131. this.transact(() => {
  132. const ysource = this.ysource;
  133. // insert and then delete.
  134. // This ensures that the cursor position is adjusted after the replaced content.
  135. ysource.insert(start, value);
  136. ysource.delete(start + value.length, end - start);
  137. });
  138. }
  139. public ysource = this.ydoc.getText('source');
  140. }
  141. /**
  142. * Shared implementation of the Shared Document types.
  143. *
  144. * Shared cells can be inserted into a SharedNotebook.
  145. * Shared cells only start emitting events when they are connected to a SharedNotebook.
  146. *
  147. * "Standalone" cells must not be inserted into a (Shared)Notebook.
  148. * Standalone cells emit events immediately after they have been created, but they must not
  149. * be included into a (Shared)Notebook.
  150. */
  151. export class YNotebook
  152. extends YDocument<models.NotebookChange>
  153. implements models.ISharedNotebook {
  154. constructor() {
  155. super();
  156. this.ycells.observe(this._onYCellsChanged);
  157. this.cells = this.ycells.toArray().map(ycell => {
  158. if (!this._ycellMapping.has(ycell)) {
  159. this._ycellMapping.set(ycell, createCellFromType(ycell));
  160. }
  161. return this._ycellMapping.get(ycell) as YCellType;
  162. });
  163. }
  164. /**
  165. * Get a shared cell by index.
  166. *
  167. * @param index: Cell's position.
  168. *
  169. * @returns The requested shared cell.
  170. */
  171. getCell(index: number): YCellType {
  172. return this.cells[index];
  173. }
  174. /**
  175. * Insert a shared cell into a specific position.
  176. *
  177. * @param index: Cell's position.
  178. *
  179. * @param cell: Cell to insert.
  180. */
  181. insertCell(index: number, cell: YCellType): void {
  182. this.insertCells(index, [cell]);
  183. }
  184. /**
  185. * Insert a list of shared cells into a specific position.
  186. *
  187. * @param index: Position to insert the cells.
  188. *
  189. * @param cells: Array of shared cells to insert.
  190. */
  191. insertCells(index: number, cells: YCellType[]): void {
  192. cells.forEach(cell => {
  193. this._ycellMapping.set(cell.ymodel, cell);
  194. // cell.yawareness = this.yawareness;
  195. // cell.yUndoManager = this.yUndoManager;
  196. });
  197. this.transact(() => {
  198. this.ycells.insert(
  199. index,
  200. cells.map(cell => cell.ymodel)
  201. );
  202. });
  203. }
  204. /**
  205. * Move a cell.
  206. *
  207. * @param fromIndex: Index of the cell to move.
  208. *
  209. * @param toIndex: New position of the cell.
  210. */
  211. moveCell(fromIndex: number, toIndex: number): void {
  212. this.transact(() => {
  213. const fromCell: any = this.getCell(fromIndex).clone();
  214. this.deleteCell(fromIndex);
  215. this.insertCell(toIndex, fromCell);
  216. });
  217. }
  218. /**
  219. * Remove a cell.
  220. *
  221. * @param index: Index of the cell to remove.
  222. */
  223. deleteCell(index: number): void {
  224. this.deleteCellRange(index, index + 1);
  225. }
  226. /**
  227. * Remove a range of cells.
  228. *
  229. * @param from: The start index of the range to remove (inclusive).
  230. *
  231. * @param to: The end index of the range to remove (exclusive).
  232. */
  233. deleteCellRange(from: number, to: number): void {
  234. this.transact(() => {
  235. this.ycells.delete(from, to - from);
  236. });
  237. }
  238. /**
  239. * Returns the metadata associated with the notebook.
  240. *
  241. * @returns Notebook's metadata.
  242. */
  243. getMetadata(): nbformat.INotebookMetadata {
  244. const meta = this.ymeta.get('metadata');
  245. return meta ? deepCopy(meta) : { orig_nbformat: 1 };
  246. }
  247. /**
  248. * Sets the metadata associated with the notebook.
  249. *
  250. * @param metadata: Notebook's metadata.
  251. */
  252. setMetadata(value: nbformat.INotebookMetadata): void {
  253. this.ymeta.set('metadata', deepCopy(value));
  254. }
  255. /**
  256. * Updates the metadata associated with the notebook.
  257. *
  258. * @param value: Metadata's attribute to update.
  259. */
  260. updateMetadata(value: Partial<nbformat.INotebookMetadata>): void {
  261. this.ymeta.set('metadata', Object.assign({}, this.getMetadata(), value));
  262. }
  263. /**
  264. * Create a new YNotebook.
  265. */
  266. public static create(): models.ISharedNotebook {
  267. return new YNotebook();
  268. }
  269. /**
  270. * Dispose of the resources.
  271. */
  272. dispose(): void {
  273. this.ycells.unobserve(this._onYCellsChanged);
  274. }
  275. /**
  276. * Handle a change to the list of cells.
  277. */
  278. private _onYCellsChanged = (event: Y.YArrayEvent<Y.Map<any>>) => {
  279. // update the type⇔cell mapping by iterating through the addded/removed types
  280. event.changes.added.forEach(item => {
  281. const type = (item.content as Y.ContentType).type as Y.Map<any>;
  282. if (!this._ycellMapping.has(type)) {
  283. this._ycellMapping.set(type, createCellFromType(type));
  284. }
  285. const cell = this._ycellMapping.get(type) as any;
  286. cell._notebook = this;
  287. cell._undoManager = this.undoManager;
  288. });
  289. event.changes.deleted.forEach(item => {
  290. const type = (item.content as Y.ContentType).type as Y.Map<any>;
  291. const model = this._ycellMapping.get(type);
  292. if (model) {
  293. model.dispose();
  294. this._ycellMapping.delete(type);
  295. }
  296. });
  297. let index = 0;
  298. // this reflects the event.changes.delta, but replaces the content of delta.insert with ycells
  299. const cellsChange: Delta<models.ISharedCell[]> = [];
  300. event.changes.delta.forEach((d: any) => {
  301. if (d.insert != null) {
  302. const insertedCells = d.insert.map((ycell: Y.Map<any>) =>
  303. this._ycellMapping.get(ycell)
  304. );
  305. cellsChange.push({ insert: insertedCells });
  306. this.cells.splice(index, 0, ...insertedCells);
  307. index += d.insert.length;
  308. } else if (d.delete != null) {
  309. cellsChange.push(d);
  310. this.cells.splice(index, d.delete);
  311. } else if (d.retain != null) {
  312. cellsChange.push(d);
  313. index += d.retain;
  314. }
  315. });
  316. this._changed.emit({
  317. cellsChange: cellsChange
  318. });
  319. };
  320. public ycells: Y.Array<Y.Map<any>> = this.ydoc.getArray('cells');
  321. public ymeta: Y.Map<any> = this.ydoc.getMap('meta');
  322. public ymodel: Y.Map<any> = this.ydoc.getMap('model');
  323. public undoManager = new Y.UndoManager([this.ycells], {
  324. trackedOrigins: new Set([this])
  325. });
  326. private _ycellMapping: Map<Y.Map<any>, YCellType> = new Map();
  327. public nbformat_minor: number = nbformat.MINOR_VERSION;
  328. public nbformat: number = nbformat.MAJOR_VERSION;
  329. public cells: YCellType[];
  330. }
  331. /**
  332. * Create a new shared cell given the type.
  333. */
  334. export const createCellFromType = (type: Y.Map<any>): YCellType => {
  335. switch (type.get('cell_type')) {
  336. case 'code':
  337. return new YCodeCell(type);
  338. case 'markdown':
  339. return new YMarkdownCell(type);
  340. case 'raw':
  341. return new YRawCell(type);
  342. default:
  343. throw new Error('Found unknown cell type');
  344. }
  345. };
  346. /**
  347. * Create a new standalone cell given the type.
  348. */
  349. export const createStandaloneCell = (
  350. cellType: 'raw' | 'code' | 'markdown',
  351. id?: string
  352. ): YCellType => {
  353. switch (cellType) {
  354. case 'markdown':
  355. return YMarkdownCell.createStandalone(id);
  356. case 'code':
  357. return YCodeCell.createStandalone(id);
  358. default:
  359. // raw
  360. return YRawCell.createStandalone(id);
  361. }
  362. };
  363. export class YBaseCell<Metadata extends models.ISharedBaseCellMetadata>
  364. implements models.ISharedBaseCell<Metadata>, IYText {
  365. constructor(ymodel: Y.Map<any>) {
  366. this.ymodel = ymodel;
  367. const ysource = ymodel.get('source');
  368. this._prevSourceLength = ysource ? ysource.length : 0;
  369. this.ymodel.observeDeep(this._modelObserver);
  370. }
  371. get ysource(): Y.Text {
  372. return this.ymodel.get('source');
  373. }
  374. get awareness(): Awareness | null {
  375. return this.notebook?.awareness || null;
  376. }
  377. /**
  378. * Perform a transaction. While the function f is called, all changes to the shared
  379. * document are bundled into a single event.
  380. */
  381. transact(f: () => void, undoable = true): void {
  382. this.notebook && undoable
  383. ? this.notebook.transact(f)
  384. : this.ymodel.doc!.transact(f, this);
  385. }
  386. /**
  387. * The notebook that this cell belongs to.
  388. */
  389. get undoManager(): Y.UndoManager | null {
  390. return this.notebook ? this.notebook.undoManager : this._undoManager;
  391. }
  392. /**
  393. * Undo an operation.
  394. */
  395. undo(): void {
  396. this.undoManager?.undo();
  397. }
  398. /**
  399. * Redo an operation.
  400. */
  401. redo(): void {
  402. this.undoManager?.redo();
  403. }
  404. /**
  405. * Whether the object can undo changes.
  406. */
  407. canUndo(): boolean {
  408. return !!this.undoManager && this.undoManager.undoStack.length > 0;
  409. }
  410. /**
  411. * Whether the object can redo changes.
  412. */
  413. canRedo(): boolean {
  414. return !!this.undoManager && this.undoManager.redoStack.length > 0;
  415. }
  416. /**
  417. * Clear the change stack.
  418. */
  419. clearUndoHistory(): void {
  420. this.undoManager?.clear();
  421. }
  422. /**
  423. * The notebook that this cell belongs to.
  424. */
  425. get notebook(): YNotebook | null {
  426. return this._notebook;
  427. }
  428. /**
  429. * The notebook that this cell belongs to.
  430. */
  431. protected _notebook: YNotebook | null = null;
  432. /**
  433. * Whether the cell is standalone or not.
  434. *
  435. * If the cell is standalone. It cannot be
  436. * inserted into a YNotebook because the Yjs model is already
  437. * attached to an anonymous Y.Doc instance.
  438. */
  439. isStandalone = false;
  440. /**
  441. * Create a new YRawCell that can be inserted into a YNotebook
  442. */
  443. public static create(id = UUID.uuid4()): YBaseCell<any> {
  444. const ymodel = new Y.Map();
  445. const ysource = new Y.Text();
  446. ymodel.set('source', ysource);
  447. ymodel.set('metadata', {});
  448. ymodel.set('cell_type', this.prototype.cell_type);
  449. ymodel.set('id', id);
  450. return new this(ymodel);
  451. }
  452. /**
  453. * Create a new YRawCell that works standalone. It cannot be
  454. * inserted into a YNotebook because the Yjs model is already
  455. * attached to an anonymous Y.Doc instance.
  456. */
  457. public static createStandalone(id?: string): YBaseCell<any> {
  458. const cell = this.create(id);
  459. cell.isStandalone = true;
  460. new Y.Doc().getArray().insert(0, [cell.ymodel]);
  461. cell._undoManager = new Y.UndoManager([cell.ymodel], {
  462. trackedOrigins: new Set([cell])
  463. });
  464. return cell;
  465. }
  466. /**
  467. * Clone the cell.
  468. *
  469. * @todo clone should only be available in the specific implementations i.e. ISharedCodeCell
  470. */
  471. public clone(): YBaseCell<any> {
  472. const ymodel = new Y.Map();
  473. const ysource = new Y.Text(this.getSource());
  474. ymodel.set('source', ysource);
  475. ymodel.set('metadata', this.getMetadata());
  476. ymodel.set('cell_type', this.cell_type);
  477. ymodel.set('id', this.getId());
  478. const Self: any = this.constructor;
  479. return new Self(ymodel);
  480. }
  481. /**
  482. * Handle a change to the ymodel.
  483. */
  484. private _modelObserver = (events: Y.YEvent[]) => {
  485. const changes: models.CellChange<Metadata> = {};
  486. const sourceEvent = events.find(
  487. event => event.target === this.ymodel.get('source')
  488. );
  489. if (sourceEvent) {
  490. changes.sourceChange = sourceEvent.changes.delta as any;
  491. }
  492. const outputEvent = events.find(
  493. event => event.target === this.ymodel.get('outputs')
  494. );
  495. if (outputEvent) {
  496. changes.outputsChange = outputEvent.changes.delta as any;
  497. }
  498. const modelEvent = events.find(event => event.target === this.ymodel) as
  499. | undefined
  500. | Y.YMapEvent<any>;
  501. if (modelEvent && modelEvent.keysChanged.has('metadata')) {
  502. const change = modelEvent.changes.keys.get('metadata');
  503. changes.metadataChange = {
  504. oldValue: change?.oldValue ? change!.oldValue : undefined,
  505. newValue: this.getMetadata()
  506. };
  507. }
  508. if (modelEvent && modelEvent.keysChanged.has('execution_count')) {
  509. const change = modelEvent.changes.keys.get('execution_count');
  510. changes.executionCountChange = {
  511. oldValue: change!.oldValue,
  512. newValue: this.ymodel.get('execution_count')
  513. };
  514. }
  515. // The model allows us to replace the complete source with a new string. We express this in the Delta format
  516. // as a replace of the complete string.
  517. const ysource = this.ymodel.get('source');
  518. if (modelEvent && modelEvent.keysChanged.has('source')) {
  519. changes.sourceChange = [
  520. { delete: this._prevSourceLength },
  521. { insert: ysource.toString() }
  522. ];
  523. }
  524. this._prevSourceLength = ysource.length;
  525. this._changed.emit(changes);
  526. };
  527. /**
  528. * The changed signal.
  529. */
  530. get changed(): ISignal<this, models.CellChange<Metadata>> {
  531. return this._changed;
  532. }
  533. /**
  534. * Dispose of the resources.
  535. */
  536. dispose(): void {
  537. this.ymodel.unobserveDeep(this._modelObserver);
  538. }
  539. /**
  540. * Gets the cell attachments.
  541. *
  542. * @returns The cell attachments.
  543. */
  544. public getAttachments(): nbformat.IAttachments | undefined {
  545. return this.ymodel.get('attachments');
  546. }
  547. /**
  548. * Sets the cell attachments
  549. *
  550. * @param attchments: The cell attachments.
  551. */
  552. public setAttachments(value: nbformat.IAttachments | undefined): void {
  553. this.transact(() => {
  554. if (value == null) {
  555. this.ymodel.set('attachments', value);
  556. } else {
  557. this.ymodel.delete('attachments');
  558. }
  559. });
  560. }
  561. /**
  562. * Get cell id.
  563. *
  564. * @returns Cell id
  565. */
  566. public getId(): string {
  567. return this.ymodel.get('id');
  568. }
  569. /**
  570. * Gets cell's source.
  571. *
  572. * @returns Cell's source.
  573. */
  574. public getSource(): string {
  575. return this.ymodel.get('source').toString();
  576. }
  577. /**
  578. * Sets cell's source.
  579. *
  580. * @param value: New source.
  581. */
  582. public setSource(value: string): void {
  583. const ytext = this.ymodel.get('source');
  584. this.transact(() => {
  585. ytext.delete(0, ytext.length);
  586. ytext.insert(0, value);
  587. });
  588. // @todo Do we need proper replace semantic? This leads to issues in editor bindings because they don't switch source.
  589. // this.ymodel.set('source', new Y.Text(value));
  590. }
  591. /**
  592. * Replace content from `start' to `end` with `value`.
  593. *
  594. * @param start: The start index of the range to replace (inclusive).
  595. *
  596. * @param end: The end index of the range to replace (exclusive).
  597. *
  598. * @param value: New source (optional).
  599. */
  600. public updateSource(start: number, end: number, value = ''): void {
  601. this.transact(() => {
  602. const ysource = this.ysource;
  603. // insert and then delete.
  604. // This ensures that the cursor position is adjusted after the replaced content.
  605. ysource.insert(start, value);
  606. ysource.delete(start + value.length, end - start);
  607. });
  608. }
  609. /**
  610. * The type of the cell.
  611. */
  612. get cell_type(): any {
  613. throw new Error('A YBaseCell must not be constructed');
  614. }
  615. /**
  616. * Returns the metadata associated with the notebook.
  617. *
  618. * @returns Notebook's metadata.
  619. */
  620. getMetadata(): Partial<Metadata> {
  621. return deepCopy(this.ymodel.get('metadata'));
  622. }
  623. /**
  624. * Sets the metadata associated with the notebook.
  625. *
  626. * @param metadata: Notebook's metadata.
  627. */
  628. setMetadata(value: Partial<Metadata>): void {
  629. this.transact(() => {
  630. this.ymodel.set('metadata', deepCopy(value));
  631. });
  632. }
  633. /**
  634. * Serialize the model to JSON.
  635. */
  636. toJSON(): nbformat.IBaseCell {
  637. return {
  638. id: this.getId(),
  639. cell_type: this.cell_type,
  640. source: this.getSource(),
  641. metadata: this.getMetadata()
  642. };
  643. }
  644. public isDisposed = false;
  645. public ymodel: Y.Map<any>;
  646. private _undoManager: Y.UndoManager | null = null;
  647. private _changed = new Signal<this, models.CellChange<Metadata>>(this);
  648. private _prevSourceLength: number;
  649. }
  650. export class YCodeCell
  651. extends YBaseCell<models.ISharedBaseCellMetadata>
  652. implements models.ISharedCodeCell {
  653. /**
  654. * The type of the cell.
  655. */
  656. get cell_type(): 'code' {
  657. return 'code';
  658. }
  659. /**
  660. * The code cell's prompt number. Will be null if the cell has not been run.
  661. */
  662. get execution_count(): number | null {
  663. return this.ymodel.get('execution_count');
  664. }
  665. /**
  666. * The code cell's prompt number. Will be null if the cell has not been run.
  667. */
  668. set execution_count(count: number | null) {
  669. this.transact(() => {
  670. this.ymodel.set('execution_count', count);
  671. });
  672. }
  673. /**
  674. * Execution, display, or stream outputs.
  675. */
  676. getOutputs(): Array<nbformat.IOutput> {
  677. return deepCopy(this.ymodel.get('outputs').toArray());
  678. }
  679. /**
  680. * Replace all outputs.
  681. */
  682. setOutputs(outputs: Array<nbformat.IOutput>): void {
  683. const youtputs = this.ymodel.get('outputs') as Y.Array<nbformat.IOutput>;
  684. this.transact(() => {
  685. youtputs.delete(0, youtputs.length);
  686. youtputs.insert(0, outputs);
  687. });
  688. }
  689. /**
  690. * Replace content from `start' to `end` with `outputs`.
  691. *
  692. * @param start: The start index of the range to replace (inclusive).
  693. *
  694. * @param end: The end index of the range to replace (exclusive).
  695. *
  696. * @param outputs: New outputs (optional).
  697. */
  698. updateOutputs(
  699. start: number,
  700. end: number,
  701. outputs: Array<nbformat.IOutput> = []
  702. ): void {
  703. const youtputs = this.ymodel.get('outputs') as Y.Array<nbformat.IOutput>;
  704. const fin = end < youtputs.length ? end - start : youtputs.length - start;
  705. this.transact(() => {
  706. youtputs.delete(start, fin);
  707. youtputs.insert(start, outputs);
  708. });
  709. }
  710. /**
  711. * Create a new YCodeCell that can be inserted into a YNotebook
  712. */
  713. public static create(id?: string): YCodeCell {
  714. const cell = super.create(id);
  715. cell.ymodel.set('execution_count', 0); // for some default value
  716. cell.ymodel.set('outputs', new Y.Array<nbformat.IOutput>());
  717. return cell as any;
  718. }
  719. /**
  720. * Create a new YCodeCell that works standalone. It cannot be
  721. * inserted into a YNotebook because the Yjs model is already
  722. * attached to an anonymous Y.Doc instance.
  723. */
  724. public static createStandalone(id?: string): YCodeCell {
  725. const cell = super.createStandalone(id);
  726. cell.ymodel.set('execution_count', null); // for some default value
  727. cell.ymodel.set('outputs', new Y.Array<nbformat.IOutput>());
  728. return cell as any;
  729. }
  730. /**
  731. * Create a new YCodeCell that can be inserted into a YNotebook
  732. *
  733. * @todo clone should only be available in the specific implementations i.e. ISharedCodeCell
  734. */
  735. public clone(): YCodeCell {
  736. const cell = super.clone();
  737. const youtputs = new Y.Array<nbformat.IOutput>();
  738. youtputs.insert(0, this.getOutputs());
  739. cell.ymodel.set('execution_count', this.execution_count); // for some default value
  740. cell.ymodel.set('outputs', youtputs);
  741. return cell as any;
  742. }
  743. /**
  744. * Serialize the model to JSON.
  745. */
  746. toJSON(): nbformat.ICodeCell {
  747. return {
  748. id: this.getId(),
  749. cell_type: 'code',
  750. source: this.getSource(),
  751. metadata: this.getMetadata(),
  752. outputs: this.getOutputs(),
  753. execution_count: this.execution_count
  754. };
  755. }
  756. }
  757. export class YRawCell
  758. extends YBaseCell<models.ISharedBaseCellMetadata>
  759. implements models.ISharedRawCell {
  760. /**
  761. * Create a new YRawCell that can be inserted into a YNotebook
  762. */
  763. public static create(id?: string): YRawCell {
  764. return super.create(id) as any;
  765. }
  766. /**
  767. * Create a new YRawCell that works standalone. It cannot be
  768. * inserted into a YNotebook because the Yjs model is already
  769. * attached to an anonymous Y.Doc instance.
  770. */
  771. public static createStandalone(id?: string): YRawCell {
  772. return super.createStandalone(id) as any;
  773. }
  774. /**
  775. * String identifying the type of cell.
  776. */
  777. get cell_type(): 'raw' {
  778. return 'raw';
  779. }
  780. /**
  781. * Serialize the model to JSON.
  782. */
  783. toJSON(): nbformat.IRawCell {
  784. return {
  785. id: this.getId(),
  786. cell_type: 'raw',
  787. source: this.getSource(),
  788. metadata: this.getMetadata(),
  789. attachments: this.getAttachments()
  790. };
  791. }
  792. }
  793. export class YMarkdownCell
  794. extends YBaseCell<models.ISharedBaseCellMetadata>
  795. implements models.ISharedMarkdownCell {
  796. /**
  797. * Create a new YMarkdownCell that can be inserted into a YNotebook
  798. */
  799. public static create(id?: string): YMarkdownCell {
  800. return super.create(id) as any;
  801. }
  802. /**
  803. * Create a new YMarkdownCell that works standalone. It cannot be
  804. * inserted into a YNotebook because the Yjs model is already
  805. * attached to an anonymous Y.Doc instance.
  806. */
  807. public static createStandalone(id?: string): YMarkdownCell {
  808. return super.createStandalone(id) as any;
  809. }
  810. /**
  811. * String identifying the type of cell.
  812. */
  813. get cell_type(): 'markdown' {
  814. return 'markdown';
  815. }
  816. /**
  817. * Serialize the model to JSON.
  818. */
  819. toJSON(): nbformat.IMarkdownCell {
  820. return {
  821. id: this.getId(),
  822. cell_type: 'markdown',
  823. source: this.getSource(),
  824. metadata: this.getMetadata(),
  825. attachments: this.getAttachments()
  826. };
  827. }
  828. }
  829. export default YNotebook;