@marimo-team/islands 0.25.3-dev169 → 0.25.3-dev170

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 (123) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BbDrlHvG.js → ConnectedDataExplorerComponent-BbSXV-Xa.js} +8 -8
  2. package/dist/{ErrorBoundary-RaIwGWT3.js → ErrorBoundary-NE86qEWZ.js} +4 -4
  3. package/dist/{abnfDiagram-VCTEODGH-Dr-fo8_z.js → abnfDiagram-VCTEODGH-D7n07nXf.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-ByKIl9GG.js → architectureDiagram-5GKGNRK7-D6CjRib5.js} +1 -1
  5. package/dist/{chat-ui-BsV2WH-4.js → chat-ui-CC7GekpG.js} +34 -34
  6. package/dist/{chunk-IMKFNOWR-IKDP18MQ.js → chunk-IMKFNOWR-6dtPz7uS.js} +1 -1
  7. package/dist/{chunk-SHT3W25Y-LkIgegwh.js → chunk-SHT3W25Y-BwnFNKDA.js} +1 -1
  8. package/dist/{chunk-SVP7TREG-DReBYtnB.js → chunk-SVP7TREG-D2eV4vBJ.js} +1 -1
  9. package/dist/{chunk-TICWLB2K-DQ0A0-1D.js → chunk-TICWLB2K-BDs4-_-r.js} +1 -1
  10. package/dist/{classDiagram-ZZMXUADV-CwvXdCNE.js → classDiagram-ZZMXUADV-DCJnf_8v.js} +2 -2
  11. package/dist/{classDiagram-v2-VYDZK3BY-D0XAxDsc.js → classDiagram-v2-VYDZK3BY-BOmSzPBD.js} +2 -2
  12. package/dist/{code-visibility-BMi0KLAQ.js → code-visibility-Bzvc6ajl.js} +23 -23
  13. package/dist/{common-BMkJrZsD.js → common-Ciw4zkh2.js} +3412 -3412
  14. package/dist/{cynefinDiagram-5FMLGOSQ-BFN68nHr.js → cynefinDiagram-5FMLGOSQ-Bln_Soky.js} +1 -1
  15. package/dist/{dagre-GXQ25YYZ-CZt4rtLq.js → dagre-GXQ25YYZ-D-nf7WzW.js} +1 -1
  16. package/dist/{data-grid-overlay-editor-CCc1P42P.js → data-grid-overlay-editor-AQREmxI7.js} +1 -1
  17. package/dist/{diagram-S7CK7UJ4-D5cOz47_.js → diagram-S7CK7UJ4-PsgSMkY2.js} +1 -1
  18. package/dist/{diagram-UQ7AKVKN-Qgdppb4a.js → diagram-UQ7AKVKN-DfwfUuSw.js} +1 -1
  19. package/dist/{diagram-VX7I27RA-BsDCvily.js → diagram-VX7I27RA-C9_e-oMc.js} +1 -1
  20. package/dist/{diagram-Z3DM3KII-igTkGjKI.js → diagram-Z3DM3KII-CWLa5tuY.js} +1 -1
  21. package/dist/{dropdown-menu-BWnnAstC.js → dropdown-menu-B6ftyzOg.js} +1 -1
  22. package/dist/{ebnfDiagram-PWID7BFC-BHuZGjIr.js → ebnfDiagram-PWID7BFC-B1QRM58v.js} +2 -2
  23. package/dist/{erDiagram-RLTQ6QDP-DKObXTVE.js → erDiagram-RLTQ6QDP-DJK_FqvW.js} +1 -1
  24. package/dist/{error-entries-BpUJSLUZ.js → error-entries-DgK3wUJR.js} +5640 -5591
  25. package/dist/{feedback-button-CMVkEos_.js → feedback-button-8a6SETmT.js} +24 -24
  26. package/dist/{flowDiagram-HODETNUW-DZI_kit0.js → flowDiagram-HODETNUW-Uxc25XSJ.js} +2 -2
  27. package/dist/{formats-CwJHWasD.js → formats-DQCyPd5Z.js} +1 -1
  28. package/dist/{glide-data-editor-BA2pcHSV.js → glide-data-editor-B70oke_v.js} +5 -5
  29. package/dist/{globals-B5jjJ0ms.js → globals-Cxd5hNlI.js} +2 -2
  30. package/dist/{html-to-image-CQr2-IDA.js → html-to-image-D4yG5Ru0.js} +25 -25
  31. package/dist/{infoDiagram-27XIBGKW-DdPl-7-n.js → infoDiagram-27XIBGKW-DmjxqwL0.js} +1 -1
  32. package/dist/{ishikawaDiagram-5VMMS53U-DbyW6l59.js → ishikawaDiagram-5VMMS53U-NgCLHqQi.js} +1 -1
  33. package/dist/{kanban-definition-UXKFOSKX-BMX9SyBS.js → kanban-definition-UXKFOSKX-CZaG-iam.js} +1 -1
  34. package/dist/main.js +187 -188
  35. package/dist/{mermaid-C8W8mTbA.js → mermaid-CSvw7c71.js} +34 -34
  36. package/dist/{mindmap-definition-YA3MSWOX-Zd_tAxWL.js → mindmap-definition-YA3MSWOX-DJYQBDH_.js} +1 -1
  37. package/dist/{number-overlay-editor-BIWiV-TU.js → number-overlay-editor-ChK-SKUk.js} +1 -1
  38. package/dist/{pegDiagram-XKGWAZYB-DjYydPdo.js → pegDiagram-XKGWAZYB-CHKO_dAe.js} +2 -2
  39. package/dist/{pieDiagram-E7YTZNPT-R5LKjO2n.js → pieDiagram-E7YTZNPT-DVevbMAo.js} +1 -1
  40. package/dist/{process-output-Uw-VJE5V.js → process-output-DxvBAeL3.js} +3 -3
  41. package/dist/{railroadDiagram-O6MQD6OU-C7uNKtfm.js → railroadDiagram-O6MQD6OU-Detj1kcB.js} +2 -2
  42. package/dist/{requirementDiagram-BXWQKSXE-CxAIzQfl.js → requirementDiagram-BXWQKSXE-BLFD0T_Z.js} +1 -1
  43. package/dist/{reveal-component-XiecZ3Ua.js → reveal-component-CXMXeDZD.js} +6 -6
  44. package/dist/{sequenceDiagram-WJ2MYXX4-B5tmlPd5.js → sequenceDiagram-WJ2MYXX4-BZBgatQV.js} +1 -1
  45. package/dist/{spec-45FnujvG.js → spec-Mnkn3XEr.js} +1 -1
  46. package/dist/{stateDiagram-D77RDMKH-C1hc5H7Y.js → stateDiagram-D77RDMKH-4BZW9-6_.js} +2 -2
  47. package/dist/{stateDiagram-v2-MP3YSRHH-DRWeqQTn.js → stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js} +2 -2
  48. package/dist/style.css +1 -1
  49. package/dist/{swimlanes-42K2YHIH-NTLas--s.js → swimlanes-42K2YHIH-SL5yeGe2.js} +1 -1
  50. package/dist/{swimlanesDiagram-VR7AAH4N-ChPggljD.js → swimlanesDiagram-VR7AAH4N-DqPmAYUv.js} +2 -2
  51. package/dist/{timeline-definition-24CTP7MA-B4CZ9srJ.js → timeline-definition-24CTP7MA-BfXUMWkq.js} +1 -1
  52. package/dist/{toDate-rySyYxqk.js → toDate-TMubKIVL.js} +22 -22
  53. package/dist/{useAsyncData-B7MiltyV.js → useAsyncData-DjMGFzFy.js} +1 -1
  54. package/dist/{useDeepCompareMemoize-DZmHBD_1.js → useDeepCompareMemoize-BQR5TJot.js} +1 -1
  55. package/dist/{useLifecycle-DRbRhzxg.js → useLifecycle-C_GvKWbQ.js} +26 -23
  56. package/dist/{useTheme-TLkqJsvo.js → useTheme-Bns2gJZ6.js} +179 -178
  57. package/dist/{vega-component-DC6P127L.js → vega-component-BsVptuhz.js} +5 -5
  58. package/dist/{vennDiagram-4TSXK5OY-DJIvMt44.js → vennDiagram-4TSXK5OY-CH65znGa.js} +1 -1
  59. package/dist/{wardleyDiagram-VM6X3IG4-45H0zg0u.js → wardleyDiagram-VM6X3IG4-z9pWKb2_.js} +1 -1
  60. package/dist/{xychartDiagram-S5SC5T6Z-ZT-XzcPN.js → xychartDiagram-S5SC5T6Z-MxyaWjjq.js} +1 -1
  61. package/package.json +1 -1
  62. package/src/__mocks__/requests.ts +0 -1
  63. package/src/components/editor/cell/StagedAICell.tsx +10 -2
  64. package/src/components/editor/cell/code/cell-editor.tsx +7 -14
  65. package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
  66. package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
  67. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  68. package/src/core/ai/__tests__/staged-cells.test.ts +8 -5
  69. package/src/core/ai/staged-cells.ts +7 -2
  70. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +3 -15
  71. package/src/core/ai/tools/edit-notebook-tool.ts +14 -3
  72. package/src/core/cells/__tests__/cells.test.ts +0 -297
  73. package/src/core/cells/__tests__/document.test.ts +410 -0
  74. package/src/core/cells/__tests__/flush-document.test.ts +40 -0
  75. package/src/core/cells/__tests__/notebook-outline.test.ts +1 -6
  76. package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
  77. package/src/core/cells/cells.ts +27 -158
  78. package/src/core/cells/columns.ts +38 -0
  79. package/src/core/cells/document.ts +360 -0
  80. package/src/core/cells/types.ts +12 -0
  81. package/src/core/codemirror/cells/__tests__/extensions.test.ts +27 -1
  82. package/src/core/codemirror/cells/extensions.ts +14 -0
  83. package/src/core/codemirror/format.ts +6 -5
  84. package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
  85. package/src/core/codemirror/language/cell-code.ts +114 -0
  86. package/src/core/codemirror/language/extension.ts +47 -30
  87. package/src/core/codemirror/language/languages/sql/sql.ts +7 -17
  88. package/src/core/codemirror/language/utils.ts +9 -39
  89. package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
  90. package/src/core/codemirror/rtc/extension.ts +0 -1
  91. package/src/core/edit-app.tsx +4 -3
  92. package/src/core/export/layout.ts +2 -2
  93. package/src/core/islands/bridge.ts +0 -1
  94. package/src/core/network/requests-lazy.ts +0 -1
  95. package/src/core/network/requests-network.ts +0 -9
  96. package/src/core/network/requests-static.ts +0 -1
  97. package/src/core/network/requests-toasting.tsx +0 -1
  98. package/src/core/network/types.ts +0 -5
  99. package/src/core/notebook/document/__tests__/codemirror.test.ts +76 -14
  100. package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
  101. package/src/core/notebook/document/__tests__/hooks.test.tsx +3 -3
  102. package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +3 -3
  103. package/src/core/notebook/document/__tests__/model.test.ts +71 -15
  104. package/src/core/notebook/document/__tests__/scenario-fixture.ts +1 -1
  105. package/src/core/notebook/document/codemirror.ts +27 -122
  106. package/src/core/notebook/document/hooks.tsx +2 -2
  107. package/src/core/notebook/document/index.ts +9 -1
  108. package/src/core/notebook/document/model.ts +74 -24
  109. package/src/core/notebook/document/sync.ts +53 -4
  110. package/src/core/run-app.tsx +44 -41
  111. package/src/core/saving/save-component.tsx +2 -5
  112. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +63 -3
  113. package/src/core/session/active-notebook.ts +4 -0
  114. package/src/core/session/useNotebookSession.tsx +39 -19
  115. package/src/core/wasm/__tests__/bridge.test.ts +3 -3
  116. package/src/core/wasm/bridge.ts +5 -12
  117. package/src/core/wasm/worker/worker.ts +0 -9
  118. package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +28 -0
  119. package/src/utils/createReducer.ts +42 -21
  120. package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
  121. package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
  122. package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
  123. package/src/core/cells/document-changes.ts +0 -944
