@marimo-team/islands 0.25.3-dev197 → 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-jQVF8yMb.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-CjZo_SV8.js → code-visibility-CInEQd1e.js} +19 -19
  15. package/dist/{common-CFLvScF3.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-CzW3mY6m.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-GR_8U4Vo.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-Dfdxc22H.js → glide-data-editor-D5YaHPKx.js} +5 -5
  31. package/dist/{globals-CxjzBEbP.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-CwHYeI4e.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-C5bOqXlB.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
@@ -8,7 +8,6 @@
8
8
  * document model and `notebookAtom` holds only runtime and editor state.
9
9
  */
10
10
 
11
- import type * as Protocol from "@marimo-team/client/protocol";
12
11
  import { dequal as isEqual } from "dequal";
13
12
  import type { createStore } from "jotai";
14
13
  import { createRef, type RefObject } from "react";
@@ -21,15 +20,13 @@ import type {
21
20
  EditableDocument,
22
21
  } from "@/core/notebook/document";
23
22
  import type { DispatchedActionOf } from "@/utils/createReducer";
24
- import { CollapsibleTree, MultiColumn } from "@/utils/id-tree";
25
23
  import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
26
- import type { CellConfig } from "../network/types";
27
24
  import { canUseRtc, isRtcEnabled } from "../rtc/state";
28
25
  import { store } from "../state/jotai";
29
26
  import { activeDocumentModelAtom } from "../session/active-notebook";
30
27
  import type { NotebookState, ReducerCellActions } from "./cells";
31
- import { groupByColumn } from "./columns";
32
28
  import type { CellId } from "./ids";
29
+ import { followColumnsAtom } from "./layout";
33
30
  import {
34
31
  type CellData,
35
32
  createCell,
@@ -42,27 +39,6 @@ type CellAction = DispatchedActionOf<ReducerCellActions>;
42
39
 
43
40
  // Following the document ----------------------------------------------------
44
41
 
45
- /** Rebuild the tree for `columns`, keeping column ids. */
46
- function reconcileColumns(
47
- current: MultiColumn<CellId>,
48
- columns: CellId[][],
49
- ): MultiColumn<CellId> {
50
- const previous = current.getColumns();
51
- // The document cannot hold an empty column, so keep the ones this client has.
52
- while (previous[columns.length]?.length === 0) {
53
- columns.push([]);
54
- }
55
- if (
56
- columns.length === previous.length &&
57
- columns.every((ids, index) => isEqual(ids, previous[index].inOrderIds))
58
- ) {
59
- return current;
60
- }
61
- return new MultiColumn(
62
- columns.map((ids, index) => CollapsibleTree.from(ids, previous[index]?.id)),
63
- );
64
- }
65
-
66
42
  /** The cell's data with the document's fields, and its editor brought along. */
67
43
  function followCell(
68
44
  current: CellData,
@@ -115,9 +91,12 @@ export function applyDocument(
115
91
  state: NotebookState,
116
92
  document: EditableDocument,
117
93
  ): NotebookState {
118
- const cellIds = reconcileColumns(state.cellIds, groupByColumn(document));
94
+ // The protocol's ids are plain strings; the notebook brands them.
95
+ const cellIds = isEqual(document.order, state.cellIds)
96
+ ? state.cellIds
97
+ : ([...document.order] as CellId[]);
119
98
  let { cellData, cellRuntime, cellHandles } = state;
120
- for (const id of cellIds.inOrderIds) {
99
+ for (const id of cellIds) {
121
100
  const cell = document.cells.get(id);
122
101
  const current = state.cellData[id];
123
102
  if (cell === undefined) {
@@ -149,8 +128,8 @@ export function applyDocument(
149
128
  cellData = { ...cellData, [id]: next };
150
129
  }
151
130
  }
152
- const present = new Set(cellIds.inOrderIds);
153
- const removed = state.cellIds.inOrderIds.filter((id) => !present.has(id));
131
+ const present = new Set(cellIds);
132
+ const removed = state.cellIds.filter((id) => !present.has(id));
154
133
  if (removed.length > 0) {
155
134
  const gone = new Set(removed);
156
135
  const keep = <V>(record: Record<CellId, V>): Record<CellId, V> =>
@@ -172,7 +151,8 @@ export function applyDocument(
172
151
  }
173
152
 
174
153
  /**
175
- * Reconcile the model's document now and on each change, until the signal aborts.
154
+ * Reconcile the model's document and name its columns, now and on each
155
+ * change, until the signal aborts.
176
156
  *
177
157
  * TODO(MO-8028): Delete with `applyDocument`.
178
158
  */
@@ -191,6 +171,7 @@ export function followDocument({
191
171
  const document = store.get(model.documentAtom);
192
172
  if (document !== null) {
193
173
  reconcile(document);
174
+ store.set(followColumnsAtom, document);
194
175
  }
195
176
  };
196
177
  const unsubscribe = store.sub(model.documentAtom, follow);
@@ -200,107 +181,22 @@ export function followDocument({
200
181
 
201
182
  // Editing the document ------------------------------------------------------
202
183
 
203
- const CONFIG_FIELDS = ["disabled", "hide_code", "expand_output"] as const;
204
-
205
- /** The fields of `after` that differ from `before`, leaving `column` to the tree. */
206
- function configPatch(
207
- before: CellConfig,
208
- after: CellConfig,
209
- ): Partial<Protocol.CellConfig> | null {
210
- const patch: Partial<Protocol.CellConfig> = {};
211
- for (const field of CONFIG_FIELDS) {
212
- if (before[field] !== after[field]) {
213
- patch[field] = after[field] ?? false;
214
- }
215
- }
216
- return Object.keys(patch).length === 0 ? null : patch;
217
- }
218
-
219
184
  /**
220
- * The `column` configs that make the document's columns match the tree's.
221
- *
222
- * A cell without a column follows the cell before it, so only cells whose
223
- * resolved column differs from their position need one.
185
+ * The edit that writes the code a reducer action changed, or null when none
186
+ * did. Order, creation, deletion, names and configs are edited on the model
187
+ * directly by their atoms.
224
188
  */
225
- function columnFixes(state: NotebookState): Array<[CellId, number | null]> {
226
- const fixes: Array<[CellId, number | null]> = [];
227
- const columns = state.cellIds.getColumns();
228
- if (columns.length === 1) {
229
- for (const id of columns[0].inOrderIds) {
230
- const column = state.cellData[id].config.column;
231
- if (column != null && column !== 0) {
232
- fixes.push([id, null]);
233
- }
234
- }
235
- return fixes;
236
- }
237
- let inherited = 0;
238
- columns.forEach((column, target) => {
239
- for (const id of column.inOrderIds) {
240
- const configured = state.cellData[id].config.column;
241
- const resolved =
242
- configured != null && configured >= 0 ? configured : inherited;
243
- if (resolved !== target) {
244
- fixes.push([id, target]);
245
- }
246
- inherited = target;
247
- }
248
- });
249
- return fixes;
250
- }
251
-
252
- /** The edit that makes the document match `next`, or null when it already does. */
253
189
  function diffNotebook(
254
190
  prev: NotebookState,
255
191
  next: NotebookState,
256
192
  ): DocumentEdit | null {
257
193
  const steps: Array<(tx: DocumentEditor) => void> = [];
258
- const prevOrder = prev.cellIds.inOrderIds;
259
- const nextOrder = next.cellIds.inOrderIds;
260
- const prevIds = new Set(prevOrder);
261
- const nextIds = new Set(nextOrder);
262
- for (const id of prevOrder) {
263
- if (!nextIds.has(id)) {
264
- steps.push((tx) => tx.deleteCell(id));
265
- }
266
- }
267
- const created: CellId[] = [];
268
- for (const id of nextOrder) {
194
+ for (const id of next.cellIds) {
269
195
  const cell = next.cellData[id];
270
196
  const before = prev.cellData[id];
271
- if (!prevIds.has(id) || before === undefined) {
272
- created.push(id);
273
- steps.push((tx) =>
274
- tx.createCell({
275
- id,
276
- code: cell.code,
277
- name: cell.name,
278
- config: cell.config,
279
- }),
280
- );
281
- continue;
282
- }
283
- if (before === cell) {
284
- continue;
285
- }
286
- if (before.code !== cell.code) {
197
+ if (before !== undefined && cell !== before && before.code !== cell.code) {
287
198
  steps.push((tx) => tx.setCode(id, cell.code));
288
199
  }
289
- if (before.name !== cell.name) {
290
- steps.push((tx) => tx.setName(id, cell.name));
291
- }
292
- const patch = configPatch(before.config, cell.config);
293
- if (patch !== null) {
294
- steps.push((tx) => tx.updateConfig(id, patch));
295
- }
296
- }
297
- for (const [id, column] of columnFixes(next)) {
298
- steps.push((tx) => tx.updateConfig(id, { column }));
299
- }
300
- // Created cells go to the end, so the order is set when they belong elsewhere.
301
- const survivors = prevOrder.filter((id) => nextIds.has(id));
302
- if (!isEqual(nextOrder, [...survivors, ...created])) {
303
- steps.push((tx) => tx.setOrder(nextOrder));
304
200
  }
305
201
  if (steps.length === 0) {
306
202
  return null;
@@ -316,8 +212,8 @@ function diffNotebook(
316
212
  * Middleware for the notebook reducer. Writes what an action changed about
317
213
  * the cells and their order into the document model as one edit.
318
214
  *
319
- * TODO(MO-8028): Each action family that moves onto the model removes its
320
- * part of `diffNotebook`. The middleware goes with the last one, code.
215
+ * TODO(MO-8028): Only code is left in `diffNotebook`. The middleware goes
216
+ * with it, once the editors bind to the model.
321
217
  */
322
218
  export function documentEditMiddleware(
323
219
  prev: NotebookState,
@@ -3,15 +3,83 @@
3
3
  /** The cells as they appear on the page: by column, with folded sections hidden. */
4
4
 
5
5
  import { atom, useAtomValue } from "jotai";
6
- import type { CellColumnId } from "@/utils/id-tree";
6
+ import type { EditableDocument } from "@/core/notebook/document";
7
7
  import { findCollapseRange } from "../dom/outline";
8
- import { cellIdsAtom, cellOutlinesAtom } from "./cells";
9
- import type { CellId } from "./ids";
8
+ import { activeDocumentModelAtom } from "../session/active-notebook";
9
+ import { cellOutlinesAtom } from "./cells";
10
+ import { groupByColumn } from "./columns";
11
+ import { CellId } from "./ids";
10
12
  import type { Outline } from "./outline";
11
13
 
14
+ /** A column's identity on this page, as drag and drop and saved widths refer to it. */
15
+ export type CellColumnId = string & { __brand: "CellColumnId" };
16
+
12
17
  /** The headers whose sections the user folded. Only this browser knows. */
13
18
  export const collapsedAtom = atom<ReadonlySet<CellId>>(new Set<CellId>());
14
19
 
20
+ /**
21
+ * This browser's names for the document's columns, by position. The
22
+ * document knows columns only by position, so `followColumnsAtom` names
23
+ * a column as it appears and forgets one as it goes, and the column
24
+ * operations carry the names along as they rearrange.
25
+ */
26
+ export const columnIdsAtom = atom<readonly CellColumnId[]>([]);
27
+
28
+ /**
29
+ * Trailing columns a rearrangement here emptied. The document cannot hold
30
+ * an empty column at its end, so the page keeps them, as drop targets for
31
+ * the cell being dragged, until a drop compacts them.
32
+ */
33
+ export const emptyColumnIdsAtom = atom<readonly CellColumnId[]>([]);
34
+
35
+ export function newColumnId(): CellColumnId {
36
+ return `column-${CellId.create()}` as CellColumnId;
37
+ }
38
+
39
+ export function isColumnId(id: unknown): id is CellColumnId {
40
+ return typeof id === "string" && id.startsWith("column-");
41
+ }
42
+
43
+ /**
44
+ * Name the document's columns as the document changes. A column that
45
+ * appears takes over an empty column the page keeps, or gets a fresh id;
46
+ * one that goes is forgotten.
47
+ */
48
+ export const followColumnsAtom = atom(
49
+ null,
50
+ (get, set, document: EditableDocument): void => {
51
+ const count = groupByColumn(document).length;
52
+ const ids = get(columnIdsAtom);
53
+ if (ids.length === count) {
54
+ return;
55
+ }
56
+ const empty = get(emptyColumnIdsAtom);
57
+ const named = [...ids, ...empty].slice(0, count);
58
+ while (named.length < count) {
59
+ named.push(newColumnId());
60
+ }
61
+ set(columnIdsAtom, named);
62
+ set(emptyColumnIdsAtom, empty.slice(Math.max(0, count - ids.length)));
63
+ },
64
+ );
65
+
66
+ /** The document's columns, each with its id, then the empty ones the page keeps. */
67
+ function columnsOf(
68
+ document: EditableDocument | null,
69
+ ids: readonly CellColumnId[],
70
+ empty: readonly CellColumnId[],
71
+ ): Array<{ id: CellColumnId; ids: CellId[] }> {
72
+ const groups = document === null ? [[]] : groupByColumn(document);
73
+ return [
74
+ ...groups.map((cells, index) => ({
75
+ // Before the first document names them, columns go by position.
76
+ id: ids[index] ?? (`column-${index}` as CellColumnId),
77
+ ids: cells,
78
+ })),
79
+ ...empty.map((id) => ({ id, ids: [] as CellId[] })),
80
+ ];
81
+ }
82
+
15
83
  export interface LayoutColumn {
16
84
  readonly id: CellColumnId;
17
85
  /** Every cell of the column, in order. */
@@ -69,18 +137,17 @@ export function deriveLayout(
69
137
  }
70
138
 
71
139
  /**
72
- * The layout of the notebook's cells.
140
+ * The layout of the notebook's cells: the document's order and columns,
141
+ * this browser's column ids and folds, and the kernel's outlines.
73
142
  *
74
- * TODO(MO-8028): Order and columns come from the reducer's tree until the
75
- * arrange family edits the document; then they derive from the document and
76
- * the column ids this browser keeps. Outlines come from executions later.
143
+ * TODO(MO-8028): Outlines come from executions later.
77
144
  */
78
145
  export const layoutAtom = atom((get): Layout => {
146
+ const model = get(activeDocumentModelAtom);
147
+ const document = model === null ? null : get(model.documentAtom);
79
148
  const outlines = get(cellOutlinesAtom);
80
149
  return deriveLayout(
81
- get(cellIdsAtom)
82
- .getColumns()
83
- .map((column) => ({ id: column.id, ids: column.inOrderIds })),
150
+ columnsOf(document, get(columnIdsAtom), get(emptyColumnIdsAtom)),
84
151
  (id) => outlines[id] ?? null,
85
152
  get(collapsedAtom),
86
153
  );
@@ -88,6 +155,8 @@ export const layoutAtom = atom((get): Layout => {
88
155
 
89
156
  export const useLayout = () => useAtomValue(layoutAtom);
90
157
 
158
+ export const numColumnsAtom = atom((get) => get(layoutAtom).columns.length);
159
+
91
160
  export function findColumn(
92
161
  layout: Layout,
93
162
  columnId: CellColumnId,
@@ -49,7 +49,7 @@ const { valueAtom: pendingDeleteStateAtom, useActions } = createReducerAndAtoms(
49
49
  const variables = store.get(variablesAtom);
50
50
 
51
51
  const emptyCells = new Set(
52
- notebook.cellIds.inOrderIds.filter(
52
+ notebook.cellIds.filter(
53
53
  (id) => notebook.cellData[id].code.trim() === "",
54
54
  ),
55
55
  );
@@ -25,7 +25,7 @@ export function notebookNeedsRun(state: NotebookState) {
25
25
  }
26
26
 
27
27
  export function notebookCells(state: NotebookState) {
28
- return state.cellIds.inOrderIds.map((cellId) => state.cellData[cellId]);
28
+ return state.cellIds.map((cellId) => state.cellData[cellId]);
29
29
  }
30
30
 
31
31
  export function notebookCellEditorViews({ cellHandles }: NotebookState) {
@@ -42,7 +42,7 @@ export function notebookCellEditorViews({ cellHandles }: NotebookState) {
42
42
  export function disabledCellIds(state: NotebookState) {
43
43
  const { cellIds, cellData } = state;
44
44
  const disabledCells: CellId[] = [];
45
- for (const cellId of cellIds.inOrderIds) {
45
+ for (const cellId of cellIds) {
46
46
  const cell = cellData[cellId];
47
47
  if (cell.config.disabled) {
48
48
  disabledCells.push(cellId);
@@ -54,7 +54,7 @@ export function disabledCellIds(state: NotebookState) {
54
54
  export function enabledCellIds(state: NotebookState) {
55
55
  const { cellIds, cellData } = state;
56
56
  const enabledCells: CellId[] = [];
57
- for (const cellId of cellIds.inOrderIds) {
57
+ for (const cellId of cellIds) {
58
58
  const cell = cellData[cellId];
59
59
  if (!cell.config.disabled) {
60
60
  enabledCells.push(cellId);
@@ -108,7 +108,7 @@ export function getDescendantsStatus(
108
108
  */
109
109
  export function staleCellIds(state: NotebookState) {
110
110
  const { cellIds, cellData, cellRuntime } = state;
111
- return cellIds.inOrderIds.filter(
111
+ return cellIds.filter(
112
112
  (cellId) =>
113
113
  isUninstantiated({
114
114
  // runElapstedTimeMs is what we've seen in this session
@@ -9,7 +9,6 @@ import { createCell } from "@/core/cells/types";
9
9
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
10
10
  import { store } from "@/core/state/jotai";
11
11
  import { variablesAtom } from "@/core/variables/state";
12
- import { MultiColumn } from "@/utils/id-tree";
13
12
  import { cellConfigExtension } from "../../config/extension";
14
13
  import { adaptiveLanguageConfiguration } from "../../language/extension";
15
14
  import {
@@ -159,7 +158,7 @@ describe("getCodes", () => {
159
158
  ];
160
159
  store.set(notebookAtom, {
161
160
  ...initialNotebookState(),
162
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2]]),
161
+ cellIds: [Cells.cell1, Cells.cell2],
163
162
  cellHandles: {
164
163
  [Cells.cell1]: { current: mockEditorViews[0] },
165
164
  [Cells.cell2]: { current: mockEditorViews[1] },
@@ -177,7 +176,7 @@ describe("getCodes", () => {
177
176
  ];
178
177
  store.set(notebookAtom, {
179
178
  ...initialNotebookState(),
180
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2]]),
179
+ cellIds: [Cells.cell1, Cells.cell2],
181
180
  cellHandles: {
182
181
  [Cells.cell1]: { current: mockEditorViews[0] },
183
182
  [Cells.cell2]: { current: mockEditorViews[1] },
@@ -196,7 +195,7 @@ describe("getCodes", () => {
196
195
  ];
197
196
  store.set(notebookAtom, {
198
197
  ...initialNotebookState(),
199
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2, Cells.cell3]]),
198
+ cellIds: [Cells.cell1, Cells.cell2, Cells.cell3],
200
199
  cellHandles: {
201
200
  [Cells.cell1]: { current: mockEditorViews[0] },
202
201
  [Cells.cell2]: { current: mockEditorViews[1] },
@@ -218,7 +217,7 @@ describe("getCodes", () => {
218
217
  const otherCode = "print('Hello World')";
219
218
  store.set(notebookAtom, {
220
219
  ...initialNotebookState(),
221
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2]]),
220
+ cellIds: [Cells.cell1, Cells.cell2],
222
221
  cellData: {
223
222
  [Cells.cell1]: createCell({ id: Cells.cell1, code: "import os" }),
224
223
  [Cells.cell2]: createCell({ id: Cells.cell2, code: "x = 1" }),
@@ -232,7 +231,7 @@ describe("getCodes", () => {
232
231
  const otherCode = "print('Hello World')";
233
232
  store.set(notebookAtom, {
234
233
  ...initialNotebookState(),
235
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2]]),
234
+ cellIds: [Cells.cell1, Cells.cell2],
236
235
  cellData: {
237
236
  [Cells.cell1]: createCell({ id: Cells.cell1, code: "import os" }),
238
237
  [Cells.cell2]: createCell({ id: Cells.cell2, code: "x = 1" }),
@@ -250,7 +249,7 @@ describe("topologicalCodesAtom", () => {
250
249
  it("should include every cell when no editor is mounted", () => {
251
250
  store.set(notebookAtom, {
252
251
  ...initialNotebookState(),
253
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2, Cells.cell3]]),
252
+ cellIds: [Cells.cell1, Cells.cell2, Cells.cell3],
254
253
  cellData: {
255
254
  [Cells.cell1]: createCell({ id: Cells.cell1, code: "import os" }),
256
255
  [Cells.cell2]: createCell({ id: Cells.cell2, code: "x = 1" }),
@@ -12,7 +12,7 @@ import { getEditorCodeAsPython } from "../language/utils";
12
12
  const notebookCellCodes = atom((get) => {
13
13
  const notebook = get(notebookAtom);
14
14
  const codes = Objects.fromEntries(
15
- notebook.cellIds.inOrderIds.map((id) => {
15
+ notebook.cellIds.map((id) => {
16
16
  // A cell editor mounts progressively, so the notebook store is the only
17
17
  // complete source of code until every editor is built.
18
18
  const editorView = notebook.cellHandles[id]?.current?.editorViewOrNull;
@@ -54,7 +54,7 @@ export function getOtherCellsCode(otherCode: string) {
54
54
 
55
55
  const inOrderCellIdsAtom = atom((get) => {
56
56
  const notebook = get(notebookAtom);
57
- return notebook.cellIds.inOrderIds;
57
+ return notebook.cellIds;
58
58
  });
59
59
  const topologicalCellIdsAtom = atom((get) => {
60
60
  const cellIds = get(inOrderCellIdsAtom);
@@ -11,7 +11,6 @@ import type { CellId } from "@/core/cells/ids";
11
11
  import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
12
12
  import { createCell } from "@/core/cells/types";
13
13
  import { store } from "@/core/state/jotai";
14
- import { MultiColumn } from "@/utils/id-tree";
15
14
  import { topologicalCodesAtom } from "../../copilot/getCodes";
16
15
  import { lspWorkspaceAtom } from "@/core/saving/file-state";
17
16
  import { languageAdapterState } from "../../language/extension";
@@ -69,7 +68,7 @@ describe("merged document with unmounted editors", () => {
69
68
  it("should include the code of cells whose editors are not mounted", () => {
70
69
  store.set(notebookAtom, {
71
70
  ...initialNotebookState(),
72
- cellIds: MultiColumn.from([[Cells.cell1, Cells.cell2]]),
71
+ cellIds: [Cells.cell1, Cells.cell2],
73
72
  cellData: {
74
73
  [Cells.cell1]: createCell({ id: Cells.cell1, code: "import math" }),
75
74
  [Cells.cell2]: createCell({
@@ -13,7 +13,7 @@ export function isKnownCellDocumentUri(uri: string): boolean {
13
13
  if (!CellDocumentUri.is(uri)) {
14
14
  return false;
15
15
  }
16
- return store.get(cellIdsAtom).inOrderIds.includes(CellDocumentUri.parse(uri));
16
+ return store.get(cellIdsAtom).includes(CellDocumentUri.parse(uri));
17
17
  }
18
18
 
19
19
  export function getLspRootUri() {
@@ -17,7 +17,7 @@ export async function requestOutputDocumentation(
17
17
  qualifiedName: string,
18
18
  ): Promise<void> {
19
19
  // We need any valid cell_id for the completion request.
20
- const cellId = getCells().inOrderIds.at(0);
20
+ const cellId = getCells().at(0);
21
21
  if (!cellId) {
22
22
  return;
23
23
  }
@@ -27,11 +27,8 @@ import { useSetOutputsExpanded } from "../components/editor/actions/useSetOutput
27
27
  import { CellArray } from "../components/editor/renderers/cell-array";
28
28
  import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
29
29
  import { useHotkey } from "../hooks/useHotkey";
30
- import {
31
- hasCellsAtom,
32
- notebookIsRunningAtom,
33
- numColumnsAtom,
34
- } from "./cells/cells";
30
+ import { hasCellsAtom, notebookIsRunningAtom } from "./cells/cells";
31
+ import { numColumnsAtom } from "./cells/layout";
35
32
  import { useCellActions } from "./cells/actions";
36
33
  import type { AppConfig, UserConfig } from "./config/config-schema";
37
34
  import { RuntimeState } from "./kernel/RuntimeState";
@@ -80,7 +80,7 @@ export function getCellErrorEntries(store: JotaiStore): CellErrorEntry[] {
80
80
  const { cellIds, cellRuntime, cellData } = store.get(notebookAtom);
81
81
  const entries: CellErrorEntry[] = [];
82
82
 
83
- for (const [cellIndex, cellId] of cellIds.inOrderIds.entries()) {
83
+ for (const [cellIndex, cellId] of cellIds.entries()) {
84
84
  const cell = cellRuntime[cellId];
85
85
  const output = cell.output;
86
86
  if (!output || !isErrorMime(output.mimetype)) {
@@ -103,15 +103,11 @@ describe("initializeIslands document", () => {
103
103
  }
104
104
 
105
105
  const notebook = store.get(notebookAtom);
106
- expect(notebook.cellIds.inOrderIds).toEqual(["c1", "c3", "k7f2"]);
106
+ expect(notebook.cellIds).toEqual(["c1", "c3", "k7f2"]);
107
107
  expect(notebook.cellData[cellId("c3")].code).toBe("x = 21");
108
108
  expect(notebook.cellRuntime[cellId("c3")]).toBeDefined();
109
109
 
110
110
  handlers?.message({ event: "reset", data: {} });
111
- expect(store.get(notebookAtom).cellIds.inOrderIds).toEqual([
112
- "c1",
113
- "c3",
114
- "k7f2",
115
- ]);
111
+ expect(store.get(notebookAtom).cellIds).toEqual(["c1", "c3", "k7f2"]);
116
112
  });
117
113
  });
@@ -104,7 +104,7 @@ export class MarimoIslandElement extends HTMLElement {
104
104
  */
105
105
  private getCellIdFromIndex(idx: number): CellId | undefined {
106
106
  const { cellIds } = store.get(notebookAtom);
107
- return cellIds.inOrderIds.at(idx);
107
+ return cellIds.at(idx);
108
108
  }
109
109
 
110
110
  /**
@@ -82,6 +82,14 @@ function languages(id: CellId) {
82
82
  dropCellOverColumn: vi.fn(),
83
83
  sendToTop: vi.fn(),
84
84
  sendToBottom: vi.fn(),
85
+ addColumn: vi.fn(),
86
+ addColumnBreakpoint: vi.fn(),
87
+ deleteColumn: vi.fn(),
88
+ mergeAllColumns: vi.fn(),
89
+ compactColumns: vi.fn(),
90
+ moveColumn: vi.fn(),
91
+ splitCell: vi.fn(),
92
+ undoSplitCell: vi.fn(),
85
93
  onRun: vi.fn(),
86
94
  deleteCell: vi.fn(),
87
95
  afterToggleMarkdown: vi.fn(),
@@ -272,7 +272,7 @@ describe("useNotebookSession document model", () => {
272
272
  });
273
273
 
274
274
  let notebook = store.get(notebookAtom);
275
- expect(notebook.cellIds.inOrderIds).toEqual(["c1", "c3", "k7f2"]);
275
+ expect(notebook.cellIds).toEqual(["c1", "c3", "k7f2"]);
276
276
  expect(notebook.cellData[cellId("k7f2")]).toMatchObject({
277
277
  code: "y = x * 2",
278
278
  config: { hide_code: true },
@@ -15,7 +15,6 @@ import { requestClientAtom } from "@/core/network/requests";
15
15
  import { resolveRequestClient } from "@/core/network/resolve";
16
16
  import type { CellConfig } from "@/core/network/types";
17
17
  import { ConnectionState } from "@/core/session/types";
18
- import { MultiColumn } from "@/utils/id-tree";
19
18
  import type { Milliseconds, Seconds } from "@/utils/time";
20
19
  import { Cell as EditorCell } from "../components/editor/notebook-cell";
21
20
  import { TooltipProvider } from "../components/ui/tooltip";
@@ -55,7 +54,7 @@ const Cell: React.FC<{
55
54
  lastExecutionTime: null,
56
55
  },
57
56
  },
58
- cellIds: MultiColumn.from([[cid]]),
57
+ cellIds: [cid],
59
58
  cellRuntime: {
60
59
  [cid]: createCellRuntimeState({
61
60
  output: overrides.output ?? null,
@@ -12,7 +12,6 @@ import { connectionAtom } from "@/core/network/connection";
12
12
  import { requestClientAtom } from "@/core/network/requests";
13
13
  import { resolveRequestClient } from "@/core/network/resolve";
14
14
  import { ConnectionState } from "@/core/session/types";
15
- import { MultiColumn } from "@/utils/id-tree";
16
15
  import type { Milliseconds, Seconds } from "@/utils/time";
17
16
  import { CellArray } from "../../../components/editor/renderers/cell-array";
18
17
  import { CellsRenderer } from "../../../components/editor/renderers/cells-renderer";
@@ -52,7 +51,7 @@ LongReprA()`,
52
51
  lastExecutionTime: null,
53
52
  },
54
53
  },
55
- cellIds: MultiColumn.from([[cellId]]),
54
+ cellIds: [cellId],
56
55
  cellRuntime: {
57
56
  [cellId]: createCellRuntimeState({
58
57
  output: {
@@ -16,7 +16,6 @@ import type { EnvironmentOperation } from "@/core/alerts/environment";
16
16
  import { notebookAtom } from "@/core/cells/cells";
17
17
  import { CellId } from "@/core/cells/ids";
18
18
  import { createCell, createCellRuntimeState } from "@/core/cells/types";
19
- import { MultiColumn } from "@/utils/id-tree";
20
19
  import { userConfigAtom } from "@/core/config/config";
21
20
  import {
22
21
  type AppConfig,
@@ -111,7 +110,7 @@ function RuntimePreview({
111
110
  {props.surface === "notebook" && (
112
111
  <div className="flex-1 min-w-0 pt-12">
113
112
  <ConnectionNotice appConfig={appConfig} onRetry={reconnect} />
114
- {notebook.cellIds.inOrderIds.map((cellId) => (
113
+ {notebook.cellIds.map((cellId) => (
115
114
  <VerticalLayoutWrapper key={cellId} appConfig={appConfig}>
116
115
  <Cell
117
116
  cellId={cellId}
@@ -351,7 +350,7 @@ export function RuntimeStory(props: Props) {
351
350
  const cellId = CellId.create();
352
351
  state.set(notebookAtom, (value) => ({
353
352
  ...value,
354
- cellIds: MultiColumn.from([[cellId]]),
353
+ cellIds: [cellId],
355
354
  cellData: {
356
355
  ...value.cellData,
357
356
  [cellId]: createCell({