@marimo-team/islands 0.25.3-dev209 → 0.25.3-dev210

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.
Files changed (140) hide show
  1. package/dist/{ConnectedDataExplorerComponent-ApTYh0V3.js → ConnectedDataExplorerComponent-D6RHJV8D.js} +114 -112
  2. package/dist/{ErrorBoundary-B1QlXN-y.js → ErrorBoundary-VruaQLnK.js} +10 -10
  3. package/dist/{abnfDiagram-VCTEODGH-uo9xhZY0.js → abnfDiagram-VCTEODGH-0q7Lbmii.js} +2 -2
  4. package/dist/{any-language-editor-BVFHDDQ3.js → any-language-editor-jGaR0EAo.js} +5 -5
  5. package/dist/{architectureDiagram-5GKGNRK7-Hz6eyeOD.js → architectureDiagram-5GKGNRK7-1PydKPmE.js} +1 -1
  6. package/dist/{button-BQRdqQi1.js → button-B6RjDKJx.js} +54 -51
  7. package/dist/{capabilities-C2QUNRXi.js → capabilities-CkVzWJEX.js} +1 -1
  8. package/dist/{chat-ui-BTA1I5i1.js → chat-ui-B5pH_aXk.js} +414 -413
  9. package/dist/{check-C9Mp9eCK.js → check-Z_vtI3OL.js} +1 -1
  10. package/dist/{chunk-IMKFNOWR-DBM2i23k.js → chunk-IMKFNOWR-DEzZZ1Zc.js} +1 -1
  11. package/dist/{chunk-SHT3W25Y-L8KQLLBy.js → chunk-SHT3W25Y-CuHj-hnn.js} +1 -1
  12. package/dist/{chunk-SVP7TREG-BOUJKJEP.js → chunk-SVP7TREG-BPiZK2bA.js} +1 -1
  13. package/dist/{chunk-TICWLB2K-DY3CzFjX.js → chunk-TICWLB2K-DF6XcIjM.js} +1 -1
  14. package/dist/{classDiagram-ZZMXUADV-s9iwW-q4.js → classDiagram-ZZMXUADV-CMdR-cP4.js} +2 -2
  15. package/dist/{classDiagram-v2-VYDZK3BY-2GI5CjGB.js → classDiagram-v2-VYDZK3BY-DRvc3_7Y.js} +2 -2
  16. package/dist/{code-visibility-CCGlDSq4.js → code-visibility-BdFhHfIC.js} +28 -28
  17. package/dist/{common-C1PmVtVW.js → common-Ce1SgsX1.js} +8034 -8027
  18. package/dist/{toDate-BZZ64K_m.js → config-DbjzjwxX.js} +98 -164
  19. package/dist/{copy-icon-De584pZ4.js → copy-icon-CG57cxwb.js} +3 -3
  20. package/dist/createReducer-DePYJ0F3.js +63 -0
  21. package/dist/{cynefinDiagram-5FMLGOSQ-DOxnhBK2.js → cynefinDiagram-5FMLGOSQ-BkBjEY9l.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-D1AhEfpM.js → dagre-GXQ25YYZ-D-HEaW4I.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-B3KXkZvo.js → data-grid-overlay-editor-Bps1p6jQ.js} +1 -1
  24. package/dist/{diagram-S7CK7UJ4-DV_GebF_.js → diagram-S7CK7UJ4-Bn-oF-rB.js} +1 -1
  25. package/dist/{diagram-UQ7AKVKN-6dNzGFfD.js → diagram-UQ7AKVKN-BpzzXWBj.js} +1 -1
  26. package/dist/{diagram-VX7I27RA-DUh9jsfi.js → diagram-VX7I27RA-COOtvfBP.js} +1 -1
  27. package/dist/{diagram-Z3DM3KII-Cug-qcQ8.js → diagram-Z3DM3KII-K5cXvbrl.js} +1 -1
  28. package/dist/{dialog-DVRfQuhD.js → dialog-DsZ49mmU.js} +26 -26
  29. package/dist/{dist-BJtmSgcE.js → dist-11T5wq3_.js} +41 -40
  30. package/dist/{dist-B1M0bGO3.js → dist-17ugwGZc.js} +59 -58
  31. package/dist/{dist-D7mrh5sO.js → dist-7cRjf--y.js} +135 -134
  32. package/dist/{dist-Ck_C32ye.js → dist-B8pkfkzc.js} +31 -30
  33. package/dist/{dist-Ba2VIn1K.js → dist-B9UoM_wq.js} +54 -53
  34. package/dist/{dist-KN80qWCP.js → dist-BGuMTs1i.js} +118 -117
  35. package/dist/{dist-T5rh6JvL.js → dist-BHW11fni.js} +18 -17
  36. package/dist/dist-BLY4m1Wi.js +850 -0
  37. package/dist/{dist-Bj4bmwUm.js → dist-BNlcVoxs.js} +31 -30
  38. package/dist/{dist-BA13RJj5.js → dist-BQYe5Bju.js} +47 -46
  39. package/dist/{dist-OUle2RT7.js → dist-BX1mn8na.js} +297 -297
  40. package/dist/{dist-CTi5FTiV.js → dist-Bh0PTKUW.js} +108 -107
  41. package/dist/{dist-D9Q3xjiP.js → dist-CKaXK92p.js} +56 -55
  42. package/dist/{dist-DYXM73-k.js → dist-CLhwfhpJ.js} +58 -57
  43. package/dist/{dist-BmS-ZGjl.js → dist-CYeqK4Xw.js} +1854 -2702
  44. package/dist/{dist-D1fnFp1J.js → dist-Cxlp3WH6.js} +16 -15
  45. package/dist/{dist-DVVwOfrD.js → dist-DSJPGj42.js} +64 -63
  46. package/dist/{dist-Bzogh7CW.js → dist-DU9ICsWZ.js} +93 -92
  47. package/dist/{dist-DjeybPOb.js → dist-DWv0ALBi.js} +47 -46
  48. package/dist/{dist-D3x9__Fu.js → dist-DiBpWEsB.js} +39 -38
  49. package/dist/{dist-DtGn9N4h.js → dist-DiLFTlnl.js} +165 -165
  50. package/dist/{dist-DTv9KkpW.js → dist-cVDV5IuI.js} +26 -25
  51. package/dist/{dist-DP60Ubh8.js → dist-fRfaOoq8.js} +44 -43
  52. package/dist/{dist-O0qFpEJh.js → dist-gTTI-9Px.js} +66 -65
  53. package/dist/{dropdown-menu-dlEm41Bt.js → dropdown-menu-CSNdPSnS.js} +2886 -1529
  54. package/dist/{ebnfDiagram-PWID7BFC-d5LKx7Vb.js → ebnfDiagram-PWID7BFC-BI1ZmjVm.js} +2 -2
  55. package/dist/{erDiagram-RLTQ6QDP-BwvlkcID.js → erDiagram-RLTQ6QDP-BtWjj9xy.js} +1 -1
  56. package/dist/{error-banner-DxJ3s8YO.js → error-banner-IsonnvA0.js} +47 -46
  57. package/dist/error-entries-CRPfxMqm.js +2505 -0
  58. package/dist/{esm-Bbw_Njjx.js → esm-BgKjMvWw.js} +389 -388
  59. package/dist/{esm-mDQNEFe_.js → esm-CHjVSexr.js} +305 -306
  60. package/dist/{feedback-button-UsovT9sL.js → feedback-button-2ycmR9GG.js} +112 -112
  61. package/dist/{flowDiagram-HODETNUW-CJcSoFer.js → flowDiagram-HODETNUW-DQvZh96A.js} +2 -2
  62. package/dist/{formats-Ry7c-aIV.js → formats-BK4VefeF.js} +6 -6
  63. package/dist/{glide-data-editor-CRd8kBSp.js → glide-data-editor-Buj2OZjL.js} +99 -99
  64. package/dist/{glide-utils-D98laidb.js → glide-utils-CQddGpa-.js} +3 -3
  65. package/dist/{globals-BDWrOstF.js → globals-B-14fUeb.js} +3 -3
  66. package/dist/{error-entries-CVUHA5r_.js → html-to-image-CgAnxFi8.js} +26314 -25474
  67. package/dist/{infoDiagram-27XIBGKW-Cezgppw3.js → infoDiagram-27XIBGKW-BdAObnc_.js} +1 -1
  68. package/dist/{ishikawaDiagram-5VMMS53U-BahZQwJz.js → ishikawaDiagram-5VMMS53U-DP-jE8dw.js} +1 -1
  69. package/dist/{kanban-definition-UXKFOSKX-Cvfn_Obm.js → kanban-definition-UXKFOSKX-BkMgPjfm.js} +1 -1
  70. package/dist/{loader-DOVB0Syl.js → loader-BisjOLiW.js} +1 -1
  71. package/dist/main.js +2996 -2992
  72. package/dist/{mermaid-BrdwbArF.js → mermaid-B-OFqheA.js} +36 -36
  73. package/dist/{mindmap-definition-YA3MSWOX-uB4B2d-J.js → mindmap-definition-YA3MSWOX-Du7toAZX.js} +1 -1
  74. package/dist/{number-overlay-editor-Bp9a4z5Q.js → number-overlay-editor-ZVFcJ7_u.js} +1 -1
  75. package/dist/{dist-CThPVKaP.js → objectWithoutPropertiesLoose-ai5NUqGa.js} +1139 -1120
  76. package/dist/{pegDiagram-XKGWAZYB-CYaIwAmH.js → pegDiagram-XKGWAZYB-CX_729j0.js} +2 -2
  77. package/dist/{pieDiagram-E7YTZNPT-BJ1oUxVv.js → pieDiagram-E7YTZNPT-dtTFJcVI.js} +1 -1
  78. package/dist/{process-output-SjJxB1wh.js → process-output-Bmev5azU.js} +5 -5
  79. package/dist/{railroadDiagram-O6MQD6OU-BqKSaBux.js → railroadDiagram-O6MQD6OU-Dsc2Cf4c.js} +2 -2
  80. package/dist/{requirementDiagram-BXWQKSXE-DwFhgDhH.js → requirementDiagram-BXWQKSXE-D3g0c60I.js} +1 -1
  81. package/dist/{reveal-component-ov3pS-oO.js → reveal-component-BWz1KjWe.js} +725 -725
  82. package/dist/schemas-CdscWNy_.js +4747 -0
  83. package/dist/select-BSxlITBo.js +1200 -0
  84. package/dist/{sequenceDiagram-WJ2MYXX4-L7ENjfKF.js → sequenceDiagram-WJ2MYXX4-w1pWbIpk.js} +1 -1
  85. package/dist/{spec-BcJ9ncAY.js → spec-Dq8RNplp.js} +4 -4
  86. package/dist/{stateDiagram-D77RDMKH-CVf88u-p.js → stateDiagram-D77RDMKH-BCv7kP4H.js} +2 -2
  87. package/dist/{stateDiagram-v2-MP3YSRHH-DvpkQ1V6.js → stateDiagram-v2-MP3YSRHH-D1cTOt0x.js} +2 -2
  88. package/dist/static-state-BldGd99T.js +72 -0
  89. package/dist/strings-BGW-fXVJ.js +507 -0
  90. package/dist/{swimlanes-42K2YHIH-DIuFHbVE.js → swimlanes-42K2YHIH-DNO6jDnd.js} +1 -1
  91. package/dist/{swimlanesDiagram-VR7AAH4N-CAqHAziW.js → swimlanesDiagram-VR7AAH4N-CkSWsxYJ.js} +2 -2
  92. package/dist/{swiper-component-BVfnhbnt.js → swiper-component-Dir1o8ml.js} +2 -2
  93. package/dist/{timeline-definition-24CTP7MA-xO4EQqBm.js → timeline-definition-24CTP7MA-DQvqGFM9.js} +1 -1
  94. package/dist/toast-BWIPi3aF.js +491 -0
  95. package/dist/{tooltip-s4zZxwKe.js → tooltip-C3UnvsRe.js} +58 -58
  96. package/dist/use-restore-focus-BWO0z1lo.js +1672 -0
  97. package/dist/{useAsyncData-Dqw22wZQ.js → useAsyncData-DsZMvNXn.js} +1 -1
  98. package/dist/{useDateFormatter-D3Tkfzej.js → useDateFormatter-ChB4eyim.js} +2 -2
  99. package/dist/{useDeepCompareMemoize-D3gozcjJ.js → useDeepCompareMemoize-KMBBux8C.js} +1 -1
  100. package/dist/{useIframeCapabilities-6a_yDNXp.js → useIframeCapabilities-cMNQ80CK.js} +1 -1
  101. package/dist/useLifecycle-BquH9ET-.js +112 -0
  102. package/dist/useTheme-B9C1JIy9.js +307 -0
  103. package/dist/utils-DXkj82sK.js +813 -0
  104. package/dist/{vega-component-Blvspl7v.js → vega-component-9NOEITbg.js} +55 -54
  105. package/dist/{vennDiagram-4TSXK5OY-DzCi-lRh.js → vennDiagram-4TSXK5OY-BfzXp4SH.js} +1 -1
  106. package/dist/{wardleyDiagram-VM6X3IG4-CW48xa0n.js → wardleyDiagram-VM6X3IG4-CiqEE8eq.js} +1 -1
  107. package/dist/{xychartDiagram-S5SC5T6Z-UPxGGI2B.js → xychartDiagram-S5SC5T6Z-BTIHCVOI.js} +1 -1
  108. package/package.json +1 -1
  109. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +1 -1
  110. package/src/components/editor/cell/code/cell-editor.tsx +13 -36
  111. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +1 -1
  112. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +11 -5
  113. package/src/components/editor/notebook-cell.tsx +0 -1
  114. package/src/components/scratchpad/scratchpad.test.tsx +3 -11
  115. package/src/components/scratchpad/scratchpad.tsx +24 -22
  116. package/src/components/scratchpad/state.ts +7 -0
  117. package/src/core/ai/__tests__/staged-cells.test.ts +6 -8
  118. package/src/core/ai/staged-cells.ts +4 -2
  119. package/src/core/cells/__tests__/cells.test.ts +44 -60
  120. package/src/core/cells/__tests__/document.test.ts +1 -139
  121. package/src/core/cells/__tests__/notebook-outline.test.ts +8 -6
  122. package/src/core/cells/actions.ts +39 -0
  123. package/src/core/cells/cells.ts +15 -35
  124. package/src/core/cells/document.ts +9 -84
  125. package/src/core/codemirror/__tests__/format.test.ts +0 -34
  126. package/src/core/codemirror/cells/extensions.ts +20 -18
  127. package/src/core/codemirror/cells/state.ts +0 -5
  128. package/src/core/codemirror/format.ts +1 -19
  129. package/src/core/codemirror/misc/__tests__/paste.test.ts +3 -1
  130. package/src/core/codemirror/misc/paste.ts +1 -1
  131. package/src/core/notebook/document/__tests__/codemirror.test.ts +172 -0
  132. package/src/stories/layout/vertical/one-column.stories.tsx +26 -18
  133. package/dist/html-to-image--E3uFbJ9.js +0 -3348
  134. package/dist/label-Qvw1E97W.js +0 -281
  135. package/dist/objectWithoutPropertiesLoose-BRDcTsfB.js +0 -23
  136. package/dist/select-2KOiReNY.js +0 -1423
  137. package/dist/toast-B5t9o6s_.js +0 -1848
  138. package/dist/use-restore-focus-BgDTd2tc.js +0 -6418
  139. package/dist/useLifecycle-CaZi4wTw.js +0 -172
  140. package/dist/useTheme-TzPgWXwB.js +0 -1114
