@marimo-team/islands 0.25.3-dev191 → 0.25.3-dev193

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 (154) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BBbOElk3.js → ConnectedDataExplorerComponent-BRuwLOIx.js} +9 -9
  2. package/dist/{ErrorBoundary--X0z83Yx.js → ErrorBoundary-BVu_xT-p.js} +2 -2
  3. package/dist/{abnfDiagram-VCTEODGH-DhMT6Uhd.js → abnfDiagram-VCTEODGH-LlJTOOU_.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-BVbEiJLq.js → architectureDiagram-5GKGNRK7-9PQtGLLZ.js} +1 -1
  5. package/dist/{arrays-R6JsImiT.js → arrays-DA6KkLfq.js} +6 -12
  6. package/dist/assets/__vite-browser-external-BCJsx8QS.js +1 -0
  7. package/dist/assets/{worker-C6e9ERyd.js → worker-DodjWT_Z.js} +2 -2
  8. package/dist/{capabilities-CI4xUPsp.js → capabilities-CvWU-ey_.js} +1 -1
  9. package/dist/{chat-ui-BguxTQEL.js → chat-ui-Bl2euMBD.js} +93 -93
  10. package/dist/{chunk-IMKFNOWR-By5hSbJJ.js → chunk-IMKFNOWR-DcXv-BD5.js} +1 -1
  11. package/dist/{chunk-SHT3W25Y-DFrEYG54.js → chunk-SHT3W25Y-VwnELN23.js} +1 -1
  12. package/dist/{chunk-SVP7TREG-DFXGc9ew.js → chunk-SVP7TREG-DgQpI5Zi.js} +1 -1
  13. package/dist/{chunk-TICWLB2K-DZdNIpoZ.js → chunk-TICWLB2K-CTg6VTJT.js} +1 -1
  14. package/dist/{classDiagram-ZZMXUADV-Cgj_eEQA.js → classDiagram-ZZMXUADV-UzmcFrnW.js} +2 -2
  15. package/dist/{classDiagram-v2-VYDZK3BY-DLAUrSqj.js → classDiagram-v2-VYDZK3BY-DhjiI6IQ.js} +2 -2
  16. package/dist/{code-visibility-uDyLo9_i.js → code-visibility-BTVimCUw.js} +38 -42
  17. package/dist/{common-DWKY45Md.js → common-fTgq7gO-.js} +849 -849
  18. package/dist/{cynefinDiagram-5FMLGOSQ-Cikf9AKa.js → cynefinDiagram-5FMLGOSQ-O4ShIbJX.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-C5KEi5_l.js → dagre-GXQ25YYZ-dtHhhcPu.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-CRnYWD1L.js → data-grid-overlay-editor-DAZZkLWg.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-BaoBTQjR.js → diagram-S7CK7UJ4-ppPYVZ6W.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-DDufBsUA.js → diagram-UQ7AKVKN-CMZDf--k.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-CY6RALJ8.js → diagram-VX7I27RA-CFvJdx-C.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-Bc-LHsD6.js → diagram-Z3DM3KII-Dt6_uFbv.js} +1 -1
  25. package/dist/{dropdown-menu-drDjxJyV.js → dropdown-menu-CeaOsjWZ.js} +2 -2
  26. package/dist/{ebnfDiagram-PWID7BFC-Cz0J-tcC.js → ebnfDiagram-PWID7BFC-DpRFrKK-.js} +2 -2
  27. package/dist/{erDiagram-RLTQ6QDP-DTXTDgFu.js → erDiagram-RLTQ6QDP-CqAU4b0u.js} +1 -1
  28. package/dist/{error-entries-CZ1NQ6Kv.js → error-entries-CYGwr5-O.js} +5596 -6012
  29. package/dist/feedback-button-DzFFn-n4.js +374 -0
  30. package/dist/{flowDiagram-HODETNUW-RbwWf2zc.js → flowDiagram-HODETNUW-i8-Gw8HO.js} +2 -2
  31. package/dist/{formats-DbXnijtH.js → formats-BtoHHy42.js} +1 -1
  32. package/dist/{glide-data-editor-CvRiA49N.js → glide-data-editor-DFsqVbWR.js} +5 -5
  33. package/dist/{globals-BR4CmGOS.js → globals-Dr0MRANu.js} +2 -2
  34. package/dist/{html-to-image-D_4_hQ8U.js → html-to-image-Cy5UDpQJ.js} +56 -56
  35. package/dist/{infoDiagram-27XIBGKW-DlmzZjmI.js → infoDiagram-27XIBGKW-DpfOKdLL.js} +1 -1
  36. package/dist/{ishikawaDiagram-5VMMS53U-EN3TYP_q.js → ishikawaDiagram-5VMMS53U-Dg7mrMG7.js} +1 -1
  37. package/dist/{kanban-definition-UXKFOSKX-CK_-Ru7Q.js → kanban-definition-UXKFOSKX-QB6e3Jrj.js} +1 -1
  38. package/dist/main.js +5875 -5876
  39. package/dist/{mermaid-DU9ZRmkG.js → mermaid-CeWG4Egr.js} +48 -48
  40. package/dist/{mindmap-definition-YA3MSWOX-C-hqgUvF.js → mindmap-definition-YA3MSWOX-BTz3uTGe.js} +1 -1
  41. package/dist/{number-overlay-editor-DiYGbxNa.js → number-overlay-editor-DupOSYHx.js} +1 -1
  42. package/dist/{once-eG8Vmvjd.js → once-DWGO2mQ2.js} +1 -1
  43. package/dist/{pegDiagram-XKGWAZYB-BiMejYPB.js → pegDiagram-XKGWAZYB-3ZIEa3mB.js} +2 -2
  44. package/dist/{pieDiagram-E7YTZNPT-B1zCZ1eq.js → pieDiagram-E7YTZNPT-BJAlk046.js} +1 -1
  45. package/dist/{process-output-DxSCHBWp.js → process-output-q9MTAIMX.js} +2 -2
  46. package/dist/{railroadDiagram-O6MQD6OU-BLcfaASl.js → railroadDiagram-O6MQD6OU-Dn3bsSRa.js} +2 -2
  47. package/dist/{requirementDiagram-BXWQKSXE-DtBOf52-.js → requirementDiagram-BXWQKSXE-wcOmQQCL.js} +1 -1
  48. package/dist/{reveal-component-D3DsXPIm.js → reveal-component-Bg8rP_Uo.js} +17 -17
  49. package/dist/{sequenceDiagram-WJ2MYXX4-DIwF19_n.js → sequenceDiagram-WJ2MYXX4-DSh_qxoF.js} +1 -1
  50. package/dist/{spec-DZDnnlgT.js → spec-C7Fos-If.js} +1 -1
  51. package/dist/{stateDiagram-D77RDMKH-sWk1AdYL.js → stateDiagram-D77RDMKH-DH_H8NMH.js} +2 -2
  52. package/dist/{stateDiagram-v2-MP3YSRHH-CiNo-OI7.js → stateDiagram-v2-MP3YSRHH-D_JM7MEK.js} +2 -2
  53. package/dist/style.css +1 -1
  54. package/dist/{swimlanes-42K2YHIH-Bj3hiOY5.js → swimlanes-42K2YHIH-CfoTJO-9.js} +1 -1
  55. package/dist/{swimlanesDiagram-VR7AAH4N-BzEo2r_N.js → swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js} +2 -2
  56. package/dist/{swiper-component-BkCm-kLg.js → swiper-component-D2geL1Ku.js} +1 -1
  57. package/dist/{timeline-definition-24CTP7MA-CrPQk42H.js → timeline-definition-24CTP7MA-BFtdScaL.js} +1 -1
  58. package/dist/{toDate-Cu2pIZcC.js → toDate-CQAFSwos.js} +24 -24
  59. package/dist/{useAsyncData-DEhLdRIu.js → useAsyncData-BeyZyjso.js} +1 -1
  60. package/dist/{useDeepCompareMemoize-RjZGVhBV.js → useDeepCompareMemoize-DOQgae0W.js} +1 -1
  61. package/dist/{useIframeCapabilities-CSdWn6NP.js → useIframeCapabilities-DNvaj9wM.js} +1 -1
  62. package/dist/{useLifecycle-DtcB2s1z.js → useLifecycle-n2lnCFS8.js} +1 -1
  63. package/dist/{useTheme-CoEtr4mK.js → useTheme-CY777LXN.js} +303 -99
  64. package/dist/{vega-component-BhjTrP9-.js → vega-component-0nQzA-I8.js} +6 -6
  65. package/dist/{vennDiagram-4TSXK5OY-BaYMoIpj.js → vennDiagram-4TSXK5OY-BXY8IGCA.js} +1 -1
  66. package/dist/{wardleyDiagram-VM6X3IG4-DjHeuj8p.js → wardleyDiagram-VM6X3IG4-D0tFe6eC.js} +1 -1
  67. package/dist/{xychartDiagram-S5SC5T6Z-Bc_hcuXS.js → xychartDiagram-S5SC5T6Z-Dx8wFM-y.js} +1 -1
  68. package/package.json +1 -1
  69. package/src/__mocks__/notebook.ts +0 -1
  70. package/src/__mocks__/partial.ts +26 -0
  71. package/src/__tests__/main.test.tsx +24 -1
  72. package/src/components/app-config/app-config-form.tsx +5 -9
  73. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -1
  74. package/src/components/editor/__tests__/output-persistence.test.tsx +0 -1
  75. package/src/components/editor/actions/useCellActionButton.tsx +6 -5
  76. package/src/components/editor/actions/useConfigActions.tsx +3 -4
  77. package/src/components/editor/actions/useNotebookActions.tsx +5 -4
  78. package/src/components/editor/cell/code/cell-editor.tsx +13 -6
  79. package/src/components/editor/cell/collapse.tsx +6 -1
  80. package/src/components/editor/chrome/components/feedback-button.tsx +1 -7
  81. package/src/components/editor/controls/Controls.tsx +1 -1
  82. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +8 -6
  83. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -15
  84. package/src/components/editor/navigation/__tests__/selection.test.ts +44 -21
  85. package/src/components/editor/navigation/focus-utils.ts +19 -0
  86. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +6 -13
  87. package/src/components/editor/navigation/navigation.ts +52 -43
  88. package/src/components/editor/navigation/selection.ts +30 -29
  89. package/src/components/editor/notebook-cell.tsx +10 -18
  90. package/src/components/editor/renderers/cell-array.tsx +6 -5
  91. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  92. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  93. package/src/components/pages/edit-page.tsx +0 -13
  94. package/src/components/scratchpad/scratchpad.tsx +0 -1
  95. package/src/components/slides/reveal-component.tsx +1 -1
  96. package/src/components/slides/slide-cell-view.tsx +0 -1
  97. package/src/components/sort/SortableCellsProvider.tsx +6 -5
  98. package/src/core/MarimoApp.tsx +1 -1
  99. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -37
  100. package/src/core/cells/__tests__/actions.test.ts +190 -80
  101. package/src/core/cells/__tests__/arrange.test.ts +263 -0
  102. package/src/core/cells/__tests__/cells.test.ts +11 -1101
  103. package/src/core/cells/__tests__/document.test.ts +3 -32
  104. package/src/core/cells/__tests__/editor.test.ts +57 -0
  105. package/src/core/cells/__tests__/harness.ts +82 -0
  106. package/src/core/cells/__tests__/layout.test.ts +189 -0
  107. package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
  108. package/src/core/cells/__tests__/session.test.ts +329 -174
  109. package/src/core/cells/__tests__/utils.test.ts +0 -1
  110. package/src/core/cells/actions.ts +247 -82
  111. package/src/core/cells/arrange.ts +341 -0
  112. package/src/core/cells/cells.ts +27 -516
  113. package/src/core/cells/document.ts +6 -5
  114. package/src/core/cells/edit.ts +30 -0
  115. package/src/core/cells/editor.ts +82 -0
  116. package/src/core/cells/layout.ts +152 -0
  117. package/src/core/cells/session.ts +75 -52
  118. package/src/core/cells/types.ts +0 -5
  119. package/src/core/cells/utils.ts +6 -4
  120. package/src/core/config/__tests__/app-config.test.ts +64 -0
  121. package/src/core/config/__tests__/config-schema.test.ts +1 -1
  122. package/src/core/config/config-schema.ts +6 -2
  123. package/src/core/config/config.ts +26 -12
  124. package/src/core/islands/bootstrap.ts +0 -1
  125. package/src/core/kernel/handlers.ts +1 -13
  126. package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
  127. package/src/core/meta/code-visibility.ts +18 -9
  128. package/src/core/notebook/__tests__/operations.test.ts +51 -0
  129. package/src/core/notebook/document/__tests__/codemirror.test.ts +15 -0
  130. package/src/core/notebook/index.ts +2 -1
  131. package/src/core/notebook/operations.ts +30 -0
  132. package/src/core/saving/__tests__/state.test.ts +111 -0
  133. package/src/core/saving/save-component.tsx +19 -33
  134. package/src/core/saving/state.ts +38 -39
  135. package/src/core/saving/useAutoSave.ts +8 -35
  136. package/src/core/session/__tests__/backend.test.ts +1 -0
  137. package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +1 -0
  138. package/src/core/session/active-notebook.ts +11 -0
  139. package/src/core/session/backend.ts +17 -19
  140. package/src/core/session/embedded.ts +7 -0
  141. package/src/core/session/useNotebookReplica.ts +3 -1
  142. package/src/core/session/useNotebookSession.tsx +7 -8
  143. package/src/core/transports/__tests__/static.test.ts +139 -8
  144. package/src/core/transports/static.ts +98 -16
  145. package/src/core/wasm/__tests__/bridge.test.ts +1 -0
  146. package/src/mount.tsx +19 -10
  147. package/src/stories/cell.stories.tsx +0 -2
  148. package/src/stories/layout/vertical/one-column.stories.tsx +0 -2
  149. package/src/utils/__tests__/id-tree.test.ts +20 -1548
  150. package/src/utils/id-tree.tsx +12 -537
  151. package/dist/assets/__vite-browser-external-C8gQho07.js +0 -1
  152. package/dist/feedback-button-CDf1yRm_.js +0 -379
  153. package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
  154. package/src/components/editor/navigation/state.ts +0 -65
