@marimo-team/islands 0.25.3-dev173 → 0.25.3-dev175

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 (134) hide show
  1. package/dist/{ErrorBoundary-wT3f3iJ1.js → ErrorBoundary-DNvcAIsP.js} +1 -1
  2. package/dist/{any-language-editor-B92WMzoC.js → any-language-editor-BVFHDDQ3.js} +2 -2
  3. package/dist/{chat-ui-CuORr6fL.js → chat-ui-X-cWrL9u.js} +1222 -1222
  4. package/dist/{code-visibility-ZReaeMaF.js → code-visibility-CH1lmM1h.js} +2 -2
  5. package/dist/{common-DCevHg1m.js → common-DIrS1UR9.js} +1074 -1078
  6. package/dist/{data-grid-overlay-editor-CTC-bJus.js → data-grid-overlay-editor-DFLZvFK0.js} +1 -1
  7. package/dist/{dist-F8Ep36rk.js → dist-B1M0bGO3.js} +1 -1
  8. package/dist/{dist-D6d86eNJ.js → dist-BA13RJj5.js} +1 -1
  9. package/dist/{dist-Bvs24R_E.js → dist-BJtmSgcE.js} +48 -48
  10. package/dist/{dist-CN_BeG2I.js → dist-Ba2VIn1K.js} +53 -53
  11. package/dist/{dist-PXYI_yuP.js → dist-Bj4bmwUm.js} +12 -12
  12. package/dist/{dist-DoE4XJBC.js → dist-BmS-ZGjl.js} +8626 -8626
  13. package/dist/{dist-BorMD-uX.js → dist-Bzogh7CW.js} +87 -87
  14. package/dist/{dist-CN8zI-XC.js → dist-CThPVKaP.js} +1385 -1382
  15. package/dist/{dist-DiDibOYr.js → dist-CTi5FTiV.js} +87 -87
  16. package/dist/{dist-diIx5qJW.js → dist-Ck_C32ye.js} +2 -2
  17. package/dist/{dist-BLfKmR-l.js → dist-D1fnFp1J.js} +1 -1
  18. package/dist/{dist-OR1OnN-y.js → dist-D3x9__Fu.js} +46 -46
  19. package/dist/{dist-BMQf5zIS.js → dist-D7mrh5sO.js} +36 -36
  20. package/dist/{dist-CSJ0MDRw.js → dist-D9Q3xjiP.js} +1 -1
  21. package/dist/{dist-BuaRHxOc.js → dist-DP60Ubh8.js} +2 -2
  22. package/dist/{dist-CSbh1PNP.js → dist-DTv9KkpW.js} +12 -12
  23. package/dist/{dist-D-wwqlzJ.js → dist-DVVwOfrD.js} +109 -109
  24. package/dist/{dist-C8fAKSH1.js → dist-DYXM73-k.js} +36 -36
  25. package/dist/{dist-DBtSl3sf.js → dist-DjeybPOb.js} +1 -1
  26. package/dist/{dist-CrSP_H1M.js → dist-DtGn9N4h.js} +197 -197
  27. package/dist/{dist-CLdpGQVr.js → dist-KN80qWCP.js} +2 -2
  28. package/dist/{dist-Cg7gzw-a.js → dist-O0qFpEJh.js} +56 -56
  29. package/dist/{dist-CkTTIdBe.js → dist-OUle2RT7.js} +207 -207
  30. package/dist/{dist-BK4Oj99o.js → dist-T5rh6JvL.js} +1 -1
  31. package/dist/{error-entries-CmWMEsY_.js → error-entries-mpoD5t79.js} +4939 -4983
  32. package/dist/{esm-D2gTsXEV.js → esm-Bbw_Njjx.js} +575 -575
  33. package/dist/{esm-AMFssoNp.js → esm-mDQNEFe_.js} +354 -354
  34. package/dist/{feedback-button-DwXkmewo.js → feedback-button-DL0DJ-LB.js} +21 -21
  35. package/dist/{glide-data-editor-BlmcP-lw.js → glide-data-editor-n4wnx19S.js} +3 -3
  36. package/dist/{globals-BdRitZ4p.js → globals-NCsYyjpB.js} +1 -1
  37. package/dist/{html-to-image-bpiQh7ND.js → html-to-image-BoZthB4P.js} +454 -445
  38. package/dist/main.js +3587 -3587
  39. package/dist/{number-overlay-editor-DQG-hFHT.js → number-overlay-editor-rlvo4mo3.js} +1 -1
  40. package/dist/{process-output-BzloOI7F.js → process-output-DLS4Mqg_.js} +2 -2
  41. package/dist/{reveal-component-D3r3vSTK.js → reveal-component-BJfmRUzb.js} +3 -3
  42. package/package.json +1 -1
  43. package/src/__mocks__/document.tsx +54 -0
  44. package/src/__mocks__/notebook.ts +2 -1
  45. package/src/__mocks__/requests.ts +0 -2
  46. package/src/components/chat/chat-panel.tsx +3 -3
  47. package/src/components/datasources/__tests__/column-preview.test.tsx +6 -8
  48. package/src/components/datasources/column-preview.tsx +4 -8
  49. package/src/components/datasources/datasources.tsx +4 -3
  50. package/src/components/dependency-graph/minimap-content.tsx +2 -6
  51. package/src/components/dependency-graph/panels.tsx +2 -1
  52. package/src/components/editor/__tests__/Output.test.tsx +50 -32
  53. package/src/components/editor/__tests__/data-attributes.test.tsx +22 -11
  54. package/src/components/editor/__tests__/output-persistence.test.tsx +6 -1
  55. package/src/components/editor/actions/__tests__/name-cell-input.test.tsx +69 -0
  56. package/src/components/editor/actions/name-cell-input.tsx +4 -3
  57. package/src/components/editor/actions/useCellActionButton.tsx +20 -26
  58. package/src/components/editor/actions/useNotebookActions.tsx +13 -18
  59. package/src/components/editor/actions/useSetCodeVisibility.ts +10 -11
  60. package/src/components/editor/actions/useSetOutputsExpanded.ts +13 -18
  61. package/src/components/editor/cell/CreateCellButton.tsx +5 -9
  62. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +3 -1
  63. package/src/components/editor/cell/code/cell-editor.tsx +13 -20
  64. package/src/components/editor/cell/useAddCell.ts +4 -8
  65. package/src/components/editor/cell/useDeleteCell.tsx +10 -36
  66. package/src/components/editor/cell/useRunCells.ts +2 -5
  67. package/src/components/editor/cell/useSplitCell.tsx +2 -1
  68. package/src/components/editor/chrome/panels/logs-panel.tsx +2 -1
  69. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -11
  70. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +3 -1
  71. package/src/components/editor/columns/sortable-column.tsx +1 -1
  72. package/src/components/editor/connections/components.tsx +4 -9
  73. package/src/components/editor/controls/Controls.tsx +1 -1
  74. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +9 -3
  75. package/src/components/editor/documentation.css +34 -11
  76. package/src/components/editor/errors/auto-fix.tsx +4 -8
  77. package/src/components/editor/file-tree/file-explorer.tsx +3 -7
  78. package/src/components/editor/links/__tests__/cell-link.test.tsx +3 -1
  79. package/src/components/editor/links/cell-link.tsx +2 -1
  80. package/src/components/editor/navigation/__tests__/clipboard.test.ts +60 -38
  81. package/src/components/editor/navigation/__tests__/navigation.test.ts +73 -64
  82. package/src/components/editor/navigation/clipboard.ts +12 -11
  83. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +11 -20
  84. package/src/components/editor/navigation/navigation.ts +20 -28
  85. package/src/components/editor/notebook-cell.tsx +7 -10
  86. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +125 -69
  87. package/src/components/editor/renderers/cell-array.tsx +10 -17
  88. package/src/components/markdown/markdown-renderer.tsx +4 -8
  89. package/src/components/pages/edit-page.tsx +32 -17
  90. package/src/components/scratchpad/scratchpad.test.tsx +3 -1
  91. package/src/components/scratchpad/scratchpad.tsx +4 -7
  92. package/src/components/slides/__tests__/minimap-actions.test.tsx +9 -25
  93. package/src/components/slides/minimap.tsx +7 -3
  94. package/src/components/slides/slide-cell-view.tsx +3 -3
  95. package/src/components/sort/SortableCellsProvider.tsx +2 -1
  96. package/src/core/ai/__tests__/staged-cells.test.ts +18 -20
  97. package/src/core/ai/staged-cells.ts +8 -16
  98. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +2 -2
  99. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +2 -2
  100. package/src/core/ai/tools/base.ts +2 -2
  101. package/src/core/ai/tools/edit-notebook-tool.ts +13 -21
  102. package/src/core/cells/__tests__/actions.test.ts +474 -34
  103. package/src/core/cells/__tests__/add-missing-import.test.ts +7 -8
  104. package/src/core/cells/__tests__/cells.test.ts +171 -813
  105. package/src/core/cells/__tests__/document.test.ts +5 -35
  106. package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
  107. package/src/core/cells/__tests__/outputs.test.tsx +71 -53
  108. package/src/core/cells/actions.ts +391 -3
  109. package/src/core/cells/add-missing-import.ts +46 -42
  110. package/src/core/cells/cells.ts +38 -413
  111. package/src/core/cells/document.ts +7 -4
  112. package/src/core/cells/outputs.ts +12 -28
  113. package/src/core/codemirror/cells/extensions.ts +6 -6
  114. package/src/core/codemirror/cells/state.ts +1 -1
  115. package/src/core/codemirror/keymaps/vim.ts +2 -2
  116. package/src/core/edit-app.tsx +19 -27
  117. package/src/core/islands/bridge.ts +0 -2
  118. package/src/core/kernel/handlers.ts +3 -3
  119. package/src/core/network/__tests__/requests-lazy.test.ts +1 -3
  120. package/src/core/network/requests-lazy.ts +0 -2
  121. package/src/core/network/requests-network.ts +0 -16
  122. package/src/core/network/requests-static.ts +0 -2
  123. package/src/core/network/requests-toasting.tsx +0 -2
  124. package/src/core/network/types.ts +0 -7
  125. package/src/core/notebook/document/__tests__/codemirror.test.ts +5 -0
  126. package/src/core/notebook/document/__tests__/draft.test.ts +41 -0
  127. package/src/core/notebook/document/__tests__/state.test.ts +21 -0
  128. package/src/core/notebook/document/draft.ts +35 -5
  129. package/src/core/notebook/document/index.ts +1 -0
  130. package/src/core/notebook/document/state.ts +6 -2
  131. package/src/core/session/useNotebookSession.tsx +6 -2
  132. package/src/core/wasm/bridge.ts +0 -16
  133. package/src/css/globals.css +56 -22
  134. package/src/stories/runtime-story.tsx +22 -15
