@marimo-team/islands 0.25.3-dev196 → 0.25.3-dev198

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 (135) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BRuwLOIx.js → ConnectedDataExplorerComponent-BsWiIPFk.js} +4 -4
  2. package/dist/{ErrorBoundary-C3Xv3yzV.js → ErrorBoundary-D1Z3KAf_.js} +2 -2
  3. package/dist/{abnfDiagram-VCTEODGH-LlJTOOU_.js → abnfDiagram-VCTEODGH-I2x4Yu25.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-9PQtGLLZ.js → architectureDiagram-5GKGNRK7-1bSQMKzK.js} +1 -1
  5. package/dist/{arrays-DA6KkLfq.js → arrays-DoQ-AILx.js} +7 -21
  6. package/dist/{capabilities-CvWU-ey_.js → capabilities-C2QUNRXi.js} +1 -1
  7. package/dist/{chat-ui-Bl2euMBD.js → chat-ui-BAyHM_GD.js} +524 -524
  8. package/dist/{chunk-IMKFNOWR-DcXv-BD5.js → chunk-IMKFNOWR-fsCGAJ9G.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-VwnELN23.js → chunk-SHT3W25Y-D2STW4hj.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-DgQpI5Zi.js → chunk-SVP7TREG-CiY7cQG-.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-CTg6VTJT.js → chunk-TICWLB2K-C_t5RBp9.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-UzmcFrnW.js → classDiagram-ZZMXUADV-B2JvZAP4.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-DhjiI6IQ.js → classDiagram-v2-VYDZK3BY-L8VsZG5A.js} +2 -2
  14. package/dist/{code-visibility-BCcqmP58.js → code-visibility-CInEQd1e.js} +19 -19
  15. package/dist/{common-DrNB2bY2.js → common-W5HfVJPN.js} +4362 -4362
  16. package/dist/{cynefinDiagram-5FMLGOSQ-O4ShIbJX.js → cynefinDiagram-5FMLGOSQ-DdllC264.js} +1 -1
  17. package/dist/{dagre-GXQ25YYZ-dtHhhcPu.js → dagre-GXQ25YYZ-BaWHw4_f.js} +1 -1
  18. package/dist/{data-grid-overlay-editor-TGXV_Hf4.js → data-grid-overlay-editor-BQCaV0fI.js} +1 -1
  19. package/dist/{diagram-S7CK7UJ4-ppPYVZ6W.js → diagram-S7CK7UJ4-pFt-tUDe.js} +1 -1
  20. package/dist/{diagram-UQ7AKVKN-CMZDf--k.js → diagram-UQ7AKVKN-DqGJiBp4.js} +1 -1
  21. package/dist/{diagram-VX7I27RA-CFvJdx-C.js → diagram-VX7I27RA-Dr-yzMjf.js} +1 -1
  22. package/dist/{diagram-Z3DM3KII-Dt6_uFbv.js → diagram-Z3DM3KII-8xzJzv-w.js} +1 -1
  23. package/dist/{dropdown-menu-CeaOsjWZ.js → dropdown-menu-NlnsmNA6.js} +2 -2
  24. package/dist/{ebnfDiagram-PWID7BFC-DpRFrKK-.js → ebnfDiagram-PWID7BFC-Br04Wy-E.js} +2 -2
  25. package/dist/{erDiagram-RLTQ6QDP-CqAU4b0u.js → erDiagram-RLTQ6QDP-D5-XVgvv.js} +1 -1
  26. package/dist/{error-entries-CYGwr5-O.js → error-entries-BacCWMWj.js} +4678 -5028
  27. package/dist/{feedback-button-PTiPn2GP.js → feedback-button-DLXUcQ7l.js} +113 -113
  28. package/dist/{flowDiagram-HODETNUW-i8-Gw8HO.js → flowDiagram-HODETNUW-CVl2kREF.js} +2 -2
  29. package/dist/{formats-BtoHHy42.js → formats-BCYthwyc.js} +1 -1
  30. package/dist/{glide-data-editor-BSyexjkB.js → glide-data-editor-D5YaHPKx.js} +5 -5
  31. package/dist/{globals-B7gkJAxm.js → globals-DjX1uxzy.js} +2 -2
  32. package/dist/{html-to-image-Cy5UDpQJ.js → html-to-image-DAmgSo5P.js} +624 -573
  33. package/dist/{infoDiagram-27XIBGKW-DpfOKdLL.js → infoDiagram-27XIBGKW-BJ4vNcZE.js} +1 -1
  34. package/dist/{ishikawaDiagram-5VMMS53U-Dg7mrMG7.js → ishikawaDiagram-5VMMS53U-B_Jy1Dsm.js} +1 -1
  35. package/dist/{kanban-definition-UXKFOSKX-QB6e3Jrj.js → kanban-definition-UXKFOSKX-C6MkS9vw.js} +1 -1
  36. package/dist/main.js +1117 -1117
  37. package/dist/{mermaid-CeWG4Egr.js → mermaid-Bh918qrq.js} +34 -34
  38. package/dist/{mindmap-definition-YA3MSWOX-BTz3uTGe.js → mindmap-definition-YA3MSWOX-BP3ZTuir.js} +1 -1
  39. package/dist/{number-overlay-editor-Bb91yQ46.js → number-overlay-editor-3C_K6w_0.js} +1 -1
  40. package/dist/{once-DWGO2mQ2.js → once-NnpX_HnX.js} +1 -1
  41. package/dist/{pegDiagram-XKGWAZYB-3ZIEa3mB.js → pegDiagram-XKGWAZYB-nNGOQtnt.js} +2 -2
  42. package/dist/{pieDiagram-E7YTZNPT-BJAlk046.js → pieDiagram-E7YTZNPT-CpRz5BZz.js} +1 -1
  43. package/dist/{process-output-q9MTAIMX.js → process-output-DweUmAmV.js} +3 -3
  44. package/dist/{railroadDiagram-O6MQD6OU-Dn3bsSRa.js → railroadDiagram-O6MQD6OU-BqOEN4I0.js} +2 -2
  45. package/dist/{requirementDiagram-BXWQKSXE-wcOmQQCL.js → requirementDiagram-BXWQKSXE-CEfg6vu9.js} +1 -1
  46. package/dist/{reveal-component-vTdjiyDv.js → reveal-component-JjJRTT9d.js} +17 -17
  47. package/dist/{sequenceDiagram-WJ2MYXX4-DSh_qxoF.js → sequenceDiagram-WJ2MYXX4-IOmpQcOi.js} +1 -1
  48. package/dist/{spec-C7Fos-If.js → spec-Dkp64vVQ.js} +1 -1
  49. package/dist/{stateDiagram-D77RDMKH-DH_H8NMH.js → stateDiagram-D77RDMKH-tgU-EY9_.js} +2 -2
  50. package/dist/{stateDiagram-v2-MP3YSRHH-D_JM7MEK.js → stateDiagram-v2-MP3YSRHH-BofhG6q1.js} +2 -2
  51. package/dist/{swimlanes-42K2YHIH-CfoTJO-9.js → swimlanes-42K2YHIH-DW4vWDBd.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js → swimlanesDiagram-VR7AAH4N-BJcrqUJr.js} +2 -2
  53. package/dist/{swiper-component-D2geL1Ku.js → swiper-component-BVfnhbnt.js} +1 -1
  54. package/dist/{timeline-definition-24CTP7MA-BFtdScaL.js → timeline-definition-24CTP7MA-D_QU3NGs.js} +1 -1
  55. package/dist/{toDate-CQAFSwos.js → toDate-BNDEXbfV.js} +30 -30
  56. package/dist/{useAsyncData-BeyZyjso.js → useAsyncData-ASDni9vH.js} +1 -1
  57. package/dist/{useDeepCompareMemoize-DOQgae0W.js → useDeepCompareMemoize-B1Ogyf53.js} +1 -1
  58. package/dist/{useIframeCapabilities-DNvaj9wM.js → useIframeCapabilities-6a_yDNXp.js} +1 -1
  59. package/dist/{useLifecycle-n2lnCFS8.js → useLifecycle-Bfpa2m7k.js} +1 -1
  60. package/dist/{useTheme-CY777LXN.js → useTheme-5TwyOh3B.js} +66 -66
  61. package/dist/{vega-component-0nQzA-I8.js → vega-component-BHS9UOtC.js} +19 -19
  62. package/dist/{vennDiagram-4TSXK5OY-BXY8IGCA.js → vennDiagram-4TSXK5OY-YFYsNIAh.js} +1 -1
  63. package/dist/{wardleyDiagram-VM6X3IG4-D0tFe6eC.js → wardleyDiagram-VM6X3IG4-i_n8mAWC.js} +1 -1
  64. package/dist/{xychartDiagram-S5SC5T6Z-Dx8wFM-y.js → xychartDiagram-S5SC5T6Z-BYGN95wW.js} +1 -1
  65. package/package.json +1 -1
  66. package/src/__mocks__/document.tsx +13 -4
  67. package/src/__mocks__/notebook.ts +1 -2
  68. package/src/components/datasources/datasources.tsx +1 -1
  69. package/src/components/dependency-graph/custom-node.tsx +1 -1
  70. package/src/components/dependency-graph/minimap-content.tsx +12 -14
  71. package/src/components/editor/actions/useSetCodeVisibility.ts +1 -1
  72. package/src/components/editor/actions/useSetOutputsExpanded.ts +1 -1
  73. package/src/components/editor/alerts/stdin-blocking-alert.tsx +1 -1
  74. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  75. package/src/components/editor/cell/useSplitCell.tsx +8 -5
  76. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +1 -1
  77. package/src/components/editor/chrome/panels/session-panel.tsx +1 -1
  78. package/src/components/editor/chrome/panels/variable-panel.tsx +1 -5
  79. package/src/components/editor/chrome/wrapper/minimap-state.ts +1 -1
  80. package/src/components/editor/columns/__tests__/cell-column.test.tsx +1 -1
  81. package/src/components/editor/columns/cell-column.tsx +1 -1
  82. package/src/components/editor/columns/sortable-column.tsx +1 -1
  83. package/src/components/editor/links/__tests__/cell-link.test.tsx +1 -1
  84. package/src/components/editor/links/cell-link-list.tsx +1 -1
  85. package/src/components/editor/links/cell-link.tsx +1 -1
  86. package/src/components/editor/navigation/__tests__/navigation.test.ts +20 -6
  87. package/src/components/editor/navigation/__tests__/selection.test.ts +1 -2
  88. package/src/components/editor/navigation/clipboard.ts +5 -3
  89. package/src/components/editor/renderers/cell-array.tsx +7 -12
  90. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  91. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +4 -4
  92. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +1 -1
  93. package/src/components/editor/stdin-blocking-alert.tsx +1 -1
  94. package/src/components/slides/__tests__/minimap-actions.test.tsx +13 -4
  95. package/src/components/slides/__tests__/minimap.test.ts +38 -19
  96. package/src/components/slides/minimap.tsx +11 -10
  97. package/src/components/sort/SortableCellsProvider.tsx +2 -21
  98. package/src/components/tracing/tracing.tsx +1 -1
  99. package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
  100. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +8 -12
  101. package/src/core/ai/context/providers/cell-output.ts +2 -2
  102. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +34 -66
  103. package/src/core/ai/tools/edit-notebook-tool.ts +12 -10
  104. package/src/core/cells/__tests__/actions.test.ts +64 -25
  105. package/src/core/cells/__tests__/arrange.test.ts +156 -4
  106. package/src/core/cells/__tests__/cells.test.ts +12 -290
  107. package/src/core/cells/__tests__/document.test.ts +2 -46
  108. package/src/core/cells/__tests__/harness.ts +28 -4
  109. package/src/core/cells/__tests__/layout.test.ts +42 -54
  110. package/src/core/cells/__tests__/session.test.ts +0 -2
  111. package/src/core/cells/__tests__/utils.test.ts +9 -107
  112. package/src/core/cells/actions.ts +112 -26
  113. package/src/core/cells/add-missing-import.ts +1 -1
  114. package/src/core/cells/arrange.ts +206 -13
  115. package/src/core/cells/cells.ts +28 -256
  116. package/src/core/cells/document.ts +18 -122
  117. package/src/core/cells/layout.ts +79 -10
  118. package/src/core/cells/pending-delete-service.ts +1 -1
  119. package/src/core/cells/utils.ts +4 -4
  120. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +6 -7
  121. package/src/core/codemirror/copilot/getCodes.ts +2 -2
  122. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +1 -2
  123. package/src/core/codemirror/lsp/utils.ts +1 -1
  124. package/src/core/documentation/doc-lookup.ts +1 -1
  125. package/src/core/edit-app.tsx +2 -5
  126. package/src/core/errors/error-entries.ts +1 -1
  127. package/src/core/islands/__tests__/bootstrap.test.ts +2 -6
  128. package/src/core/islands/components/web-components.tsx +1 -1
  129. package/src/core/notebook/document/__tests__/codemirror.test.ts +8 -0
  130. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +1 -1
  131. package/src/stories/cell.stories.tsx +1 -2
  132. package/src/stories/layout/vertical/one-column.stories.tsx +1 -2
  133. package/src/stories/runtime-story.tsx +2 -3
  134. package/src/utils/__tests__/id-tree.test.ts +0 -617
  135. package/src/utils/id-tree.tsx +0 -609