@@ -1,991 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- /**
4
- * Tests for fromDocumentChanges / applyTransactionChanges in isolation.
5
- *
6
- * These test the change→action mapping for edge cases and error paths that
7
- * can't be exercised via the round-trip tests (since toDocumentChanges would
8
- * never produce malformed or conflicting changes). Basic correctness is
9
- * covered by document-roundtrip.test.ts. This file focuses on:
10
- *
11
- * - Multi-change transactions (create+move, create+set-code, set-code+delete)
12
- * - Cancelled changes (create+delete same cell)
13
- * - Missing/nonexistent anchors and cells
14
- * - Config propagation on create-cell (disabled, column)
15
- */
16
-
17
- import { python } from "@codemirror/lang-python";
18
- import { EditorState } from "@codemirror/state";
19
- import { EditorView } from "@codemirror/view";
20
- import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
21
- import { cellId } from "@/__tests__/branded";
22
- import type { CellHandle } from "@/components/editor/notebook-cell";
23
- import { adaptiveLanguageConfiguration } from "@/core/codemirror/language/extension";
24
- import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
25
- import { MultiColumn } from "@/utils/id-tree";
26
- import { exportedForTesting, type NotebookState } from "../cells";
27
- import {
28
- applyTransactionChanges,
29
- exportedForTesting as middlewareExports,
30
- } from "../document-changes";
31
- import { CellId } from "../ids";
32
-
33
- const { initialNotebookState, reducer, createActions } = exportedForTesting;
34
-
35
- function createEditor(code: string) {
36
- const state = EditorState.create({
37
- doc: code,
38
- extensions: [
39
- python(),
40
- adaptiveLanguageConfiguration({
41
- cellId: "cell1" as CellId,
42
- completionConfig: {
43
- activate_on_typing: true,
44
- signature_hint_on_typing: false,
45
- copilot: false,
46
- codeium_api_key: null,
47
- },
48
- hotkeys: new OverridingHotkeyProvider({}),
49
- placeholderType: "marimo-import",
50
- lspConfig: {},
51
- }),
52
- ],
53
- });
54
- return new EditorView({ state, parent: document.body });
55
- }
56
-
57
- let state: NotebookState;
58
- let actions: ReturnType<typeof createActions>;
59
-
60
- function setup(...codes: string[]) {
61
- state = initialNotebookState();
62
- state.cellIds = MultiColumn.from([]);
63
- actions = createActions((action) => {
64
- state = reducer(state, action);
65
- for (const [cellId, handle] of Object.entries(state.cellHandles)) {
66
- if (!handle.current) {
67
- const view = createEditor(state.cellData[cellId as CellId].code);
68
- const h: CellHandle = { editorView: view, editorViewOrNull: view };
69
- state.cellHandles[cellId as CellId] = { current: h };
70
- }
71
- }
72
- });
73
- for (const code of codes) {
74
- actions.createNewCell({ cellId: "__end__", before: false, code });
75
- }
76
- }
77
-
78
- afterEach(() => {
79
- middlewareExports.cancelPendingChanges();
80
- });
81
-
82
- function apply(changes: Parameters<typeof applyTransactionChanges>[0]) {
83
- applyTransactionChanges(changes, actions, () => state.cellIds.inOrderIds);
84
- }
85
-
86
- /** Snapshot of document state: ordering, code, name, config. */
87
- function pretty(s: NotebookState): string {
88
- const lines = s.cellIds.inOrderIds.map((id) => {
89
- const cell = s.cellData[id];
90
- const flags: string[] = [];
91
- if (cell.name && cell.name !== "_") {
92
- flags.push(`name=${cell.name}`);
93
- }
94
- if (cell.config.hide_code) {
95
- flags.push("hide_code");
96
- }
97
- if (cell.config.disabled) {
98
- flags.push("disabled");
99
- }
100
- if (cell.config.column != null) {
101
- flags.push(`col=${cell.config.column}`);
102
- }
103
- const suffix = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
104
- return `${id}: '${cell.code}'${suffix}`;
105
- });
106
- return `\n${lines.join("\n")}\n`;
107
- }
108
-
109
- /** Snapshot showing the physical column grouping in the MultiColumn tree. */
110
- function prettyColumns(s: NotebookState): string {
111
- const lines = s.cellIds
112
- .getColumns()
113
- .map((col, idx) => `col${idx}: [${col.inOrderIds.join(", ")}]`);
114
- return `\n${lines.join("\n")}\n`;
115
- }
116
-
117
- let i = 0;
118
-
119
- beforeAll(() => {
120
- CellId.create = () => `${i++}` as CellId;
121
- });
122
-
123
- beforeEach(() => {
124
- i = 0;
125
- });
126
-
127
- describe("applyTransactionChanges edge cases", () => {
128
- it("create-cell applies disabled and column config", () => {
129
- setup("a");
130
- apply([
131
- {
132
- type: "create-cell",
133
- cellId: cellId("new-cell"),
134
- code: "configured",
135
- name: "",
136
- config: { hide_code: true, disabled: true, column: 1 },
137
- },
138
- ]);
139
- expect(pretty(state)).toMatchInlineSnapshot(`
140
- "
141
- 0: 'a'
142
- new-cell: 'configured' [hide_code, disabled, col=1]
143
- "
144
- `);
145
- // The new cell must physically land in the second column, not just
146
- // carry col=1 as stale metadata.
147
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
148
- "
149
- col0: [0]
150
- col1: [new-cell]
151
- "
152
- `);
153
- });
154
-
155
- it("create-cell then move-cell in same transaction", () => {
156
- setup("a", "b");
157
- const [a] = state.cellIds.inOrderIds;
158
- apply([
159
- {
160
- type: "create-cell",
161
- cellId: cellId("new-cell"),
162
- code: "new",
163
- name: "",
164
- config: {},
165
- },
166
- { type: "move-cell", cellId: cellId("new-cell"), before: a },
167
- ]);
168
- expect(pretty(state)).toMatchInlineSnapshot(`
169
- "
170
- new-cell: 'new'
171
- 0: 'a'
172
- 1: 'b'
173
- "
174
- `);
175
- });
176
-
177
- it("create-cell then set-code in same transaction", () => {
178
- setup("a");
179
- apply([
180
- {
181
- type: "create-cell",
182
- cellId: cellId("new-cell"),
183
- code: "initial",
184
- name: "",
185
- config: {},
186
- },
187
- { type: "set-code", cellId: cellId("new-cell"), code: "updated" },
188
- ]);
189
- expect(pretty(state)).toMatchInlineSnapshot(`
190
- "
191
- 0: 'a'
192
- new-cell: 'updated'
193
- "
194
- `);
195
- });
196
-
197
- it("create-cell then delete-cell same cell cancels out", () => {
198
- setup("a");
199
- apply([
200
- {
201
- type: "create-cell",
202
- cellId: cellId("ephemeral"),
203
- code: "tmp",
204
- name: "",
205
- config: {},
206
- },
207
- { type: "delete-cell", cellId: cellId("ephemeral") },
208
- ]);
209
- expect(pretty(state)).toMatchInlineSnapshot(`
210
- "
211
- 0: 'a'
212
- "
213
- `);
214
- });
215
-
216
- it("multiple changes in one transaction", () => {
217
- setup("a", "b", "c");
218
- const [a, b, c] = state.cellIds.inOrderIds;
219
- apply([
220
- { type: "set-code", cellId: a, code: "x = 1" },
221
- { type: "set-name", cellId: b, name: "middle" },
222
- { type: "delete-cell", cellId: c },
223
- ]);
224
- expect(pretty(state)).toMatchInlineSnapshot(`
225
- "
226
- 0: 'x = 1'
227
- 1: 'b' [name=middle]
228
- "
229
- `);
230
- });
231
-
232
- it("move-cell with no anchor appends to end", () => {
233
- setup("a", "b", "c");
234
- const [a] = state.cellIds.inOrderIds;
235
- apply([{ type: "move-cell", cellId: a }]);
236
- expect(pretty(state)).toMatchInlineSnapshot(`
237
- "
238
- 1: 'b'
239
- 2: 'c'
240
- 0: 'a'
241
- "
242
- `);
243
- });
244
-
245
- it("move-cell with missing after anchor falls back to end", () => {
246
- setup("a", "b");
247
- const [a] = state.cellIds.inOrderIds;
248
- apply([{ type: "move-cell", cellId: a, after: cellId("nonexistent") }]);
249
- expect(pretty(state)).toMatchInlineSnapshot(`
250
- "
251
- 1: 'b'
252
- 0: 'a'
253
- "
254
- `);
255
- });
256
-
257
- it("move-cell with missing before anchor falls back to start", () => {
258
- setup("a", "b");
259
- const [, b] = state.cellIds.inOrderIds;
260
- apply([{ type: "move-cell", cellId: b, before: cellId("nonexistent") }]);
261
- expect(pretty(state)).toMatchInlineSnapshot(`
262
- "
263
- 1: 'b'
264
- 0: 'a'
265
- "
266
- `);
267
- });
268
-
269
- it("move-cell on nonexistent cell is a no-op", () => {
270
- setup("a", "b");
271
- apply([
272
- {
273
- type: "move-cell",
274
- cellId: cellId("nonexistent"),
275
- after: cellId("0"),
276
- },
277
- ]);
278
- expect(pretty(state)).toMatchInlineSnapshot(`
279
- "
280
- 0: 'a'
281
- 1: 'b'
282
- "
283
- `);
284
- });
285
-
286
- it("delete-cell for nonexistent cell does not crash subsequent changes", () => {
287
- setup("a", "b", "c");
288
- const [, b] = state.cellIds.inOrderIds;
289
- // Simulate the scenario from the bug report: a delete-cell for a cell ID
290
- // that was never added to the frontend, followed by a create-cell and
291
- // a set-code update. The delete should be silently skipped, and the rest
292
- // of the transaction should still apply.
293
- apply([
294
- { type: "delete-cell", cellId: cellId("nonexistent") },
295
- {
296
- type: "create-cell",
297
- cellId: cellId("VrZA"),
298
- code: "import altair as alt",
299
- name: "",
300
- config: { hide_code: true },
301
- },
302
- { type: "set-code", cellId: b, code: "updated" },
303
- ]);
304
- expect(pretty(state)).toMatchInlineSnapshot(`
305
- "
306
- 0: 'a'
307
- 1: 'updated'
308
- 2: 'c'
309
- VrZA: 'import altair as alt' [hide_code]
310
- "
311
- `);
312
- });
313
-
314
- it("empty changes is a no-op", () => {
315
- setup("a", "b");
316
- apply([]);
317
- expect(pretty(state)).toMatchInlineSnapshot(`
318
- "
319
- 0: 'a'
320
- 1: 'b'
321
- "
322
- `);
323
- });
324
-
325
- it("set-code updates the mounted editor view's document", () => {
326
- // Existing tests only check cellData.code — this covers the editor
327
- // view side, so regressions in the reducer's imperative sync (or in
328
- // the CellEditor useEffect that backs it up) don't go unnoticed.
329
- setup('x = "BEFORE"');
330
- const [a] = state.cellIds.inOrderIds;
331
- const editorView = state.cellHandles[a].current?.editorViewOrNull;
332
- expect(editorView?.state.doc.toString()).toBe('x = "BEFORE"');
333
-
334
- apply([{ type: "set-code", cellId: a, code: 'x = "AFTER"' }]);
335
-
336
- expect(state.cellData[a].code).toBe('x = "AFTER"');
337
- expect(editorView?.state.doc.toString()).toBe('x = "AFTER"');
338
- });
339
-
340
- it("create-cell then set-code on same cell updates editor", () => {
341
- // Mirrors the code_mode flow that exposed marimo-pair#27: create_cell
342
- // in one batch, edit_cell in a second batch, each arriving as a
343
- // separate transaction.
344
- setup();
345
- apply([
346
- {
347
- type: "create-cell",
348
- cellId: cellId("repro"),
349
- code: 'x = "BEFORE"',
350
- name: "repro_bug",
351
- config: {},
352
- },
353
- ]);
354
- const editorView =
355
- state.cellHandles[cellId("repro")].current?.editorViewOrNull;
356
- expect(editorView?.state.doc.toString()).toBe('x = "BEFORE"');
357
-
358
- apply([
359
- { type: "set-code", cellId: cellId("repro"), code: 'x = "AFTER"' },
360
- { type: "reorder-cells", cellIds: [cellId("repro")] },
361
- ]);
362
-
363
- expect(state.cellData[cellId("repro")].code).toBe('x = "AFTER"');
364
- expect(editorView?.state.doc.toString()).toBe('x = "AFTER"');
365
- });
366
- });
367
-
368
- describe("applyTransactionChanges column rebuild", () => {
369
- it("boundary anchors: set-config on column boundaries splits cells into columns", () => {
370
- // The user's exact example. Server sends a reorder + set-config only on
371
- // the cells at column boundaries. The replica must infer that the cells
372
- // in between inherit the column of the preceding anchor.
373
- setup("a", "b", "c", "d");
374
- const [a, b, c, d] = state.cellIds.inOrderIds;
375
- apply([
376
- { type: "reorder-cells", cellIds: [a, b, c, d] },
377
- {
378
- type: "set-config",
379
- cellId: a,
380
- column: 0,
381
- disabled: false,
382
- hideCode: false,
383
- expandOutput: false,
384
- },
385
- {
386
- type: "set-config",
387
- cellId: c,
388
- column: 1,
389
- disabled: false,
390
- hideCode: false,
391
- expandOutput: false,
392
- },
393
- ]);
394
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
395
- "
396
- col0: [0, 1]
397
- col1: [2, 3]
398
- "
399
- `);
400
- });
401
-
402
- it("boundary anchors on reordered cells: order follows reorder-cells", () => {
403
- // Start from a single column, reorder the cells and split at c.
404
- // The rebuild must use the new flat order from reorder-cells so that
405
- // b ends up next to a (col 0) and d ends up next to c (col 1).
406
- setup("a", "b", "c", "d");
407
- const [a, b, c, d] = state.cellIds.inOrderIds;
408
- apply([
409
- { type: "reorder-cells", cellIds: [d, a, c, b] },
410
- {
411
- type: "set-config",
412
- cellId: d,
413
- column: 0,
414
- disabled: false,
415
- hideCode: false,
416
- expandOutput: false,
417
- },
418
- {
419
- type: "set-config",
420
- cellId: c,
421
- column: 1,
422
- disabled: false,
423
- hideCode: false,
424
- expandOutput: false,
425
- },
426
- ]);
427
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
428
- "
429
- col0: [3, 0]
430
- col1: [2, 1]
431
- "
432
- `);
433
- });
434
-
435
- it("three columns: inherits column through consecutive cells", () => {
436
- setup("a", "b", "c", "d", "e", "f");
437
- const [a, b, c, d, e, f] = state.cellIds.inOrderIds;
438
- apply([
439
- { type: "reorder-cells", cellIds: [a, b, c, d, e, f] },
440
- {
441
- type: "set-config",
442
- cellId: a,
443
- column: 0,
444
- disabled: false,
445
- hideCode: false,
446
- expandOutput: false,
447
- },
448
- {
449
- type: "set-config",
450
- cellId: c,
451
- column: 1,
452
- disabled: false,
453
- hideCode: false,
454
- expandOutput: false,
455
- },
456
- {
457
- type: "set-config",
458
- cellId: e,
459
- column: 2,
460
- disabled: false,
461
- hideCode: false,
462
- expandOutput: false,
463
- },
464
- ]);
465
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
466
- "
467
- col0: [0, 1]
468
- col1: [2, 3]
469
- col2: [4, 5]
470
- "
471
- `);
472
- });
473
-
474
- it("every cell explicitly tagged with a column", () => {
475
- setup("a", "b", "c", "d");
476
- const [a, b, c, d] = state.cellIds.inOrderIds;
477
- apply([
478
- { type: "reorder-cells", cellIds: [a, b, c, d] },
479
- {
480
- type: "set-config",
481
- cellId: a,
482
- column: 0,
483
- disabled: false,
484
- hideCode: false,
485
- expandOutput: false,
486
- },
487
- {
488
- type: "set-config",
489
- cellId: b,
490
- column: 1,
491
- disabled: false,
492
- hideCode: false,
493
- expandOutput: false,
494
- },
495
- {
496
- type: "set-config",
497
- cellId: c,
498
- column: 0,
499
- disabled: false,
500
- hideCode: false,
501
- expandOutput: false,
502
- },
503
- {
504
- type: "set-config",
505
- cellId: d,
506
- column: 1,
507
- disabled: false,
508
- hideCode: false,
509
- expandOutput: false,
510
- },
511
- ]);
512
- // a and c in col0; b and d in col1. Order within each column follows
513
- // the reorder-cells order.
514
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
515
- "
516
- col0: [0, 2]
517
- col1: [1, 3]
518
- "
519
- `);
520
- });
521
-
522
- it("set-config without reorder-cells moves the cell to the new column", () => {
523
- setup("a", "b", "c");
524
- const [, b] = state.cellIds.inOrderIds;
525
- apply([
526
- {
527
- type: "set-config",
528
- cellId: b,
529
- column: 1,
530
- disabled: false,
531
- hideCode: false,
532
- expandOutput: false,
533
- },
534
- ]);
535
- // Without reorder-cells, the flat order comes from the current tree.
536
- // b gets explicit col=1; a and c stay with default null → follow the
537
- // previous cell's column (a → col0 because prev=0; c → col1 because
538
- // prev was just set to 1 by b).
539
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
540
- "
541
- col0: [0]
542
- col1: [1, 2]
543
- "
544
- `);
545
- });
546
-
547
- it("no column change: set-config only touching other fields does not repartition", () => {
548
- setup("a", "b", "c");
549
- const [, b] = state.cellIds.inOrderIds;
550
- apply([
551
- {
552
- type: "set-config",
553
- cellId: b,
554
- column: null,
555
- disabled: false,
556
- hideCode: true,
557
- expandOutput: false,
558
- },
559
- ]);
560
- // All three cells remain in a single column. No rebuild triggered.
561
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
562
- "
563
- col0: [0, 1, 2]
564
- "
565
- `);
566
- expect(pretty(state)).toMatchInlineSnapshot(`
567
- "
568
- 0: 'a'
569
- 1: 'b' [hide_code]
570
- 2: 'c'
571
- "
572
- `);
573
- });
574
-
575
- it("no changes: empty transaction leaves column structure alone", () => {
576
- // Setup a multi-column state first
577
- setup("a", "b");
578
- const [a, b] = state.cellIds.inOrderIds;
579
- apply([
580
- { type: "reorder-cells", cellIds: [a, b] },
581
- {
582
- type: "set-config",
583
- cellId: a,
584
- column: 0,
585
- disabled: false,
586
- hideCode: false,
587
- expandOutput: false,
588
- },
589
- {
590
- type: "set-config",
591
- cellId: b,
592
- column: 1,
593
- disabled: false,
594
- hideCode: false,
595
- expandOutput: false,
596
- },
597
- ]);
598
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
599
- "
600
- col0: [0]
601
- col1: [1]
602
- "
603
- `);
604
- // Now apply no changes — column structure should be preserved.
605
- apply([]);
606
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
607
- "
608
- col0: [0]
609
- col1: [1]
610
- "
611
- `);
612
- });
613
-
614
- it("merging columns: set-config col=0 for everything collapses to one column", () => {
615
- // Start in a multi-column state.
616
- setup("a", "b", "c", "d");
617
- const [a, b, c, d] = state.cellIds.inOrderIds;
618
- apply([
619
- { type: "reorder-cells", cellIds: [a, b, c, d] },
620
- {
621
- type: "set-config",
622
- cellId: a,
623
- column: 0,
624
- disabled: false,
625
- hideCode: false,
626
- expandOutput: false,
627
- },
628
- {
629
- type: "set-config",
630
- cellId: c,
631
- column: 1,
632
- disabled: false,
633
- hideCode: false,
634
- expandOutput: false,
635
- },
636
- ]);
637
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
638
- "
639
- col0: [0, 1]
640
- col1: [2, 3]
641
- "
642
- `);
643
- // Now merge everything back to col 0.
644
- apply([
645
- { type: "reorder-cells", cellIds: [a, b, c, d] },
646
- {
647
- type: "set-config",
648
- cellId: c,
649
- column: 0,
650
- disabled: false,
651
- hideCode: false,
652
- expandOutput: false,
653
- },
654
- {
655
- type: "set-config",
656
- cellId: d,
657
- column: 0,
658
- disabled: false,
659
- hideCode: false,
660
- expandOutput: false,
661
- },
662
- ]);
663
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
664
- "
665
- col0: [0, 1, 2, 3]
666
- "
667
- `);
668
- });
669
-
670
- it("create-cell with column places cell in correct column", () => {
671
- setup("a", "b");
672
- const [, b] = state.cellIds.inOrderIds;
673
- apply([
674
- {
675
- type: "create-cell",
676
- cellId: cellId("fresh"),
677
- code: "x",
678
- name: "",
679
- config: { column: 1 },
680
- after: b,
681
- },
682
- ]);
683
- // a and b are in col 0 (unchanged). The new cell is created at the end
684
- // with column=1, so it should land physically in col 1.
685
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
686
- "
687
- col0: [0, 1]
688
- col1: [fresh]
689
- "
690
- `);
691
- expect(pretty(state)).toMatchInlineSnapshot(`
692
- "
693
- 0: 'a'
694
- 1: 'b'
695
- fresh: 'x' [col=1]
696
- "
697
- `);
698
- });
699
-
700
- it("multi-change transaction with column + code + name updates", () => {
701
- setup("a", "b", "c");
702
- const [a, b, c] = state.cellIds.inOrderIds;
703
- apply([
704
- { type: "reorder-cells", cellIds: [a, b, c] },
705
- { type: "set-code", cellId: a, code: "x = 1" },
706
- { type: "set-name", cellId: b, name: "middle" },
707
- {
708
- type: "set-config",
709
- cellId: a,
710
- column: 0,
711
- disabled: false,
712
- hideCode: false,
713
- expandOutput: false,
714
- },
715
- {
716
- type: "set-config",
717
- cellId: b,
718
- column: 1,
719
- disabled: false,
720
- hideCode: false,
721
- expandOutput: false,
722
- },
723
- ]);
724
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
725
- "
726
- col0: [0]
727
- col1: [1, 2]
728
- "
729
- `);
730
- expect(pretty(state)).toMatchInlineSnapshot(`
731
- "
732
- 0: 'x = 1' [col=0]
733
- 1: 'b' [name=middle, col=1]
734
- 2: 'c'
735
- "
736
- `);
737
- });
738
-
739
- it("cancelled create+delete does not trigger column rebuild", () => {
740
- setup("a", "b");
741
- apply([
742
- {
743
- type: "create-cell",
744
- cellId: cellId("ephemeral"),
745
- code: "tmp",
746
- name: "",
747
- config: { column: 1 },
748
- },
749
- { type: "delete-cell", cellId: cellId("ephemeral") },
750
- ]);
751
- // The create+delete cancel out. The column metadata on the cancelled
752
- // create-cell shouldn't cause a spurious column rebuild.
753
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
754
- "
755
- col0: [0, 1]
756
- "
757
- `);
758
- });
759
-
760
- it("boundary-anchor convention: moving an anchor pulls non-anchored followers", () => {
761
- // This documents the boundary-anchor convention used by the server: only
762
- // cells at column boundaries carry an explicit column. Non-anchor cells
763
- // have config.column=null and inherit the column of the previous cell.
764
- // That means moving an anchor also moves its silent followers.
765
- setup("a", "b", "c", "d");
766
- const [a, b, c, d] = state.cellIds.inOrderIds;
767
- // Split into two columns. Only a and c are anchors.
768
- apply([
769
- { type: "reorder-cells", cellIds: [a, b, c, d] },
770
- {
771
- type: "set-config",
772
- cellId: a,
773
- column: 0,
774
- disabled: false,
775
- hideCode: false,
776
- expandOutput: false,
777
- },
778
- {
779
- type: "set-config",
780
- cellId: c,
781
- column: 1,
782
- disabled: false,
783
- hideCode: false,
784
- expandOutput: false,
785
- },
786
- ]);
787
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
788
- "
789
- col0: [0, 1]
790
- col1: [2, 3]
791
- "
792
- `);
793
- // Move the c anchor to col 0. d has no explicit column so it follows c.
794
- apply([
795
- { type: "reorder-cells", cellIds: [a, b, c, d] },
796
- {
797
- type: "set-config",
798
- cellId: c,
799
- column: 0,
800
- disabled: false,
801
- hideCode: false,
802
- expandOutput: false,
803
- },
804
- ]);
805
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
806
- "
807
- col0: [0, 1, 2, 3]
808
- "
809
- `);
810
- });
811
-
812
- it("explicit anchors on followers: move anchor leaves explicitly-tagged follower behind", () => {
813
- // Contrast with the previous test: if d is explicitly tagged col=1,
814
- // moving c back to col 0 should leave d in col 1.
815
- setup("a", "b", "c", "d");
816
- const [a, b, c, d] = state.cellIds.inOrderIds;
817
- apply([
818
- { type: "reorder-cells", cellIds: [a, b, c, d] },
819
- {
820
- type: "set-config",
821
- cellId: a,
822
- column: 0,
823
- disabled: false,
824
- hideCode: false,
825
- expandOutput: false,
826
- },
827
- {
828
- type: "set-config",
829
- cellId: c,
830
- column: 1,
831
- disabled: false,
832
- hideCode: false,
833
- expandOutput: false,
834
- },
835
- {
836
- type: "set-config",
837
- cellId: d,
838
- column: 1,
839
- disabled: false,
840
- hideCode: false,
841
- expandOutput: false,
842
- },
843
- ]);
844
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
845
- "
846
- col0: [0, 1]
847
- col1: [2, 3]
848
- "
849
- `);
850
- // Move c back to col 0. Because d has its own explicit column=1, it
851
- // does NOT follow c.
852
- apply([
853
- { type: "reorder-cells", cellIds: [a, b, c, d] },
854
- {
855
- type: "set-config",
856
- cellId: c,
857
- column: 0,
858
- disabled: false,
859
- hideCode: false,
860
- expandOutput: false,
861
- },
862
- ]);
863
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
864
- "
865
- col0: [0, 1, 2]
866
- col1: [3]
867
- "
868
- `);
869
- });
870
-
871
- it("set-config before reorder-cells: processing is sorted so set-config runs last", () => {
872
- // Defensive test: even if a transaction has set-config *before*
873
- // reorder-cells, the implementation must process reorder-cells first so
874
- // that the column rebuild sees the intended final flat order. This
875
- // matches the order the backend plans transactions in.
876
- setup("a", "b", "c", "d");
877
- const [a, b, c, d] = state.cellIds.inOrderIds;
878
- apply([
879
- // set-config appears FIRST in the transaction.
880
- {
881
- type: "set-config",
882
- cellId: a,
883
- column: 0,
884
- disabled: false,
885
- hideCode: false,
886
- expandOutput: false,
887
- },
888
- {
889
- type: "set-config",
890
- cellId: c,
891
- column: 1,
892
- disabled: false,
893
- hideCode: false,
894
- expandOutput: false,
895
- },
896
- // reorder-cells comes afterwards.
897
- { type: "reorder-cells", cellIds: [a, b, c, d] },
898
- ]);
899
- // Result must be identical to the canonical order:
900
- // the tree is reshaped first, then column metadata is applied.
901
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
902
- "
903
- col0: [0, 1]
904
- col1: [2, 3]
905
- "
906
- `);
907
- });
908
-
909
- it("set-config interleaved between other changes is still run last", () => {
910
- // Another ordering variant: set-config interleaved between set-code and
911
- // reorder-cells. Our sort must move set-config to the end regardless of
912
- // position, while preserving the relative order of the other changes.
913
- setup("a", "b", "c", "d");
914
- const [a, b, c, d] = state.cellIds.inOrderIds;
915
- apply([
916
- {
917
- type: "set-config",
918
- cellId: a,
919
- column: 0,
920
- disabled: false,
921
- hideCode: false,
922
- expandOutput: false,
923
- },
924
- { type: "set-code", cellId: a, code: "x = 1" },
925
- {
926
- type: "set-config",
927
- cellId: c,
928
- column: 1,
929
- disabled: false,
930
- hideCode: false,
931
- expandOutput: false,
932
- },
933
- { type: "reorder-cells", cellIds: [a, b, c, d] },
934
- { type: "set-name", cellId: d, name: "last" },
935
- ]);
936
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
937
- "
938
- col0: [0, 1]
939
- col1: [2, 3]
940
- "
941
- `);
942
- expect(pretty(state)).toMatchInlineSnapshot(`
943
- "
944
- 0: 'x = 1' [col=0]
945
- 1: 'b'
946
- 2: 'c' [col=1]
947
- 3: 'd' [name=last]
948
- "
949
- `);
950
- });
951
-
952
- it("reorder-cells alone (no column changes) preserves existing columns", () => {
953
- // Start in a two-column state.
954
- setup("a", "b", "c", "d");
955
- const [a, b, c, d] = state.cellIds.inOrderIds;
956
- apply([
957
- { type: "reorder-cells", cellIds: [a, b, c, d] },
958
- {
959
- type: "set-config",
960
- cellId: a,
961
- column: 0,
962
- disabled: false,
963
- hideCode: false,
964
- expandOutput: false,
965
- },
966
- {
967
- type: "set-config",
968
- cellId: c,
969
- column: 1,
970
- disabled: false,
971
- hideCode: false,
972
- expandOutput: false,
973
- },
974
- ]);
975
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
976
- "
977
- col0: [0, 1]
978
- col1: [2, 3]
979
- "
980
- `);
981
- // Now reorder without any column changes. The rebuild must NOT fire.
982
- // setCellIds with fromWithPreviousShape preserves the column assignments.
983
- apply([{ type: "reorder-cells", cellIds: [b, a, d, c] }]);
984
- expect(prettyColumns(state)).toMatchInlineSnapshot(`
985
- "
986
- col0: [1, 0]
987
- col1: [3, 2]
988
- "
989
- `);
990
- });
991
- });