@@ -1,48 +1,488 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
4
- import { MockNotebook } from "@/__mocks__/notebook";
5
- import { scrollAndHighlightCell } from "@/components/editor/links/cell-link";
6
- import { notebookAtom } from "@/core/cells/cells";
7
- import type { CellId } from "@/core/cells/ids";
8
- import { createCellRuntimeState } from "@/core/cells/types";
9
- import { store } from "@/core/state/jotai";
10
- import { notebookScrollToRunning } from "../actions";
11
-
12
- vi.mock("@/components/editor/links/cell-link", () => ({
13
- scrollAndHighlightCell: vi.fn(),
3
+ import { history } from "@codemirror/commands";
4
+ import { EditorState } from "@codemirror/state";
5
+ import { EditorView } from "@codemirror/view";
6
+ import type * as Protocol from "@marimo-team/client/protocol";
7
+ import { createStore } from "jotai";
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ import { mockDocumentModel } from "@/__mocks__/document";
10
+ import { cellId } from "@/__tests__/branded";
11
+ import {
12
+ cell,
13
+ CONFIG,
14
+ document as documentOf,
15
+ withCells,
16
+ } from "@/core/notebook/document/__tests__/fixtures";
17
+ import type { CellIndex } from "@/utils/id-tree";
18
+ import {
19
+ addSetupCellAtom,
20
+ createCellAtom,
21
+ deleteCellsAtom,
22
+ moveToNextCellAtom,
23
+ undoDeleteCellAtom,
24
+ } from "../actions";
25
+ import {
26
+ cellDataAtom,
27
+ cellHandleAtom,
28
+ exportedForTesting,
29
+ notebookAtom,
30
+ notebookReducer,
31
+ } from "../cells";
32
+ import { followDocument } from "../document";
33
+ import { type CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "../ids";
34
+ import { focusAndScrollCellIntoView } from "../scrollCellIntoView";
35
+
36
+ vi.mock("../scrollCellIntoView", () => ({
37
+ focusAndScrollCellIntoView: vi.fn(),
14
38
  }));
15
39
 
16
- describe("notebookScrollToRunning", () => {
17
- beforeEach(() => {
18
- vi.clearAllMocks();
19
- store.set(notebookAtom, MockNotebook.notebookState({ cellData: {} }));
40
+ const c1 = cellId("c1");
41
+ const c2 = cellId("c2");
42
+ const c3 = cellId("c3");
43
+ const stops: Array<() => void> = [];
44
+
45
+ afterEach(() => {
46
+ for (const stop of stops.splice(0)) {
47
+ stop();
48
+ }
49
+ vi.clearAllMocks();
50
+ });
51
+
52
+ /** A store whose notebook follows a document model, as the session wires it. */
53
+ function setup(cells?: Record<string, Protocol.Cell | null>) {
54
+ const store = createStore();
55
+ store.set(notebookAtom, exportedForTesting.initialNotebookState());
56
+ const { model, stop } = mockDocumentModel(
57
+ store,
58
+ cells ? withCells(documentOf(), cells) : documentOf(),
59
+ );
60
+ const controller = new AbortController();
61
+ stops.push(stop, () => controller.abort());
62
+ followDocument({
63
+ store,
64
+ model,
65
+ reconcile: (document) =>
66
+ store.set(notebookAtom, (state) =>
67
+ notebookReducer(state, {
68
+ type: "reconcileDocument",
69
+ payload: document,
70
+ }),
71
+ ),
72
+ signal: controller.signal,
73
+ });
74
+ return {
75
+ store,
76
+ model,
77
+ order: () => store.get(notebookAtom).cellIds.inOrderIds,
78
+ notebook: () => store.get(notebookAtom),
79
+ cellIn: (id: string) => store.get(model.documentAtom)?.cells.get(id),
80
+ /** Give a cell an editor, as mounting it would. */
81
+ mountEditor: (id: string) => {
82
+ const view = new EditorView({
83
+ state: EditorState.create({
84
+ doc: store.get(model.documentAtom)?.cells.get(id)?.code ?? "",
85
+ extensions: [history()],
86
+ }),
87
+ });
88
+ store.get(notebookAtom).cellHandles[id as never].current = {
89
+ editorView: view,
90
+ editorViewOrNull: view,
91
+ };
92
+ return view;
93
+ },
94
+ /** Apply a reducer action, as `useReducerActions()` does. */
95
+ dispatch: (action: Parameters<typeof notebookReducer>[1]) =>
96
+ store.set(notebookAtom, (state) => notebookReducer(state, action)),
97
+ };
98
+ }
99
+
100
+ describe("createCellAtom", () => {
101
+ it("creates a cell after another and focuses it once it mounts", () => {
102
+ const { store, order, notebook, cellIn } = setup();
103
+
104
+ const id = store.set(createCellAtom, { after: c1, code: "z = 3" });
105
+
106
+ expect(id).not.toBeNull();
107
+ expect(order()).toEqual([c1, id, c2]);
108
+ expect(cellIn(id as string)?.code).toBe("z = 3");
109
+ expect(notebook().scrollKey).toBe(id);
110
+ expect(notebook().cellRuntime[id as never]).toBeDefined();
111
+ });
112
+
113
+ it("appends to the first column by default", () => {
114
+ const { store, order } = setup();
115
+
116
+ const id = store.set(createCellAtom, {});
117
+
118
+ expect(order()).toEqual([c1, c2, id]);
119
+ });
120
+
121
+ it("keeps a focused hidden cell untouched until it is blurred", () => {
122
+ const { store, notebook } = setup();
123
+
124
+ const hidden = store.set(createCellAtom, {
125
+ after: c1,
126
+ config: { hide_code: true },
127
+ });
128
+ const unfocused = store.set(createCellAtom, {
129
+ after: c1,
130
+ config: { hide_code: true },
131
+ focus: false,
132
+ });
133
+
134
+ expect(notebook().untouchedNewCells.has(hidden as never)).toBe(true);
135
+ expect(notebook().untouchedNewCells.has(unfocused as never)).toBe(false);
136
+ expect(notebook().scrollKey).toBe(hidden);
137
+ });
138
+
139
+ it("names the column of a cell placed first in a later column", () => {
140
+ const { store, notebook, cellIn } = setup({
141
+ c2: cell("c2", "y = 2", { config: { ...CONFIG, column: 1 } }),
142
+ });
143
+
144
+ const id = store.set(createCellAtom, { before: c2 });
145
+
146
+ expect(cellIn(id as string)?.config.column).toBe(1);
147
+ expect(cellIn(c2)?.config.column).toBeNull();
148
+ expect(notebook().cellIds.atOrThrow(1).inOrderIds).toEqual([id, c2]);
149
+ expect(notebook().cellIds.atOrThrow(0).inOrderIds).toEqual([c1]);
150
+ });
151
+
152
+ it("places a cell in the column of its anchor, whatever config it carries", () => {
153
+ const { store, order, notebook, cellIn } = setup({
154
+ c2: cell("c2", "y = 2", { config: { ...CONFIG, column: 1 } }),
155
+ });
156
+
157
+ // A pasted cell carries the config of the cell it was copied from.
158
+ const id = store.set(createCellAtom, {
159
+ after: c2,
160
+ code: "z = 3",
161
+ config: { hide_code: true },
162
+ });
163
+
164
+ expect(order()).toEqual([c1, c2, id]);
165
+ expect(cellIn(id as string)?.config).toMatchObject({
166
+ hide_code: true,
167
+ column: null,
168
+ });
169
+ expect(notebook().cellIds.atOrThrow(1).inOrderIds).toEqual([c2, id]);
170
+ });
171
+
172
+ it("names the column when appending to an empty column", () => {
173
+ const { store, notebook, cellIn } = setup();
174
+ store.set(notebookAtom, (state) => ({
175
+ ...state,
176
+ cellIds: state.cellIds.addColumn(state.cellIds.atOrThrow(0).id),
177
+ }));
178
+ const empty = notebook().cellIds.atOrThrow(1).id;
179
+
180
+ const id = store.set(createCellAtom, { endOf: empty });
181
+
182
+ expect(cellIn(id as string)?.config.column).toBe(1);
183
+ expect(notebook().cellIds.atOrThrow(1).inOrderIds).toEqual([id]);
184
+ });
185
+
186
+ it("creates nothing when a cell already has the code", () => {
187
+ const { store, order } = setup();
188
+
189
+ const id = store.set(createCellAtom, {
190
+ code: "x = 1",
191
+ skipIfCodeExists: true,
192
+ });
193
+
194
+ expect(id).toBeNull();
195
+ expect(order()).toEqual([c1, c2]);
196
+ });
197
+
198
+ it("refuses without a document", () => {
199
+ const store = createStore();
200
+ store.set(notebookAtom, exportedForTesting.initialNotebookState());
201
+
202
+ expect(() => store.set(createCellAtom, {})).toThrow("no document");
203
+ });
204
+ });
205
+
206
+ describe("moveToNextCellAtom", () => {
207
+ it("focuses the neighbouring cell within the column", () => {
208
+ const { store, order } = setup();
209
+
210
+ store.set(moveToNextCellAtom, { cellId: c1, before: false });
211
+
212
+ expect(focusAndScrollCellIntoView).toHaveBeenCalledWith(
213
+ expect.objectContaining({ cellId: c2, codeFocus: "top" }),
214
+ );
215
+ expect(order()).toEqual([c1, c2]);
216
+ });
217
+
218
+ it("creates a cell past the edge of the column, unless told not to", () => {
219
+ const { store, order } = setup();
220
+
221
+ store.set(moveToNextCellAtom, { cellId: c2, before: false, create: false });
222
+ expect(order()).toEqual([c1, c2]);
223
+
224
+ store.set(moveToNextCellAtom, { cellId: c2, before: false });
225
+ expect(order()).toHaveLength(3);
226
+ expect(order().slice(0, 2)).toEqual([c1, c2]);
227
+ });
228
+
229
+ it("creates a cell before the first one", () => {
230
+ const { store, order } = setup();
231
+
232
+ store.set(moveToNextCellAtom, { cellId: c1, before: true });
233
+
234
+ expect(order()).toHaveLength(3);
235
+ expect(order().slice(1)).toEqual([c1, c2]);
236
+ });
237
+
238
+ it("leaves the scratchpad's focus alone", () => {
239
+ const { store, order } = setup();
240
+
241
+ store.set(moveToNextCellAtom, { cellId: SCRATCH_CELL_ID, before: false });
242
+
243
+ expect(focusAndScrollCellIntoView).not.toHaveBeenCalled();
244
+ expect(order()).toEqual([c1, c2]);
20
245
  });
246
+ });
21
247
 
22
- it("scrolls to the first running cell in notebook order", () => {
23
- const runtimeOnlyCellId = "runtime-only" as CellId;
24
- const idleCellId = "idle-cell" as CellId;
25
- const runningCellId = "running-cell" as CellId;
248
+ describe("addSetupCellAtom", () => {
249
+ it("creates the setup cell at the top and focuses it", () => {
250
+ const { store, order, notebook, cellIn } = setup();
251
+
252
+ store.set(addSetupCellAtom, { code: "import marimo as mo" });
253
+
254
+ expect(order()[0]).toBe(SETUP_CELL_ID);
255
+ expect(cellIn(SETUP_CELL_ID)).toMatchObject({
256
+ name: "setup",
257
+ code: "import marimo as mo",
258
+ });
259
+ expect(notebook().scrollKey).toBe(SETUP_CELL_ID);
260
+ });
26
261
 
27
- const notebook = MockNotebook.notebookState({
28
- cellData: {
29
- [idleCellId]: {},
30
- [runningCellId]: {},
262
+ it("places the setup cell in an empty first column", () => {
263
+ const { store, notebook, cellIn } = setup({
264
+ c1: cell(c1, "x = 1", { config: { ...CONFIG, column: 1 } }),
265
+ });
266
+ expect(notebook().cellIds.atOrThrow(0).inOrderIds).toEqual([]);
267
+
268
+ store.set(addSetupCellAtom, { code: "import marimo as mo" });
269
+
270
+ expect(notebook().cellIds.atOrThrow(0).inOrderIds).toEqual([SETUP_CELL_ID]);
271
+ expect(cellIn(SETUP_CELL_ID)?.config.column).toBe(0);
272
+ expect(notebook().scrollKey).toBe(SETUP_CELL_ID);
273
+ });
274
+
275
+ it("only focuses a setup cell that exists", () => {
276
+ const { store, order, cellIn } = setup();
277
+ store.set(addSetupCellAtom, { code: "first" });
278
+
279
+ store.set(addSetupCellAtom, { code: "second" });
280
+
281
+ expect(order().filter((id) => id === SETUP_CELL_ID)).toHaveLength(1);
282
+ expect(cellIn(SETUP_CELL_ID)?.code).toBe("first");
283
+ });
284
+ });
285
+
286
+ describe("deleteCellsAtom", () => {
287
+ it("deletes a cell, focuses the one above, and remembers it", () => {
288
+ const { store, order, notebook } = setup();
289
+
290
+ expect(store.set(deleteCellsAtom, { cellIds: [c2] })).toBe(true);
291
+
292
+ expect(order()).toEqual([c1]);
293
+ expect(notebook().cellData[c2]).toBeUndefined();
294
+ expect(notebook().scrollKey).toBe(c1);
295
+ expect(notebook().history).toEqual([
296
+ {
297
+ type: "delete",
298
+ cells: [
299
+ {
300
+ cell: { id: c2, code: "y = 2", name: "_", config: CONFIG },
301
+ placement: { after: c1 },
302
+ serializedEditorState: null,
303
+ },
304
+ ],
31
305
  },
32
- cellRuntime: {
33
- [idleCellId]: { status: "idle" },
34
- [runningCellId]: { status: "running" },
306
+ ]);
307
+ });
308
+
309
+ it("focuses the cell below when the first cell goes", () => {
310
+ const { store, notebook } = setup();
311
+
312
+ store.set(deleteCellsAtom, { cellIds: [c1] });
313
+
314
+ expect(notebook().scrollKey).toBe(c2);
315
+ expect(notebook().history[0]).toMatchObject({
316
+ cells: [{ placement: { before: c2 } }],
317
+ });
318
+ });
319
+
320
+ it("keeps the last cell", () => {
321
+ const { store, order, notebook } = setup();
322
+
323
+ expect(store.set(deleteCellsAtom, { cellIds: [c1, c2] })).toBe(true);
324
+ expect(order()).toEqual([c2]);
325
+
326
+ expect(store.set(deleteCellsAtom, { cellIds: [c2] })).toBe(false);
327
+ expect(order()).toEqual([c2]);
328
+ expect(notebook().history).toHaveLength(1);
329
+ });
330
+
331
+ it("skips cells the document does not have", () => {
332
+ const { store, order } = setup();
333
+
334
+ expect(store.set(deleteCellsAtom, { cellIds: [cellId("gone")] })).toBe(
335
+ false,
336
+ );
337
+
338
+ expect(order()).toEqual([c1, c2]);
339
+ });
340
+
341
+ it("names the column of a cell that followed a deleted column start", () => {
342
+ const { store, notebook, cellIn } = setup({
343
+ c2: cell("c2", "y = 2", { config: { ...CONFIG, column: 1 } }),
344
+ c3: cell("c3", "z = 3"),
345
+ });
346
+ expect(notebook().cellIds.atOrThrow(1).inOrderIds).toEqual([c2, c3]);
347
+
348
+ store.set(deleteCellsAtom, { cellIds: [c2] });
349
+
350
+ expect(cellIn(c3)?.config.column).toBe(1);
351
+ expect(notebook().cellIds.atOrThrow(1).inOrderIds).toEqual([c3]);
352
+ });
353
+
354
+ it("forgets the untouched state and the cell atoms of a deleted cell", () => {
355
+ const { store, notebook } = setup();
356
+ const id = store.set(createCellAtom, {
357
+ after: c1,
358
+ config: { hide_code: true },
359
+ }) as CellId;
360
+ const dataAtom = cellDataAtom(id);
361
+ const handleAtom = cellHandleAtom(id);
362
+
363
+ store.set(deleteCellsAtom, { cellIds: [id] });
364
+
365
+ expect(notebook().untouchedNewCells.has(id)).toBe(false);
366
+ expect(cellDataAtom(id)).not.toBe(dataAtom);
367
+ expect(cellHandleAtom(id)).not.toBe(handleAtom);
368
+ });
369
+ });
370
+
371
+ describe("undoDeleteCellAtom", () => {
372
+ it("brings a deleted cell back where it was, with its editor history", () => {
373
+ const { store, order, notebook, cellIn, mountEditor } = setup({
374
+ c3: cell("c3", "z = 3"),
375
+ });
376
+ const view = mountEditor(c2);
377
+ view.dispatch({ changes: { from: 0, insert: "# " } });
378
+ store.set(deleteCellsAtom, { cellIds: [c2] });
379
+ expect(order()).toEqual([c1, c3]);
380
+
381
+ store.set(undoDeleteCellAtom);
382
+
383
+ expect(order()).toEqual([c1, c2, c3]);
384
+ expect(cellIn(c2)?.code).toBe("y = 2");
385
+ expect(notebook().cellData[c2].serializedEditorState).toMatchObject({
386
+ doc: "y = 2",
387
+ });
388
+ expect(notebook().cellData[c2].edited).toBe(true);
389
+ expect(notebook().scrollKey).toBe(c2);
390
+ expect(notebook().history).toEqual([]);
391
+ });
392
+
393
+ it("restores the config and the id of the setup cell", () => {
394
+ const { store, order, cellIn } = setup({
395
+ [SETUP_CELL_ID]: cell(SETUP_CELL_ID, "import marimo as mo", {
396
+ name: "setup",
397
+ config: { ...CONFIG, hide_code: true, disabled: true },
398
+ }),
399
+ });
400
+ store.set(deleteCellsAtom, { cellIds: [SETUP_CELL_ID] });
401
+ expect(order()).toEqual([c1, c2]);
402
+
403
+ store.set(undoDeleteCellAtom);
404
+
405
+ expect(order()).toEqual([c1, c2, SETUP_CELL_ID]);
406
+ expect(cellIn(SETUP_CELL_ID)).toMatchObject({
407
+ name: "setup",
408
+ code: "import marimo as mo",
409
+ config: { hide_code: true, disabled: true },
410
+ });
411
+ });
412
+
413
+ it("restores every cell of one deletion at once, in order", () => {
414
+ const { store, order, notebook } = setup({ c3: cell("c3", "z = 3") });
415
+ store.set(deleteCellsAtom, { cellIds: [c1, c2] });
416
+ expect(order()).toEqual([c3]);
417
+ expect(notebook().history).toHaveLength(1);
418
+
419
+ store.set(undoDeleteCellAtom);
420
+
421
+ expect(order()).toEqual([c1, c2, c3]);
422
+ expect(notebook().history).toEqual([]);
423
+ });
424
+
425
+ it("undoes a move recorded in the history", () => {
426
+ const { store, model, order, notebook, dispatch } = setup({
427
+ c3: cell("c3", "z = 3"),
428
+ });
429
+ const column = notebook().cellIds.atOrThrow(0);
430
+ dispatch({
431
+ type: "moveCellsRelativeTo",
432
+ payload: {
433
+ cellIds: [c1, c2],
434
+ targetCellId: c3,
435
+ position: "after",
436
+ previousPlacements: [
437
+ { columnId: column.id, index: 0 as CellIndex },
438
+ { columnId: column.id, index: 1 as CellIndex },
439
+ ],
35
440
  },
36
441
  });
37
- notebook.cellRuntime = {
38
- [runtimeOnlyCellId]: createCellRuntimeState({ status: "running" }),
39
- ...notebook.cellRuntime,
40
- };
41
- store.set(notebookAtom, notebook);
442
+ expect(order()).toEqual([c3, c1, c2]);
443
+ // The middleware would write the order to the document.
444
+ store.set(model.editAtom, (tx) => tx.setOrder([c3, c1, c2]));
445
+
446
+ store.set(deleteCellsAtom, { cellIds: [c3] });
447
+ expect(order()).toEqual([c1, c2]);
448
+
449
+ store.set(undoDeleteCellAtom);
450
+ expect(order()).toEqual([c3, c1, c2]);
451
+
452
+ store.set(undoDeleteCellAtom);
453
+ expect(order()).toEqual([c1, c2, c3]);
454
+ expect(notebook().history).toEqual([]);
455
+ });
456
+
457
+ it("appends a restored cell whose anchor another client removed", () => {
458
+ const { store, order, notebook } = setup();
459
+ store.set(notebookAtom, (state) => ({
460
+ ...state,
461
+ history: [
462
+ {
463
+ type: "delete",
464
+ cells: [
465
+ {
466
+ cell: { id: "c9", code: "q = 9" },
467
+ placement: { after: cellId("gone") },
468
+ serializedEditorState: null,
469
+ },
470
+ ],
471
+ },
472
+ ],
473
+ }));
474
+
475
+ store.set(undoDeleteCellAtom);
476
+
477
+ expect(order()).toEqual([c1, c2, "c9"]);
478
+ expect(notebook().history).toEqual([]);
479
+ });
480
+
481
+ it("does nothing without history", () => {
482
+ const { store, order } = setup();
42
483
 
43
- notebookScrollToRunning();
484
+ store.set(undoDeleteCellAtom);
44
485
 
45
- expect(scrollAndHighlightCell).toHaveBeenCalledOnce();
46
- expect(scrollAndHighlightCell).toHaveBeenCalledWith(runningCellId, "focus");
486
+ expect(order()).toEqual([c1, c2]);
47
487
  });
48
488
  });
@@ -107,35 +107,34 @@ describe("maybeAddMissingImport", () => {
107
107
  }),
108
108
  );
109
109
 
110
- const createNewCell = vi.fn();
110
+ const createCell = vi.fn();
111
111
  const result = addImport({
112
112
  autoInstantiate: false,
113
- createNewCell,
113
+ createCell,
114
114
  fromCellId: Cell1,
115
115
  before: false,
116
116
  });
117
117
 
118
118
  // Should not create a new cell since the import already exists
119
- expect(createNewCell).not.toHaveBeenCalled();
119
+ expect(createCell).not.toHaveBeenCalled();
120
120
  expect(result).toBeNull();
121
121
  }
122
122
  });
123
123
 
124
124
  it("should create a marimo import before the source cell when requested", () => {
125
- const createNewCell = vi.fn();
125
+ const createCell = vi.fn();
126
126
 
127
127
  const result = maybeAddMarimoImport({
128
128
  autoInstantiate: false,
129
- createNewCell,
129
+ createCell,
130
130
  fromCellId: Cell2,
131
131
  before: true,
132
132
  });
133
133
 
134
134
  expect(result).not.toBeNull();
135
- expect(createNewCell).toHaveBeenCalledWith(
135
+ expect(createCell).toHaveBeenCalledWith(
136
136
  expect.objectContaining({
137
- cellId: Cell2,
138
- before: true,
137
+ before: Cell2,
139
138
  code: "import marimo as mo",
140
139
  }),
141
140
  );