@@ -1,12 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { createStore } from "jotai";
4
- import { describe, expect, it } from "vitest";
3
+ import { afterEach, describe, expect, it } from "vitest";
5
4
  import { cellId } from "@/__tests__/branded";
6
- import type { EditableCell } from "@/core/notebook/document";
7
- import type { CellColumnId } from "@/utils/id-tree";
8
- import { exportedForTesting, notebookAtom, notebookReducer } from "../cells";
9
- import { applyDocument } from "../document";
5
+ import type { CellColumnId } from "@/core/cells/layout";
10
6
  import type { CellId } from "../ids";
11
7
  import {
12
8
  collapsedAtom,
@@ -15,10 +11,13 @@ import {
15
11
  foldSections,
16
12
  foldsAfter,
17
13
  foldsHiding,
14
+ isColumnId,
18
15
  layoutAtom,
16
+ newColumnId,
19
17
  visibleNeighbor,
20
18
  } from "../layout";
21
19
  import type { Outline } from "../outline";
20
+ import { setupNotebook } from "./harness";
22
21
 
23
22
  const [h1, a, h2, b, c, h1b, d] = ["h1", "a", "h2", "b", "c", "h1b", "d"].map(
24
23
  cellId,
@@ -71,6 +70,13 @@ describe("foldSections", () => {
71
70
  });
72
71
  });