@@ -21,7 +21,6 @@ import {
21
21
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
22
22
  import { store } from "@/core/state/jotai";
23
23
  import { activeDocumentModelAtom } from "@/core/session/active-notebook";
24
- import { MultiColumn } from "@/utils/id-tree";
25
24
  import { isRtcEnabled } from "../../rtc/state";
26
25
  import {
27
26
  createNotebookActions,
@@ -206,7 +205,7 @@ describe("applyDocument", () => {
206
205
  expect(next.cellHandles[b]).toBeUndefined();
207
206
  });
208
207
 
209
- it("regroups columns from the cells' configs, keeping column ids and collapsed sections", () => {
208
+ it("regroups columns from the cells' configs, keeping column ids", () => {
210
209
  const column1 = { ...cell(d).config, column: 1 };
211
210
  const state = notebookOf([
212
211
  cell(a),
@@ -214,22 +213,13 @@ describe("applyDocument", () => {
214
213
  cell(c),
215
214
  cell(d, { config: column1 }),
216
215
  ]);
217
- const [first, second] = state.cellIds.getColumns();
218
- const collapsed = {
219
- ...state,
220
- cellIds: new MultiColumn([first.collapse(a, b), second]),
221
- };
222
-
223
216
  const next = applyDocument(
224
- collapsed,
217
+ state,
225
218
  documentOf([cell(c), cell(a), cell(b), cell(d, { config: column1 })]),
226
219
  );
227
220
 
228
221
  expect(next.cellIds.inOrderIds).toEqual([c, a, b, d]);
229
- expect(next.cellIds.getColumnIds()).toEqual(
230
- collapsed.cellIds.getColumnIds(),
231
- );
232
- expect(next.cellIds.atOrThrow(0).isCollapsed(a)).toBe(true);
222
+ expect(next.cellIds.getColumnIds()).toEqual(state.cellIds.getColumnIds());
233
223
  expect(next.cellIds.atOrThrow(1).inOrderIds).toEqual([d]);
234
224
  });
235
225
 
@@ -293,25 +283,6 @@ describe("documentEditMiddleware", () => {
293
283
  expect(calls).toEqual([["setCode", a, "a = 2"]]);
294
284
  });
295
285
 
296
- it("sets the order when a cell moves", () => {
297
- actions.moveCell({ cellId: c, before: true });
298
-
299
- expect(calls).toEqual([["setOrder", [a, c, b]]]);
300
- });
301
-
302
- it("sets the column of a cell that changes column", () => {
303
- state = notebookOf([
304
- cell(a),
305
- cell(b),
306
- cell(c, { config: { ...cell(c).config, column: 1 } }),
307
- ]);
308
-
309
- actions.moveCell({ cellId: b, direction: "right" });
310
-
311
- expect(state.cellIds.atOrThrow(1).inOrderIds).toEqual([b, c]);
312
- expect(calls).toEqual([["updateConfig", b, { column: 1 }]]);
313
- });
314
-
315
286
  it("clears columns merged into one", () => {
316
287
  state = notebookOf([
317
288
  cell(a),
@@ -0,0 +1,57 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { act, renderHook } from "@testing-library/react";
5
+ import { createStore, Provider } from "jotai";
6
+ import { createElement, type PropsWithChildren } from "react";
7
+ import { describe, expect, it } from "vitest";
8
+ import { cellId } from "@/__tests__/branded";
9
+ import {
10
+ hideShownCodeAtom,
11
+ isCodeHidden,
12
+ restoredEditorStatesAtom,
13
+ showHiddenCodeAtom,
14
+ shownHiddenCodeAtom,
15
+ takeRestoredEditorStateAtom,
16
+ useRestoredEditorState,
17
+ } from "../editor";
18
+
19
+ const a = cellId("a");
20
+ const b = cellId("b");
21
+
22
+ describe("hidden code showing for now", () => {
23
+ it("hides code unless the cell is showing it", () => {
24
+ const shown = new Set([a]);
25
+ expect(isCodeHidden(true, shown, a)).toBe(false);
26
+ expect(isCodeHidden(true, shown, b)).toBe(true);
27
+ expect(isCodeHidden(false, shown, b)).toBe(false);
28
+ expect(isCodeHidden(undefined, shown, b)).toBe(false);
29
+ });
30
+
31
+ it("shows a cell's code and hides it again, saying whether it was showing", () => {
32
+ const store = createStore();
33
+ store.set(showHiddenCodeAtom, { cellId: a });
34
+ expect(store.get(shownHiddenCodeAtom).has(a)).toBe(true);
35
+ expect(store.set(hideShownCodeAtom, { cellId: a })).toBe(true);
36
+ expect(store.get(shownHiddenCodeAtom).has(a)).toBe(false);
37
+ expect(store.set(hideShownCodeAtom, { cellId: a })).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe("restored editor states", () => {
42
+ it("hands a state over once", () => {
43
+ const store = createStore();
44
+ store.set(restoredEditorStatesAtom, new Map([[a, { doc: "x = 1" }]]));
45
+ const wrapper = ({ children }: PropsWithChildren) =>
46
+ createElement(Provider, { store }, children);
47
+ const { result } = renderHook(() => useRestoredEditorState(a), {
48
+ wrapper,
49
+ });
50
+ expect(result.current).toEqual({ doc: "x = 1" });
51
+
52
+ act(() => store.set(takeRestoredEditorStateAtom, { cellId: a }));
53
+
54
+ expect(result.current).toBeNull();
55
+ expect(store.get(restoredEditorStatesAtom).size).toBe(0);
56
+ });
57
+ });
@@ -0,0 +1,82 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** A store whose notebook follows a document model, as the session wires it. */
4
+
5
+ import { history } from "@codemirror/commands";
6
+ import { EditorState } from "@codemirror/state";
7
+ import { EditorView } from "@codemirror/view";
8
+ import type * as Protocol from "@marimo-team/client/protocol";
9
+ import { createStore } from "jotai";
10
+ import { mockDocumentModel } from "@/__mocks__/document";
11
+ import {
12
+ document as documentOf,
13
+ withCells,
14
+ } from "@/core/notebook/document/__tests__/fixtures";
15
+ import { exportedForTesting, notebookAtom, notebookReducer } from "../cells";
16
+ import { followDocument } from "../document";
17
+ import { layoutAtom } from "../layout";
18
+
19
+ export function setupNotebook(
20
+ stops: Array<() => void>,
21
+ cells?: Record<string, Protocol.Cell | null>,
22
+ ) {
23
+ const store = createStore();
24
+ store.set(notebookAtom, exportedForTesting.initialNotebookState());
25
+ const { model, stop } = mockDocumentModel(
26
+ store,
27
+ cells ? withCells(documentOf(), cells) : documentOf(),
28
+ );
29
+ const controller = new AbortController();
30
+ stops.push(stop, () => controller.abort());
31
+ followDocument({
32
+ store,
33
+ model,
34
+ reconcile: (document) =>
35
+ store.set(notebookAtom, (state) =>
36
+ notebookReducer(state, {
37
+ type: "reconcileDocument",
38
+ payload: document,
39
+ }),
40
+ ),
41
+ signal: controller.signal,
42
+ });
43
+ return {
44
+ store,
45
+ model,
46
+ order: () => store.get(notebookAtom).cellIds.inOrderIds,
47
+ /** The cells of each column, as the page lays them out. */
48
+ columns: () => store.get(layoutAtom).columns.map((column) => column.ids),
49
+ notebook: () => store.get(notebookAtom),
50
+ cellIn: (id: string) => store.get(model.documentAtom)?.cells.get(id),
51
+ /** Give a cell an editor, as mounting it would. */
52
+ mountEditor: (id: string) => {
53
+ const view = new EditorView({
54
+ state: EditorState.create({
55
+ doc: store.get(model.documentAtom)?.cells.get(id)?.code ?? "",
56
+ extensions: [history()],
57
+ }),
58
+ });
59
+ store.get(notebookAtom).cellHandles[id as never].current = {
60
+ editorView: view,
61
+ editorViewOrNull: view,
62
+ };
63
+ return view;
64
+ },
65
+ /** Give a cell's output a heading, as the kernel's outline would. */
66
+ heading: (id: string, level: number) =>
67
+ store.set(notebookAtom, (state) => ({
68
+ ...state,
69
+ cellRuntime: {
70
+ ...state.cellRuntime,
71
+ [id]: {
72
+ ...state.cellRuntime[id as never],
73
+ outline: { items: [{ name: `H${level}`, level, by: { id } }] },
74
+ },
75
+ },
76
+ })),
77
+ visible: () => store.get(layoutAtom).columns[0].visible,
78
+ /** Apply a reducer action, as `useReducerActions()` does. */
79
+ dispatch: (action: Parameters<typeof notebookReducer>[1]) =>
80
+ store.set(notebookAtom, (state) => notebookReducer(state, action)),
81
+ };
82
+ }
@@ -0,0 +1,189 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { createStore } from "jotai";
4
+ import { describe, expect, it } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import type { EditableCell } from "@/core/notebook/document";
7
+ import type { CellColumnId } from "@/utils/id-tree";
8
+ import { exportedForTesting, notebookAtom, notebookReducer } from "../cells";
9
+ import { applyDocument } from "../document";
10
+ import type { CellId } from "../ids";
11
+ import {
12
+ collapsedAtom,
13
+ columnOf,
14
+ deriveLayout,
15
+ foldSections,
16
+ foldsAfter,
17
+ foldsHiding,
18
+ layoutAtom,
19
+ visibleNeighbor,
20
+ } from "../layout";
21
+ import type { Outline } from "../outline";
22
+
23
+ const [h1, a, h2, b, c, h1b, d] = ["h1", "a", "h2", "b", "c", "h1b", "d"].map(
24
+ cellId,
25
+ );
26
+ const ids = [h1, a, h2, b, c, h1b, d];
27
+
28
+ function heading(level: number): Outline {
29
+ return { items: [{ name: `H${level}`, level, by: { id: `h${level}` } }] };
30
+ }
31
+ const outlines = new Map<CellId, Outline>([
32
+ [h1, heading(1)],
33
+ [h2, heading(2)],
34
+ [h1b, heading(1)],
35
+ ]);
36
+ const outlineOf = (id: CellId) => outlines.get(id) ?? null;
37
+ const column = (id: string, cells: CellId[]) => ({
38
+ id: id as CellColumnId,
39
+ ids: cells,
40
+ });
41
+
42
+ describe("foldSections", () => {
43
+ it("folds a header until the next header of its level or higher", () => {
44
+ const folds = foldSections(ids, ids.map(outlineOf), new Set([h2]));
45
+ expect([...folds]).toEqual([[h2, [b, c]]]);
46
+ });
47
+
48
+ it("hides a folded section inside a folded section", () => {
49
+ const folds = foldSections(ids, ids.map(outlineOf), new Set([h1, h2]));
50
+ expect(folds.get(h1)).toEqual([a, h2, b, c]);
51
+ expect(folds.get(h2)).toEqual([b, c]);
52
+ });
53
+
54
+ it("folds the last section to the end", () => {
55
+ const folds = foldSections(ids, ids.map(outlineOf), new Set([h1b]));
56
+ expect(folds.get(h1b)).toEqual([d]);
57
+ });
58
+
59
+ it("ignores cells without a heading", () => {
60
+ const folds = foldSections([h1, a], [heading(1), null], new Set([a]));
61
+ expect(folds.size).toBe(0);
62
+ });
63
+
64
+ it("keeps an empty fold for a header with nothing under it yet", () => {
65
+ const folds = foldSections(
66
+ [h1, a, h1b],
67
+ [heading(1), null, heading(1)],
68
+ new Set([h1b]),
69
+ );
70
+ expect(folds.get(h1b)).toEqual([]);
71
+ });
72
+ });
73
+
74
+ describe("deriveLayout", () => {
75
+ const layout = deriveLayout(
76
+ [column("left", ids), column("right", [])],
77
+ outlineOf,
78
+ new Set([h2]),
79
+ );
80
+
81
+ it("lists visible cells per column and keeps empty columns", () => {
82
+ expect(layout.columns.map((c) => c.id)).toEqual(["left", "right"]);
83
+ expect(layout.columns[0].visible).toEqual([h1, a, h2, h1b, d]);
84
+ expect(layout.columns[0].ids).toEqual(ids);
85
+ expect(layout.columns[1].visible).toEqual([]);
86
+ });
87
+
88
+ it("finds a cell's column, the folds hiding it, and its visible neighbours", () => {
89
+ expect(columnOf(layout, b).id).toBe("left");
90
+ expect(() => columnOf(layout, cellId("missing"))).toThrow(
91
+ "not in the layout",
92
+ );
93
+ expect(foldsHiding(layout, c)).toEqual([h2]);
94
+ expect(foldsHiding(layout, a)).toEqual([]);
95
+ expect(visibleNeighbor(layout, h2, 1)).toBe(h1b);
96
+ expect(visibleNeighbor(layout, h1b, -1)).toBe(h2);
97
+ expect(visibleNeighbor(layout, h1, -1)).toBeUndefined();
98
+ expect(visibleNeighbor(layout, b, 1)).toBeUndefined();
99
+ });
100
+
101
+ it("names the folds a cell placed after another would fall into", () => {
102
+ // After the folded header or the last cell of its fold, inside it.
103
+ expect(foldsAfter(layout, h2)).toEqual([h2]);
104
+ expect(foldsAfter(layout, c)).toEqual([h2]);
105
+ // After a visible cell no fold reaches, nothing.
106
+ expect(foldsAfter(layout, a)).toEqual([]);
107
+ expect(foldsAfter(layout, h1b)).toEqual([]);
108
+
109
+ const nested = deriveLayout(
110
+ [column("left", ids)],
111
+ outlineOf,
112
+ new Set([h1, h2]),
113
+ );
114
+ expect(foldsAfter(nested, c)).toEqual([h1, h2]);
115
+ });
116
+ });
117
+
118
+ describe("layoutAtom", () => {
119
+ /** A store whose notebook holds `ids`, in that order, with nothing run. */
120
+ function notebookOf(...ids: CellId[]) {
121
+ const store = createStore();
122
+ const cells = ids.map((id): EditableCell => ({
123
+ id,
124
+ code: "",
125
+ name: "_",
126
+ config: {
127
+ column: null,
128
+ disabled: false,
129
+ hide_code: false,
130
+ expand_output: false,
131
+ },
132
+ }));
133
+ store.set(
134
+ notebookAtom,
135
+ applyDocument(exportedForTesting.initialNotebookState(), {
136
+ order: ids,
137
+ cells: new Map(cells.map((cell) => [cell.id, cell])),
138
+ }),
139
+ );
140
+ return store;
141
+ }
142
+
143
+ /** Deliver `html` as a cell's output, as the kernel's message does. */
144
+ function output(
145
+ store: ReturnType<typeof createStore>,
146
+ id: CellId,
147
+ html: string,
148
+ ) {
149
+ store.set(notebookAtom, (state) =>
150
+ notebookReducer(state, {
151
+ type: "handleCellMessage",
152
+ payload: {
153
+ cell_id: id,
154
+ output: {
155
+ channel: "output",
156
+ mimetype: "text/html",
157
+ data: html,
158
+ timestamp: 1,
159
+ },
160
+ console: null,
161
+ status: "idle",
162
+ stale_inputs: null,
163
+ timestamp: 1,
164
+ },
165
+ }),
166
+ );
167
+ }
168
+
169
+ it("is the same layout after output that changes no heading", () => {
170
+ const store = notebookOf(h1, a);
171
+ output(store, h1, "<h1>Title</h1>");
172
+ const layout = store.get(layoutAtom);
173
+
174
+ output(store, a, "<p>1</p>");
175
+ output(store, h1, "<h1>Title</h1>");
176
+
177
+ expect(store.get(layoutAtom)).toBe(layout);
178
+ });
179
+
180
+ it("follows a heading arriving in a cell's output", () => {
181
+ const store = notebookOf(h1, a);
182
+ store.set(collapsedAtom, new Set([h1]));
183
+ expect(store.get(layoutAtom).columns[0].folds.size).toBe(0);
184
+
185
+ output(store, h1, "<h1>Title</h1>");
186
+
187
+ expect(store.get(layoutAtom).columns[0].folds.get(h1)).toEqual([a]);
188
+ });
189
+ });
@@ -80,7 +80,13 @@ describe("notebookOutline", () => {
80
80
 
81
81
  it("keeps headings in cell order after moving and deleting cells", () => {
82
82
  const { store, actions } = setup();
83
- actions.moveCell({ cellId: second, before: true });
83
+ actions.reconcileDocument({
84
+ order: [second, first],
85
+ cells: new Map([
86
+ [first, { id: first, code: "x = 1", name: "_", config: CONFIG }],
87
+ [second, { id: second, code: "y = 2", name: "_", config: CONFIG }],
88
+ ]),
89
+ });
84
90
  expect(store.get(notebookOutline)).toEqual({
85
91
  items: [...secondOutline.items, ...firstOutline.items],
86
92
  });