@@ -95,7 +95,7 @@ interface UpdateStagedCellAction {
95
95
  /** Manage staged cells without assuming which AI workflow owns them. */
96
96
  export function useStagedCells(store: JotaiStore) {
97
97
  const { addStagedCell, removeStagedCell } = useStagedAICellsActions();
98
- const { updateCellCode, createCell } = useCellActions();
98
+ const { setCellCode, createCell } = useCellActions();
99
99
  const deleteCellCallback = useDeleteCellCallback();
100
100
 
101
101
  const createStagedCell = (code: string): CellId => {
@@ -114,6 +114,8 @@ export function useStagedCells(store: JotaiStore) {
114
114
  return;
115
115
  }
116
116
 
117
+ // The AI may change the cell's language, and only the editor infers
118
+ // one. Without a mounted editor the code arrives through the document.
117
119
  const editorView = getCellEditorView(cellId);
118
120
  if (editorView) {
119
121
  const update = prepareCellCodeUpdate(editorView.state, code, {
@@ -123,7 +125,7 @@ export function useStagedCells(store: JotaiStore) {
123
125
  editorView.dispatch(update);
124
126
  }
125
127
  } else {
126
- updateCellCode({ cellId, code, formattingChange: false });
128
+ setCellCode({ cellId, code });
127
129
  }
128
130
  };
129
131
 
@@ -178,6 +178,38 @@ describe("cell reducer", () => {
178
178
  return id;
179
179
  }
180
180
 
181
+ /** Edit a cell's code as the document delivers it. */
182
+ function editCode(cellId: CellId, code: string) {
183
+ const cells = state.cellIds.map((id) => {
184
+ const cell = state.cellData[id];
185
+ return {
186
+ id,
187
+ code: id === cellId ? code : cell.code,
188
+ name: cell.name,
189
+ config: {
190
+ column: cell.config.column ?? null,
191
+ disabled: cell.config.disabled ?? false,
192
+ hide_code: cell.config.hide_code ?? false,
193
+ expand_output: cell.config.expand_output ?? false,
194
+ },
195
+ };
196
+ });
197
+ actions.reconcileDocument({
198
+ order: state.cellIds,
199
+ cells: new Map(cells.map((cell) => [cell.id, cell])),
200
+ });
201
+ }
202
+
203
+ /** Format a cell's code, as the editor reports it. */
204
+ function formatCode(cellId: CellId, code: string) {
205
+ actions.codeFormatted({
206
+ cellId,
207
+ before: state.cellData[cellId].code,
208
+ after: code,
209
+ });
210
+ editCode(cellId, code);
211
+ }
212
+
181
213
  let i = 0;
182
214
  const originalCreate = CellId.create.bind(CellId);
183
215
 
@@ -200,11 +232,7 @@ describe("cell reducer", () => {
200
232
  });
201
233
 
202
234
  it("can update a cell", () => {
203
- actions.updateCellCode({
204
- cellId: firstCellId,
205
- code: "import numpy as np",
206
- formattingChange: false,
207
- });
235
+ editCode(firstCellId, "import numpy as np");
208
236
  expect(formatCells(state)).toMatchInlineSnapshot(`
209
237
  "
210
238
  [0] 'import numpy as np'
@@ -243,11 +271,7 @@ describe("cell reducer", () => {
243
271
  stale_inputs: null,
244
272
  timestamp: 1,
245
273
  });
246
- actions.updateCellCode({
247
- cellId: firstCellId,
248
- code: "print('new code')",
249
- formattingChange: false,
250
- });
274
+ editCode(firstCellId, "print('new code')");
251
275
  actions.prepareForRun({ cellId: firstCellId });
252
276
  actions.handleCellMessage({
253
277
  cell_id: firstCellId,
@@ -275,11 +299,7 @@ describe("cell reducer", () => {
275
299
  expect(cell).toMatchSnapshot("initial state");
276
300
 
277
301
  // Update code
278
- actions.updateCellCode({
279
- cellId: firstCellId,
280
- code: "import marimo as mo",
281
- formattingChange: false,
282
- });
302
+ editCode(firstCellId, "import marimo as mo");
283
303
  cell = cells[0];
284
304
  expect(cell.status).toBe("idle");
285
305
  expect(cell.lastCodeRun).toBe(null);
@@ -384,32 +404,20 @@ describe("cell reducer", () => {
384
404
  // EDITING BACK AND FORTH
385
405
  /////////////////
386
406
  // Update code again
387
- actions.updateCellCode({
388
- cellId: firstCellId,
389
- code: "import marimo as mo\nimport numpy",
390
- formattingChange: false,
391
- });
407
+ editCode(firstCellId, "import marimo as mo\nimport numpy");
392
408
  cell = cells[0];
393
409
  expect(cell.status).toBe("idle");
394
410
  expect(cell.edited).toBe(true);
395
411
  expect(cell).toMatchSnapshot("code edited again");
396
412
 
397
413
  // Update code should be unedited
398
- actions.updateCellCode({
399
- cellId: firstCellId,
400
- code: "import marimo as mo",
401
- formattingChange: false,
402
- });
414
+ editCode(firstCellId, "import marimo as mo");
403
415
  cell = cells[0];
404
416
  expect(cell.edited).toBe(false);
405
417
  expect(cell).toMatchSnapshot("code reverted");
406
418
 
407
419
  // Update code should be edited again
408
- actions.updateCellCode({
409
- cellId: firstCellId,
410
- code: "import marimo as mo\nimport numpy",
411
- formattingChange: false,
412
- });
420
+ editCode(firstCellId, "import marimo as mo\nimport numpy");
413
421
  cell = cells[0];
414
422
  expect(cell.status).toBe("idle");
415
423
  expect(cell.lastCodeRun).toBe("import marimo as mo");
@@ -533,11 +541,7 @@ describe("cell reducer", () => {
533
541
  expect(cell).toMatchSnapshot("initial state");
534
542
 
535
543
  // Update code
536
- actions.updateCellCode({
537
- cellId: firstCellId,
538
- code: "import marimo as mo",
539
- formattingChange: false,
540
- });
544
+ editCode(firstCellId, "import marimo as mo");
541
545
  cell = cells[0];
542
546
  expect(cell.status).toBe("idle");
543
547
  expect(cell.lastCodeRun).toBe(null);
@@ -589,11 +593,7 @@ describe("cell reducer", () => {
589
593
 
590
594
  it("can run a stale cell", () => {
591
595
  // Update code of first cell
592
- actions.updateCellCode({
593
- cellId: firstCellId,
594
- code: "import marimo as mo",
595
- formattingChange: false,
596
- });
596
+ editCode(firstCellId, "import marimo as mo");
597
597
  // Add cell
598
598
  seedCell({
599
599
  cellId: firstCellId,
@@ -601,11 +601,7 @@ describe("cell reducer", () => {
601
601
  });
602
602
  const secondCell = cells[1].id;
603
603
  // Update code
604
- actions.updateCellCode({
605
- code: "mo.slider()",
606
- cellId: secondCell,
607
- formattingChange: false,
608
- });
604
+ editCode(secondCell, "mo.slider()");
609
605
 
610
606
  // Prepare for run
611
607
  actions.prepareForRun({
@@ -649,11 +645,7 @@ describe("cell reducer", () => {
649
645
 
650
646
  it("can format code and update cell", () => {
651
647
  const firstCellId = cells[0].id;
652
- actions.updateCellCode({
653
- cellId: firstCellId,
654
- code: "import marimo as mo",
655
- formattingChange: false,
656
- });
648
+ editCode(firstCellId, "import marimo as mo");
657
649
  let cell = cells[0];
658
650
  expect(cell.status).toBe("idle");
659
651
  expect(cell.lastCodeRun).toBe(null);
@@ -689,11 +681,7 @@ describe("cell reducer", () => {
689
681
  expect(cell.lastCodeRun).toBe("import marimo as mo");
690
682
 
691
683
  // Format code
692
- actions.updateCellCode({
693
- cellId: firstCellId,
694
- code: "import marimo as mo",
695
- formattingChange: true,
696
- });
684
+ formatCode(firstCellId, "import marimo as mo");
697
685
  cell = cells[0];
698
686
  expect(cell.status).toBe("idle");
699
687
  expect(cell.lastCodeRun).toBe("import marimo as mo");
@@ -702,11 +690,7 @@ describe("cell reducer", () => {
702
690
 
703
691
  it("can run a stopped cell", () => {
704
692
  // Update code of first cell
705
- actions.updateCellCode({
706
- cellId: firstCellId,
707
- code: "mo.md('This has an ancestor that was stopped')",
708
- formattingChange: false,
709
- });
693
+ editCode(firstCellId, "mo.md('This has an ancestor that was stopped')");
710
694
 
711
695
  // Prepare for run
712
696
  actions.prepareForRun({
@@ -1,32 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { python } from "@codemirror/lang-python";
4
- import { EditorState } from "@codemirror/state";
5
- import { EditorView } from "@codemirror/view";
6
3
  import { atom } from "jotai";
7
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
5
  import { cellId } from "@/__tests__/branded";
9
6
  import type {
10
- DocumentEdit,
11
- DocumentEditor,
12
7
  DocumentModel,
13
8
  DocumentSyncStatus,
14
9
  EditableCell,
15
10
  EditableDocument,
16
11
  } from "@/core/notebook/document";
17
- import {
18
- adaptiveLanguageConfiguration,
19
- languageAdapterState,
20
- } from "@/core/codemirror/language/extension";
21
- import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
22
12
  import { store } from "@/core/state/jotai";
23
13
  import { activeDocumentModelAtom } from "@/core/session/active-notebook";
24
- import {
25
- createNotebookActions,
26
- exportedForTesting,
27
- type NotebookState,
28
- notebookReducer,
29
- } from "../cells";
14
+ import { exportedForTesting, type NotebookState } from "../cells";
30
15
  import { applyDocument, flushDocument, followDocument } from "../document";
31
16
  import { type CellId, SCRATCH_CELL_ID } from "../ids";
32
17
 
@@ -58,29 +43,6 @@ function documentOf(cells: EditableCell[]): EditableDocument {
58
43
  };
59
44
  }
60
45
 
61
- /** A mounted cell editor, as the notebook's cells have. */
62
- function createEditor(content: string) {
63
- const state = EditorState.create({
64
- doc: content,
65
- extensions: [
66
- python(),
67
- adaptiveLanguageConfiguration({
68
- cellId: a,
69
- completionConfig: {
70
- activate_on_typing: true,
71
- signature_hint_on_typing: false,
72
- copilot: false,
73
- codeium_api_key: null,
74
- },
75
- hotkeys: new OverridingHotkeyProvider({}),
76
- placeholderType: "marimo-import",
77
- lspConfig: {},
78
- }),
79
- ],
80
- });
81
- return new EditorView({ state, parent: globalThis.document.body });
82
- }
83
-
84
46
  /** A notebook that follows `cells`, as the session builds one. */
85
47
  function notebookOf(cells: EditableCell[]): NotebookState {
86
48
  return applyDocument(initialNotebookState(), documentOf(cells));
@@ -110,38 +72,6 @@ describe("applyDocument", () => {
110
72
  expect(applyDocument(state, documentOf(cells))).toBe(state);
111
73
  });
112
74
 
113
- it("brings a code change into the cell's editor", () => {
114
- const state = notebookOf([cell(a), cell(b)]);
115
- const view = createEditor("b = 1");
116
- state.cellHandles[b].current = { editorView: view, editorViewOrNull: view };
117
- state.cellData[b] = { ...state.cellData[b], lastCodeRun: "b = 1" };
118
-
119
- const next = applyDocument(
120
- state,
121
- documentOf([cell(a), cell(b, { code: "b = 2" })]),
122
- );
123
-
124
- expect(next.cellData[b]).toMatchObject({ code: "b = 2", edited: true });
125
- expect(next.cellData[a]).toBe(state.cellData[a]);
126
- expect(view.state.doc.toString()).toBe("b = 2");
127
- });
128
-
129
- it("leaves an editor that already shows the document's code alone", () => {
130
- const state = notebookOf([cell(a)]);
131
- // The editor runs ahead of the notebook while this client types.
132
- const view = createEditor('mo.md("hi")');
133
- state.cellHandles[a].current = { editorView: view, editorViewOrNull: view };
134
-
135
- const next = applyDocument(
136
- state,
137
- documentOf([cell(a, { code: 'mo.md("hi")' })]),
138
- );
139
-
140
- expect(next.cellData[a].code).toBe('mo.md("hi")');
141
- expect(view.state.doc.toString()).toBe('mo.md("hi")');
142
- expect(view.state.field(languageAdapterState).type).toBe("python");
143
- });
144
-
145
75
  it("keeps the code this client has when the host withholds source", () => {
146
76
  const state = notebookOf([cell(a)]);
147
77
 
@@ -182,74 +112,6 @@ describe("applyDocument", () => {
182
112
  });
183
113
  });
184
114
 
185
- describe("documentEditMiddleware", () => {
186
- type Call = [string, ...unknown[]];
187
- let calls: Call[];
188
- let state: NotebookState;
189
- const editor: DocumentEditor = {
190
- setCode: (id, code) => calls.push(["setCode", id, code]),
191
- setName: (id, name) => calls.push(["setName", id, name]),
192
- updateConfig: (id, patch) => calls.push(["updateConfig", id, patch]),
193
- createCell: (cell, placement) =>
194
- calls.push(["createCell", cell, placement]),
195
- deleteCell: (id) => calls.push(["deleteCell", id]),
196
- setOrder: (order) => calls.push(["setOrder", order]),
197
- };
198
- const model: DocumentModel = {
199
- documentAtom: atom(null),
200
- editAtom: atom(null, (_get, _set, edit: DocumentEdit) => edit(editor)),
201
- syncStatusAtom: atom<DocumentSyncStatus>({ status: "idle" }),
202
- flush: vi.fn(),
203
- retry: vi.fn(),
204
- start: vi.fn(),
205
- };
206
- const actions = createNotebookActions((action) => {
207
- state = notebookReducer(state, action);
208
- });
209
-
210
- beforeEach(() => {
211
- calls = [];
212
- state = notebookOf([cell(a), cell(b), cell(c)]);
213
- store.set(activeDocumentModelAtom, model);
214
- });
215
-
216
- afterEach(() => {
217
- store.set(activeDocumentModelAtom, null);
218
- });
219
-
220
- it("writes code", () => {
221
- actions.updateCellCode({
222
- cellId: a,
223
- code: "a = 2",
224
- formattingChange: false,
225
- });
226
-
227
- expect(calls).toEqual([["setCode", a, "a = 2"]]);
228
- });
229
-
230
- it("ignores the document and the kernel's run state", () => {
231
- actions.prepareForRun({ cellId: a });
232
- actions.setLastRun({
233
- lastCodeRun: { [a]: "a = 0" },
234
- lastExecutionTime: {},
235
- });
236
- actions.resetCellRuntimes();
237
- actions.reconcileDocument(documentOf([cell(a)]));
238
-
239
- expect(calls).toEqual([]);
240
- });
241
-
242
- it("leaves the scratchpad alone", () => {
243
- actions.updateCellCode({
244
- cellId: SCRATCH_CELL_ID,
245
- code: "scratch",
246
- formattingChange: false,
247
- });
248
-
249
- expect(calls).toEqual([]);
250
- });
251
- });
252
-
253
115
  describe("followDocument", () => {
254
116
  it("reconciles the document now and on each change, until aborted", () => {
255
117
  const documentAtom = atom<EditableDocument | null>(null);
@@ -42,16 +42,18 @@ function setup() {
42
42
 
43
43
  describe("notebookOutline", () => {
44
44
  it("does not notify outline subscribers when cell code changes", () => {
45
- const { store, actions } = setup();
45
+ const { store } = setup();
46
46
  const initial = store.get(notebookOutline);
47
47
  const listener = vi.fn();
48
48
  const unsubscribe = store.sub(notebookOutline, listener);
49
49
 
50
- actions.updateCellCode({
51
- cellId: first,
52
- code: "x = 2",
53
- formattingChange: false,
54
- });
50
+ store.set(notebookAtom, (state) => ({
51
+ ...state,
52
+ cellData: {
53
+ ...state.cellData,
54
+ [first]: { ...state.cellData[first], code: "x = 2" },
55
+ },
56
+ }));
55
57
 
56
58
  expect(store.get(notebookOutline)).toBe(initial);
57
59
  expect(listener).not.toHaveBeenCalled();
@@ -159,6 +159,20 @@ export const createCellAtom = atom(
159
159
  },
160
160
  );
161
161
 
162
+ /**
163
+ * Write a cell's code, or nothing for a cell the document no longer has.
164
+ * A mounted editor follows the document.
165
+ */
166
+ export const setCellCodeAtom = atom(
167
+ null,
168
+ (get, set, { cellId, code }: { cellId: CellId; code: string }): void => {
169
+ if (!documentOf(get).cells.has(cellId)) {
170
+ return;
171
+ }
172
+ set(modelOf(get).editAtom, (tx) => tx.setCode(cellId, code));
173
+ },
174
+ );
175
+
162
176
  /**
163
177
  * Split a cell at its cursor. The text before stays, the text after
164
178
  * becomes a new cell below, which is focused. Returns the new cell's id,
@@ -398,6 +412,27 @@ export const addSetupCellAtom = atom(
398
412
  },
399
413
  );
400
414
 
415
+ /**
416
+ * Create the setup cell with `code`, or append `code` to the one that exists.
417
+ */
418
+ export const addOrAppendSetupCellAtom = atom(
419
+ null,
420
+ (get, set, { code }: { code: string }): void => {
421
+ const setup = documentOf(get).cells.get(SETUP_CELL_ID);
422
+ if (setup === undefined) {
423
+ set(addSetupCellAtom, { code });
424
+ return;
425
+ }
426
+ // Withheld source keeps the code this client has.
427
+ const existing =
428
+ setup.code ?? get(notebookAtom).cellData[SETUP_CELL_ID]?.code ?? "";
429
+ set(setCellCodeAtom, {
430
+ cellId: SETUP_CELL_ID,
431
+ code: existing.trim() ? `${existing.trimEnd()}\n\n${code}` : code,
432
+ });
433
+ },
434
+ );
435
+
401
436
  /**
402
437
  * Delete cells, keeping them in the history for undo. The notebook keeps at
403
438
  * least one cell. Returns false when nothing was deleted.
@@ -574,6 +609,7 @@ export function useCellActions() {
574
609
  const createCell = useSetAtom(createCellAtom);
575
610
  const moveToNextCell = useSetAtom(moveToNextCellAtom);
576
611
  const addSetupCell = useSetAtom(addSetupCellAtom);
612
+ const addOrAppendSetupCell = useSetAtom(addOrAppendSetupCellAtom);
577
613
  const deleteCells = useSetAtom(deleteCellsAtom);
578
614
  const undoDeleteCell = useSetAtom(undoDeleteCellAtom);
579
615
  const focusCell = useSetAtom(focusCellAtom);
@@ -597,11 +633,13 @@ export function useCellActions() {
597
633
  const mergeAllColumns = useSetAtom(mergeAllColumnsAtom);
598
634
  const compactColumns = useSetAtom(compactColumnsAtom);
599
635
  const moveColumn = useSetAtom(moveColumnAtom);
636
+ const setCellCode = useSetAtom(setCellCodeAtom);
600
637
  const splitCell = useSetAtom(splitCellAtom);
601
638
  const undoSplitCell = useSetAtom(undoSplitCellAtom);
602
639
  return useMemo(
603
640
  () => ({
604
641
  ...operations,
642
+ setCellCode,
605
643
  splitCell,
606
644
  undoSplitCell,
607
645
  addColumn,
@@ -620,6 +658,7 @@ export function useCellActions() {
620
658
  createCell,
621
659
  moveToNextCell,
622
660
  addSetupCell,
661
+ addOrAppendSetupCell,
623
662
  deleteCells,
624
663
  undoDeleteCell,
625
664
  focusCell,
@@ -22,7 +22,7 @@ import type { CellConfig } from "../network/types";
22
22
  import { createDeepEqualAtom, store } from "../state/jotai";
23
23
  import { isWasm } from "../wasm/utils";
24
24
  import { prepareCellForExecution, transitionCell } from "./cell";
25
- import { applyDocument, documentEditMiddleware } from "./document";
25
+ import { applyDocument } from "./document";
26
26
  import { isCodeHidden, shownHiddenCodeAtom } from "./editor";
27
27
  import { type CellId, SCRATCH_CELL_ID } from "./ids";
28
28
  import { type CellLog, getCellLogsForMessage } from "./logs";
@@ -154,50 +154,35 @@ export function initialNotebookState(): NotebookState {
154
154
  */
155
155
  const {
156
156
  reducer,
157
- addMiddleware,
158
157
  createActions,
159
158
  useActions,
160
159
  valueAtom: notebookAtom,
161
160
  } = createReducerAndAtoms(initialNotebookState, {
162
161
  /**
163
- * Put moved cells back where they were.
164
- *
165
- * TODO(MO-8028): Move to `undoDeleteCellAtom` with the order family.
162
+ * Keep a cell as run, or as edited, as it was before formatting rewrote
163
+ * its code from `before` to `after`. The code itself arrives through the
164
+ * document.
166
165
  */
167
- updateCellCode: (
166
+ codeFormatted: (
168
167
  state,
169
- action: {
170
- cellId: CellId;
171
- code: string;
172
- /**
173
- * Whether or not the update is a formatting change,
174
- * if so, the 'edited' state will be handled differently.
175
- */
176
- formattingChange: boolean;
177
- },
168
+ action: { cellId: CellId; before: string; after: string },
178
169
  ) => {
179
- const { cellId, code, formattingChange } = action;
170
+ const { cellId, before, after } = action;
180
171
  if (!state.cellData[cellId]) {
181
172
  return state;
182
173
  }
183
-
184
174
  return updateCellData({
185
175
  state,
186
176
  cellId,
187
177
  cellReducer: (cell) => {
188
- // Formatting-only change means we can re-use the last code run
189
- // if it was not previously edited. And we don't change the edited state.
190
- return formattingChange
191
- ? {
192
- ...cell,
193
- code: code,
194
- lastCodeRun: cell.edited ? cell.lastCodeRun : code,
195
- }
196
- : {
197
- ...cell,
198
- code: code,
199
- edited: code.trim() !== cell.lastCodeRun,
200
- };
178
+ const lastCodeRun = isEdited(cell.lastCodeRun, before)
179
+ ? cell.lastCodeRun
180
+ : after;
181
+ return {
182
+ ...cell,
183
+ lastCodeRun,
184
+ edited: isEdited(lastCodeRun, cell.code),
185
+ };
201
186
  },
202
187
  });
203
188
  },
@@ -426,11 +411,6 @@ const {
426
411
  },
427
412
  });
428
413
 
429
- // We apply the middleware here (rather than inline in createReducerAndAtoms)
430
- // so that the document edit middleware can import CellActions and
431
- // strictly type the dispatched actions without creating a circular dependency.
432
- addMiddleware(documentEditMiddleware);
433
-
434
414
  // Helper function to update a cell in the array
435
415
  function updateCellRuntimeState({
436
416
  state,