73
72
 
73
+ describe("column ids", () => {
74
+ it("are told apart from cell ids, as drag and drop needs", () => {
75
+ expect(isColumnId(newColumnId())).toBe(true);
76
+ expect(isColumnId(cellId("Hbol"))).toBe(false);
77
+ });
78
+ });
79
+
74
80
  describe("deriveLayout", () => {
75
81
  const layout = deriveLayout(
76
82
  [column("left", ids), column("right", [])],
@@ -116,74 +122,56 @@ describe("deriveLayout", () => {
116
122
  });
117
123
 
118
124
  describe("layoutAtom", () => {
119
- /** A store whose notebook holds `ids`, in that order, with nothing run. */
120
- function notebookOf(...ids: CellId[]) {
121
- const store = createStore();
122
- const cells = ids.map((id): EditableCell => ({
123
- id,
124
- code: "",
125
- name: "_",
126
- config: {
127
- column: null,
128
- disabled: false,
129
- hide_code: false,
130
- expand_output: false,
131
- },
132
- }));
133
- store.set(
134
- notebookAtom,
135
- applyDocument(exportedForTesting.initialNotebookState(), {
136
- order: ids,
137
- cells: new Map(cells.map((cell) => [cell.id, cell])),
138
- }),
139
- );
140
- return store;
141
- }
125
+ const stops: Array<() => void> = [];
126
+ afterEach(() => {
127
+ for (const stop of stops.splice(0)) {
128
+ stop();
129
+ }
130
+ });
131
+ const [c1, c2] = ["c1", "c2"].map(cellId);
142
132
 
143
133
  /** Deliver `html` as a cell's output, as the kernel's message does. */
144
134
  function output(
145
- store: ReturnType<typeof createStore>,
135
+ dispatch: ReturnType<typeof setupNotebook>["dispatch"],
146
136
  id: CellId,
147
137
  html: string,
148
138
  ) {
149
- store.set(notebookAtom, (state) =>
150
- notebookReducer(state, {
151
- type: "handleCellMessage",
152
- payload: {
153
- cell_id: id,
154
- output: {
155
- channel: "output",
156
- mimetype: "text/html",
157
- data: html,
158
- timestamp: 1,
159
- },
160
- console: null,
161
- status: "idle",
162
- stale_inputs: null,
139
+ dispatch({
140
+ type: "handleCellMessage",
141
+ payload: {
142
+ cell_id: id,
143
+ output: {
144
+ channel: "output",
145
+ mimetype: "text/html",
146
+ data: html,
163
147
  timestamp: 1,
164
148
  },
165
- }),
166
- );
149
+ console: null,
150
+ status: "idle",
151
+ stale_inputs: null,
152
+ timestamp: 1,
153
+ },
154
+ });
167
155
  }
168
156
 
169
157
  it("is the same layout after output that changes no heading", () => {
170
- const store = notebookOf(h1, a);
171
- output(store, h1, "<h1>Title</h1>");
158
+ const { store, dispatch } = setupNotebook(stops);
159
+ output(dispatch, c1, "<h1>Title</h1>");
172
160
  const layout = store.get(layoutAtom);
173
161
 
174
- output(store, a, "<p>1</p>");
175
- output(store, h1, "<h1>Title</h1>");
162
+ output(dispatch, c2, "<p>1</p>");
163
+ output(dispatch, c1, "<h1>Title</h1>");
176
164
 
177
165
  expect(store.get(layoutAtom)).toBe(layout);
178
166
  });
179
167
 
180
168
  it("follows a heading arriving in a cell's output", () => {
181
- const store = notebookOf(h1, a);
182
- store.set(collapsedAtom, new Set([h1]));
169
+ const { store, dispatch } = setupNotebook(stops);
170
+ store.set(collapsedAtom, new Set([c1]));
183
171
  expect(store.get(layoutAtom).columns[0].folds.size).toBe(0);
184
172
 
185
- output(store, h1, "<h1>Title</h1>");
173
+ output(dispatch, c1, "<h1>Title</h1>");
186
174
 
187
- expect(store.get(layoutAtom).columns[0].folds.get(h1)).toEqual([a]);
175
+ expect(store.get(layoutAtom).columns[0].folds.get(c1)).toEqual([c2]);
188
176
  });
189
177
  });
@@ -6,7 +6,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { Mocks } from "@/__mocks__/common";
7
7
  import { cellId } from "@/__tests__/branded";
8
8
  import { parseOutline } from "@/core/dom/outline";
9
- import { visibleForTesting } from "@/utils/id-tree";
10
9
  import { invariant } from "@/utils/invariant";
11
10
  import { Logger } from "@/utils/Logger";
12
11
  import { SETUP_CELL_ID } from "../ids";
@@ -35,7 +34,6 @@ function cellIn(result: ReturnType<typeof readExport>, id: string) {
35
34
  describe("readExport", () => {
36
35
  beforeEach(() => {
37
36
  vi.clearAllMocks();
38
- visibleForTesting.reset();
39
37
  });
40
38
 
41
39
  // Test data factories
@@ -1,15 +1,10 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
  import { describe, expect, it } from "vitest";
4
- import {
5
- getCellConfigs,
6
- initialNotebookState,
7
- type NotebookState,
8
- } from "@/core/cells/cells";
4
+ import { initialNotebookState, type NotebookState } from "@/core/cells/cells";
9
5
  import { CellId } from "@/core/cells/ids";
10
- import type { CellData, CellRuntimeState } from "@/core/cells/types";
6
+ import type { CellData } from "@/core/cells/types";
11
7
  import type { OutputMessage } from "@/core/kernel/messages";
12
- import { MultiColumn } from "@/utils/id-tree";
13
8
  import {
14
9
  disabledCellIds,
15
10
  enabledCellIds,
@@ -18,103 +13,10 @@ import {
18
13
  staleCellIds,
19
14
  } from "../utils";
20
15
 
21
- describe("getCellConfigs", () => {
22
- it("should return correct cell configs with column indices", () => {
23
- // Create mock cell IDs
24
- const cellId1 = CellId.create();
25
- const cellId2 = CellId.create();
26
- const cellId3 = CellId.create();
27
- const cellId4 = CellId.create();
28
-
29
- // Create a mock NotebookState
30
- const mockState: NotebookState = {
31
- ...initialNotebookState(),
32
- cellIds: MultiColumn.from([
33
- [cellId1, cellId2],
34
- [cellId3, cellId4],
35
- ]),
36
- cellData: {
37
- [cellId1]: {
38
- id: cellId1,
39
- config: { hide_code: false, disabled: false },
40
- } as CellData,
41
- [cellId2]: {
42
- id: cellId2,
43
- config: { hide_code: true, disabled: false },
44
- } as CellData,
45
- [cellId3]: {
46
- id: cellId3,
47
- config: { hide_code: false, disabled: true },
48
- } as CellData,
49
- [cellId4]: {
50
- id: cellId4,
51
- config: { hide_code: true, disabled: true },
52
- } as CellData,
53
- },
54
- cellRuntime: {} as Record<CellId, CellRuntimeState>,
55
- };
56
-
57
- // Call the function
58
- const result = getCellConfigs(mockState);
59
-
60
- // Assert the results
61
- expect(result).toEqual([
62
- { hide_code: false, disabled: false, column: 0 },
63
- { hide_code: true, disabled: false, column: null },
64
- { hide_code: false, disabled: true, column: 1 },
65
- { hide_code: true, disabled: true, column: null },
66
- ]);
67
-
68
- // Check that the original state was not modified
69
- expect(mockState.cellData[cellId1].config).toEqual({
70
- hide_code: false,
71
- disabled: false,
72
- });
73
- expect(mockState.cellData[cellId2].config).toEqual({
74
- hide_code: true,
75
- disabled: false,
76
- });
77
- expect(mockState.cellData[cellId3].config).toEqual({
78
- hide_code: false,
79
- disabled: true,
80
- });
81
- expect(mockState.cellData[cellId4].config).toEqual({
82
- hide_code: true,
83
- disabled: true,
84
- });
85
- });
86
-
87
- it("should handle single column", () => {
88
- const cellId1 = CellId.create();
89
- const mockState: NotebookState = {
90
- ...initialNotebookState(),
91
- cellIds: MultiColumn.from([[cellId1]]),
92
- cellData: {
93
- [cellId1]: {
94
- id: cellId1,
95
- config: { hide_code: false, disabled: false },
96
- } as CellData,
97
- },
98
- };
99
-
100
- const result = getCellConfigs(mockState);
101
- expect(result).toEqual([
102
- { hide_code: false, disabled: false, column: null },
103
- ]);
104
- });
105
-
106
- it("should handle empty notebook state", () => {
107
- const mockState: NotebookState = initialNotebookState();
108
- const result = getCellConfigs(mockState);
109
-
110
- expect(result).toEqual([]);
111
- });
112
- });
113
-
114
16
  describe("staleCellIds", () => {
115
17
  it("should return cell IDs that have not been instantiated when auto-instantiate is false", () => {
116
18
  const state: NotebookState = {
117
- cellIds: MultiColumn.from([["cell1", "cell2"]]),
19
+ cellIds: ["cell1", "cell2"],
118
20
  cellData: {
119
21
  cell1: {
120
22
  lastExecutionTime: null,
@@ -154,7 +56,7 @@ describe("staleCellIds", () => {
154
56
 
155
57
  it("should use lastExecutionTime when runElapsedTimeMs is null", () => {
156
58
  const state: NotebookState = {
157
- cellIds: MultiColumn.from([["cell1", "cell2"]]),
59
+ cellIds: ["cell1", "cell2"],
158
60
  cellData: {
159
61
  cell1: {
160
62
  lastExecutionTime: null,
@@ -194,7 +96,7 @@ describe("staleCellIds", () => {
194
96
 
195
97
  it("should return cell IDs that are edited", () => {
196
98
  const state: NotebookState = {
197
- cellIds: MultiColumn.from([["cell1", "cell2"]]),
99
+ cellIds: ["cell1", "cell2"],
198
100
  cellData: {
199
101
  cell1: {
200
102
  lastExecutionTime: 123,
@@ -234,7 +136,7 @@ describe("staleCellIds", () => {
234
136
 
235
137
  it("should return cell IDs that are interrupted", () => {
236
138
  const state: NotebookState = {
237
- cellIds: MultiColumn.from([["cell1", "cell2"]]),
139
+ cellIds: ["cell1", "cell2"],
238
140
  cellData: {
239
141
  cell1: {
240
142
  lastExecutionTime: 123,
@@ -274,7 +176,7 @@ describe("staleCellIds", () => {
274
176
 
275
177
  it("should not return cell IDs that are disabled", () => {
276
178
  const state: NotebookState = {
277
- cellIds: MultiColumn.from([["cell1", "cell2"]]),
179
+ cellIds: ["cell1", "cell2"],
278
180
  cellData: {
279
181
  cell1: {
280
182
  lastExecutionTime: 123,
@@ -394,7 +296,7 @@ describe("disabledCellIds", () => {
394
296
 
395
297
  const state: NotebookState = {
396
298
  ...initialNotebookState(),
397
- cellIds: MultiColumn.from([[cellId1, cellId2, cellId3]]),
299
+ cellIds: [cellId1, cellId2, cellId3],
398
300
  cellData: {
399
301
  [cellId1]: { id: cellId1, config: { disabled: true } } as CellData,
400
302
  [cellId2]: { id: cellId2, config: { disabled: false } } as CellData,
@@ -415,7 +317,7 @@ describe("enabledCellIds", () => {
415
317
 
416
318
  const state: NotebookState = {
417
319
  ...initialNotebookState(),
418
- cellIds: MultiColumn.from([[cellId1, cellId2, cellId3]]),
320
+ cellIds: [cellId1, cellId2, cellId3],
419
321
  cellData: {
420
322
  [cellId1]: { id: cellId1, config: { disabled: true } } as CellData,
421
323
  [cellId2]: { id: cellId2, config: { disabled: false } } as CellData,
@@ -3,21 +3,28 @@
3
3
  /** Notebook operations that edit the document and then adjust the editor. */
4
4
 
5
5
  import { historyField } from "@codemirror/commands";
6
- import { atom, type Getter, useSetAtom } from "jotai";
6
+ import { atom, type Getter, type Setter, useSetAtom } from "jotai";
7
7
  import { useMemo } from "react";
8
8
  import { scrollAndHighlightCell } from "@/components/editor/links/cell-link";
9
9
  import type { EditableDocument } from "@/core/notebook/document";
10
- import type { CellColumnId } from "@/utils/id-tree";
11
10
  import { Logger } from "@/utils/Logger";
12
11
  import { clamp } from "@/utils/math";
12
+ import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
13
+ import { splitEditor } from "../codemirror/language/utils";
13
14
  import type { SerializedEditorState } from "../codemirror/types";
14
15
  import { canCollapseOutline } from "../dom/outline";
15
16
  import type { CellConfig } from "../network/types";
16
17
  import { store } from "../state/jotai";
17
18
  import {
19
+ addColumnAtom,
20
+ addColumnBreakpointAtom,
21
+ compactColumnsAtom,
22
+ deleteColumnAtom,
18
23
  dropCellOverCellAtom,
19
24
  dropCellOverColumnAtom,
25
+ mergeAllColumnsAtom,
20
26
  moveCellAtom,
27
+ moveColumnAtom,
21
28
  moveCellsRelativeToAtom,
22
29
  moveCellToIndexAtom,
23
30
  placeCells,
@@ -27,6 +34,7 @@ import {
27
34
  import {
28
35
  type DeletedCell,
29
36
  notebookAtom,
37
+ notebookReducer,
30
38
  releaseCellAtoms,
31
39
  useReducerActions,
32
40
  } from "./cells";
@@ -39,8 +47,10 @@ import {
39
47
  } from "./editor";
40
48
  import { CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "./ids";
41
49
  import {
50
+ type CellColumnId,
42
51
  collapsedAtom,
43
52
  columnOf,
53
+ findColumn,
44
54
  foldOf,
45
55
  foldsAfter,
46
56
  foldsHiding,
@@ -91,7 +101,7 @@ export const createCellAtom = atom(
91
101
  if (options.skipIfCodeExists && hasCellWithCode(document, code)) {
92
102
  return null;
93
103
  }
94
- const { cellIds } = get(notebookAtom);
104
+ const layout = get(layoutAtom);
95
105
 
96
106
  // The document knows cells and order. Columns come from each cell's
97
107
  // `column` config, which the cell before it provides unless it starts a
@@ -100,31 +110,31 @@ export const createCellAtom = atom(
100
110
  let column: number | null = null;
101
111
  // The cell the new one comes right after, whose folds would hide it.
102
112
  let follows: CellId | undefined;
103
- const layout = get(layoutAtom);
104
113
  if (options.after) {
105
114
  // A folded header hides the cells after it, so a cell placed after
106
115
  // the header goes after its fold.
107
116
  follows = foldOf(layout, options.after)?.at(-1) ?? options.after;
108
117
  placement = { after: follows };
109
118
  } else if (options.before) {
110
- const tree = cellIds.findWithId(options.before);
111
- const index = cellIds.indexOf(tree);
119
+ const target = columnOf(layout, options.before);
120
+ const index = layout.columns.indexOf(target);
112
121
  placement = { before: options.before };
113
- follows = tree.before(options.before);
114
- if (index > 0 && tree.indexOfOrThrow(options.before) === 0) {
122
+ follows = target.ids[target.ids.indexOf(options.before) - 1];
123
+ if (index > 0 && target.ids[0] === options.before) {
115
124
  column = index;
116
125
  }
117
126
  } else {
118
- const tree =
119
- (options.endOf && cellIds.get(options.endOf)) ?? cellIds.atOrThrow(0);
120
- if (tree.length > 0) {
121
- follows = tree.last();
122
- placement = { after: follows };
123
- } else if (cellIds.colLength > 1) {
124
- column = cellIds.indexOf(tree);
127
+ const target =
128
+ (options.endOf && findColumn(layout, options.endOf)) ??
129
+ layout.columns[0];
130
+ const last = target.ids.at(-1);
131
+ if (last !== undefined) {
132
+ follows = last;
133
+ placement = { after: last };
134
+ } else if (layout.columns.length > 1) {
135
+ column = layout.columns.indexOf(target);
125
136
  }
126
137
  }
127
-
128
138
  const id = options.id ?? CellId.create();
129
139
  const config = { ...options.config, column };
130
140
  set(modelOf(get).editAtom, (tx) => {
@@ -157,6 +167,70 @@ export const createCellAtom = atom(
157
167
  },
158
168
  );
159
169
 
170
+ /**
171
+ * Split a cell at its cursor. The text before stays, the text after
172
+ * becomes a new cell below, which is focused. Returns the new cell's id,
173
+ * or null for a cell without an editor.
174
+ */
175
+ export const splitCellAtom = atom(
176
+ null,
177
+ (get, set, { cellId }: { cellId: CellId }): CellId | null => {
178
+ const view = get(notebookAtom).cellHandles[cellId]?.current?.editorView;
179
+ if (!view) {
180
+ return null;
181
+ }
182
+ const { beforeCursorCode, afterCursorCode } = splitEditor(view);
183
+ const update = prepareCellCodeUpdate(view.state, beforeCursorCode);
184
+ if (update) {
185
+ view.dispatch(update);
186
+ }
187
+ const id = CellId.create();
188
+ set(modelOf(get).editAtom, (tx) => {
189
+ tx.setCode(cellId, beforeCursorCode);
190
+ tx.createCell({ id, code: afterCursorCode }, { after: cellId });
191
+ });
192
+ clearOutput(set, cellId);
193
+ focusWhenMounted(set, id);
194
+ return id;
195
+ },
196
+ );
197
+
198
+ /** Undo a split: the cell takes its code back and the cell split off goes. */
199
+ export const undoSplitCellAtom = atom(
200
+ null,
201
+ (
202
+ get,
203
+ set,
204
+ {
205
+ cellId,
206
+ newCellId,
207
+ snapshot,
208
+ }: { cellId: CellId; newCellId: CellId; snapshot: string },
209
+ ): void => {
210
+ const view = get(notebookAtom).cellHandles[cellId]?.current?.editorView;
211
+ const update = view && prepareCellCodeUpdate(view.state, snapshot);
212
+ if (view && update) {
213
+ view.dispatch(update);
214
+ }
215
+ const document = documentOf(get);
216
+ set(modelOf(get).editAtom, (tx) => {
217
+ tx.setCode(cellId, snapshot);
218
+ if (document.cells.has(newCellId)) {
219
+ tx.deleteCell(newCellId);
220
+ }
221
+ });
222
+ releaseCellAtoms(newCellId);
223
+ clearOutput(set, cellId);
224
+ },
225
+ );
226
+
227
+ /** The output belonged to the code before the change; neither version has run. */
228
+ function clearOutput(set: Setter, cellId: CellId): void {
229
+ set(notebookAtom, (state) =>
230
+ notebookReducer(state, { type: "clearCellOutput", payload: { cellId } }),
231
+ );
232
+ }
233
+
160
234
  /**
161
235
  * Focus the cell before or after `cellId`. Past the edge of the column,
162
236
  * create one there, unless `create` is false.
@@ -283,9 +357,7 @@ export const collapseAllCellsAtom = atom(null, (get, set): void => {
283
357
  set(
284
358
  collapsedAtom,
285
359
  new Set(
286
- cellIds.inOrderIds.filter((id) =>
287
- canCollapseOutline(cellRuntime[id]?.outline),
288
- ),
360
+ cellIds.filter((id) => canCollapseOutline(cellRuntime[id]?.outline)),
289
361
  ),
290
362
  );
291
363
  });
@@ -318,18 +390,18 @@ export const addSetupCellAtom = atom(
318
390
  null,
319
391
  (get, set, { code }: { code?: string } = {}): void => {
320
392
  const { cellIds } = get(notebookAtom);
321
- if (cellIds.setupCellExists()) {
393
+ if (cellIds.includes(SETUP_CELL_ID)) {
322
394
  focusWhenMounted(set, SETUP_CELL_ID);
323
395
  return;
324
396
  }
325
397
  // The setup cell leads the first column, which may be empty.
326
- const first = cellIds.atOrThrow(0);
398
+ const first = get(layoutAtom).columns[0];
327
399
  set(createCellAtom, {
328
400
  id: SETUP_CELL_ID,
329
401
  name: SETUP_CELL_ID,
330
402
  code: code ?? "",
331
- ...(first.length > 0
332
- ? { before: first.atOrThrow(0) }
403
+ ...(first.ids.length > 0
404
+ ? { before: first.ids[0] }
333
405
  : { endOf: first.id }),
334
406
  });
335
407
  },
@@ -528,9 +600,25 @@ export function useCellActions() {
528
600
  const dropCellOverColumn = useSetAtom(dropCellOverColumnAtom);
529
601
  const sendToTop = useSetAtom(sendToTopAtom);
530
602
  const sendToBottom = useSetAtom(sendToBottomAtom);
603
+ const addColumn = useSetAtom(addColumnAtom);
604
+ const addColumnBreakpoint = useSetAtom(addColumnBreakpointAtom);
605
+ const deleteColumn = useSetAtom(deleteColumnAtom);
606
+ const mergeAllColumns = useSetAtom(mergeAllColumnsAtom);
607
+ const compactColumns = useSetAtom(compactColumnsAtom);
608
+ const moveColumn = useSetAtom(moveColumnAtom);
609
+ const splitCell = useSetAtom(splitCellAtom);
610
+ const undoSplitCell = useSetAtom(undoSplitCellAtom);
531
611
  return useMemo(
532
612
  () => ({
533
613
  ...operations,
614
+ splitCell,
615
+ undoSplitCell,
616
+ addColumn,
617
+ addColumnBreakpoint,
618
+ deleteColumn,
619
+ mergeAllColumns,
620
+ compactColumns,
621
+ moveColumn,
534
622
  moveCell,
535
623
  moveCellToIndex,
536
624
  moveCellsRelativeTo,
@@ -563,9 +651,7 @@ export type CellActions = ReturnType<typeof useCellActions>;
563
651
  /** Scroll to the first cell that is currently running. */
564
652
  export function notebookScrollToRunning() {
565
653
  const { cellIds, cellRuntime } = store.get(notebookAtom);
566
- const cellId = cellIds.inOrderIds.find(
567
- (id) => cellRuntime[id]?.status === "running",
568
- );
654
+ const cellId = cellIds.find((id) => cellRuntime[id]?.status === "running");
569
655
  if (!cellId) {
570
656
  return;
571
657
  }
@@ -36,7 +36,7 @@ export function maybeAddMissingImport({
36
36
  `import[ \t]+${moduleName}[ \t]+as[ \t]+${variableName}`,
37
37
  "g",
38
38
  );
39
- for (const cell of cellIds.inOrderIds) {
39
+ for (const cell of cellIds) {
40
40
  if (regex.test(cellData[cell].code)) {
41
41
  return false;
42
42
  }