@marimo-team/islands 0.25.3-dev192 → 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-Dw6uRKVk.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-v_7ijLtx.js → code-visibility-BTVimCUw.js} +38 -42
  17. package/dist/{common-BUwWHpnK.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-4IZymnXZ.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-DU8_EDG5.js → glide-data-editor-DFsqVbWR.js} +5 -5
  33. package/dist/{globals-BeBDOk22.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-BoIkyh6H.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-5M3a2rpj.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-MpeoIn1D.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
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
4
  import { cellId } from "@/__tests__/branded";
5
- import { MultiColumn } from "@/utils/id-tree";
5
+ import type { CellId } from "@/core/cells/ids";
6
+ import type { Layout } from "@/core/cells/layout";
7
+ import type { CellColumnId } from "@/utils/id-tree";
6
8
  import type { CellSelectionState } from "../selection";
7
9
  import { exportedForTesting } from "../selection";
8
10
 
@@ -16,6 +18,14 @@ const CellIds = {
16
18
  e: cellId("e"),
17
19
  };
18
20
 
21
+ /** A one-column page of `ids`, with `hidden` folded away under the cell before them. */
22
+ function page(ids: CellId[], hidden: CellId[] = []): Layout {
23
+ const visible = ids.filter((id) => !hidden.includes(id));
24
+ return {
25
+ columns: [{ id: "column" as CellColumnId, ids, visible, folds: new Map() }],
26
+ };
27
+ }
28
+
19
29
  describe("cell selection reducer", () => {
20
30
  let state: CellSelectionState;
21
31
 
@@ -57,15 +67,13 @@ describe("cell selection reducer", () => {
57
67
  });
58
68
 
59
69
  it("should extend selection forward", () => {
60
- const allCellIds = MultiColumn.from([
61
- [CellIds.a, CellIds.b, CellIds.c, CellIds.d],
62
- ]);
70
+ const layout = page([CellIds.a, CellIds.b, CellIds.c, CellIds.d]);
63
71
 
64
72
  // First select cell a
65
73
  actions.select({ cellId: CellIds.a });
66
74
 
67
75
  // Then extend to cell c
68
- actions.extend({ cellId: CellIds.c, allCellIds });
76
+ actions.extend({ cellId: CellIds.c, layout });
69
77
 
70
78
  expect(state).toEqual({
71
79
  selectionStart: CellIds.a,
@@ -75,15 +83,13 @@ describe("cell selection reducer", () => {
75
83
  });
76
84
 
77
85
  it("should extend selection backward", () => {
78
- const allCellIds = MultiColumn.from([
79
- [CellIds.a, CellIds.b, CellIds.c, CellIds.d],
80
- ]);
86
+ const layout = page([CellIds.a, CellIds.b, CellIds.c, CellIds.d]);
81
87
 
82
88
  // First select cell c
83
89
  actions.select({ cellId: CellIds.c });
84
90
 
85
91
  // Then extend to cell a (backward)
86
- actions.extend({ cellId: CellIds.a, allCellIds });
92
+ actions.extend({ cellId: CellIds.a, layout });
87
93
 
88
94
  expect(state).toEqual({
89
95
  selectionStart: CellIds.c,
@@ -93,13 +99,13 @@ describe("cell selection reducer", () => {
93
99
  });
94
100
 
95
101
  it("should extend selection to same cell", () => {
96
- const allCellIds = MultiColumn.from([[CellIds.a, CellIds.b, CellIds.c]]);
102
+ const layout = page([CellIds.a, CellIds.b, CellIds.c]);
97
103
 
98
104
  // First select cell b
99
105
  actions.select({ cellId: CellIds.b });
100
106
 
101
107
  // Then extend to same cell
102
- actions.extend({ cellId: CellIds.b, allCellIds });
108
+ actions.extend({ cellId: CellIds.b, layout });
103
109
 
104
110
  expect(state).toEqual({
105
111
  selectionStart: CellIds.b,
@@ -109,10 +115,10 @@ describe("cell selection reducer", () => {
109
115
  });
110
116
 
111
117
  it("should fallback to single select when extending without selectionStart", () => {
112
- const allCellIds = MultiColumn.from([[CellIds.a, CellIds.b, CellIds.c]]);
118
+ const layout = page([CellIds.a, CellIds.b, CellIds.c]);
113
119
 
114
120
  // Try to extend without any previous selection
115
- actions.extend({ cellId: CellIds.b, allCellIds });
121
+ actions.extend({ cellId: CellIds.b, layout });
116
122
 
117
123
  expect(state).toEqual({
118
124
  selectionStart: CellIds.b,
@@ -122,11 +128,11 @@ describe("cell selection reducer", () => {
122
128
  });
123
129
 
124
130
  it("should fallback to single select when extending with invalid cell ids", () => {
125
- const allCellIds = MultiColumn.from([[CellIds.a, CellIds.b, CellIds.c]]);
131
+ const layout = page([CellIds.a, CellIds.b, CellIds.c]);
126
132
 
127
- // Create a state with invalid selection start (not in allCellIds)
133
+ // Create a state with invalid selection start (not on the page)
128
134
  const invalidState = {
129
- selectionStart: CellIds.d, // not in allCellIds
135
+ selectionStart: CellIds.d, // not on the page
130
136
  selectionEnd: CellIds.d,
131
137
  selected: new Set([CellIds.d]),
132
138
  };
@@ -134,7 +140,7 @@ describe("cell selection reducer", () => {
134
140
  // Test reducer directly since we need to start with custom state
135
141
  const result = reducer(invalidState, {
136
142
  type: "extend",
137
- payload: { cellId: CellIds.b, allCellIds },
143
+ payload: { cellId: CellIds.b, layout },
138
144
  });
139
145
 
140
146
  expect(result).toEqual({
@@ -144,6 +150,19 @@ describe("cell selection reducer", () => {
144
150
  });
145
151
  });
146
152
 
153
+ it("steps over the cells a fold hides", () => {
154
+ const layout = page(
155
+ [CellIds.a, CellIds.b, CellIds.c, CellIds.d, CellIds.e],
156
+ [CellIds.b, CellIds.c],
157
+ );
158
+
159
+ actions.select({ cellId: CellIds.a });
160
+ actions.extend({ cellId: CellIds.d, layout });
161
+
162
+ expect(state.selected).toEqual(new Set([CellIds.a, CellIds.d]));
163
+ expect(state.selectionEnd).toBe(CellIds.d);
164
+ });
165
+
147
166
  it("should clear selection", () => {
148
167
  // First select some cells
149
168
  actions.select({ cellId: CellIds.a });
@@ -171,19 +190,23 @@ describe("cell selection reducer", () => {
171
190
  });
172
191
 
173
192
  it("should handle complex selection scenarios", () => {
174
- const allCellIds = MultiColumn.from([
175
- [CellIds.a, CellIds.b, CellIds.c, CellIds.d, CellIds.e],
193
+ const layout = page([
194
+ CellIds.a,
195
+ CellIds.b,
196
+ CellIds.c,
197
+ CellIds.d,
198
+ CellIds.e,
176
199
  ]);
177
200
 
178
201
  // Select middle cell
179
202
  actions.select({ cellId: CellIds.c });
180
203
 
181
204
  // Extend to end
182
- actions.extend({ cellId: CellIds.e, allCellIds });
205
+ actions.extend({ cellId: CellIds.e, layout });
183
206
  expect(state.selected).toEqual(new Set([CellIds.c, CellIds.d, CellIds.e]));
184
207
 
185
208
  // Extend to beginning (should reverse)
186
- actions.extend({ cellId: CellIds.a, allCellIds });
209
+ actions.extend({ cellId: CellIds.a, layout });
187
210
  expect(state.selected).toEqual(new Set([CellIds.a, CellIds.b, CellIds.c]));
188
211
  expect(state.selectionStart).toBe(CellIds.c);
189
212
  expect(state.selectionEnd).toBe(CellIds.a);
@@ -3,6 +3,7 @@
3
3
  import type { createStore } from "jotai";
4
4
  import { getCellEditorView } from "@/core/cells/cells";
5
5
  import { type CellId, HTMLCellId } from "@/core/cells/ids";
6
+ import { scrollCellIntoView } from "@/core/cells/scrollCellIntoView";
6
7
  import { Logger } from "@/utils/Logger";
7
8
 
8
9
  export function focusCellEditor(
@@ -38,6 +39,24 @@ export function raf2(callback: () => void): void {
38
39
  });
39
40
  }
40
41
 
42
+ /**
43
+ * Keep the focused cell in view after a layout shift, such as its code being
44
+ * hidden again.
45
+ */
46
+ export function keepFocusedCellInView(): void {
47
+ raf2(() => {
48
+ const activeElement = document.activeElement;
49
+ if (!activeElement) {
50
+ return;
51
+ }
52
+ const focusedCell = HTMLCellId.findElementThroughShadowDOMs(activeElement);
53
+ if (!focusedCell) {
54
+ return;
55
+ }
56
+ scrollCellIntoView(HTMLCellId.parse(focusedCell.id));
57
+ });
58
+ }
59
+
41
60
  /**
42
61
  * Checks if the cell is focused at the top level.
43
62
  */
@@ -30,12 +30,9 @@ import {
30
30
  DropdownMenuSeparator,
31
31
  DropdownMenuTrigger,
32
32
  } from "@/components/ui/dropdown-menu";
33
- import {
34
- getCellEditorView,
35
- hasOnlyOneCellAtom,
36
- notebookAtom,
37
- } from "@/core/cells/cells";
33
+ import { getCellEditorView, hasOnlyOneCellAtom } from "@/core/cells/cells";
38
34
  import { useCellActions } from "@/core/cells/actions";
35
+ import { columnOf, layoutAtom } from "@/core/cells/layout";
39
36
  import { useNotebookDocumentEdit } from "@/core/notebook/document";
40
37
  import type { CellId } from "@/core/cells/ids";
41
38
  import { usePendingDeleteService } from "@/core/cells/pending-delete-service";
@@ -142,10 +139,8 @@ export function useMultiCellActionButtons(cellIds: CellId[]) {
142
139
  /// If moving down, make sure the last cell is not at the bottom of the notebook
143
140
  if (direction === "down") {
144
141
  const lastCellId = cellIds[cellIds.length - 1];
145
- const notebook = store.get(notebookAtom);
146
- const isLast =
147
- notebook.cellIds.findWithId(lastCellId).last() === lastCellId;
148
- if (isLast) {
142
+ const layout = store.get(layoutAtom);
143
+ if (columnOf(layout, lastCellId).visible.at(-1) === lastCellId) {
149
144
  return;
150
145
  }
151
146
  }
@@ -153,10 +148,8 @@ export function useMultiCellActionButtons(cellIds: CellId[]) {
153
148
  // If moving up, make sure the first cell is not at the top of the notebook
154
149
  if (direction === "up") {
155
150
  const firstCellId = cellIds[0];
156
- const notebook = store.get(notebookAtom);
157
- const isFirst =
158
- notebook.cellIds.findWithId(firstCellId).first() === firstCellId;
159
- if (isFirst) {
151
+ const layout = store.get(layoutAtom);
152
+ if (columnOf(layout, firstCellId).visible[0] === firstCellId) {
160
153
  return;
161
154
  }
162
155
  }
@@ -13,7 +13,14 @@ import { mergeProps, useFocusWithin, useKeyboard } from "react-aria";
13
13
  import { DATA_FOR_CELL_ID } from "@/components/data-table/cell-utils";
14
14
  import { aiCompletionCellAtom } from "@/core/ai/state";
15
15
  import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
16
- import { cellIdsAtom, notebookAtom } from "@/core/cells/cells";
16
+ import { notebookAtom } from "@/core/cells/cells";
17
+ import { hideShownCodeAtom, showHiddenCodeAtom } from "@/core/cells/editor";
18
+ import {
19
+ columnOf,
20
+ type LayoutColumn,
21
+ layoutAtom,
22
+ visibleNeighbor,
23
+ } from "@/core/cells/layout";
17
24
  import { useCellActions } from "@/core/cells/actions";
18
25
  import { useNotebookDocumentEdit } from "@/core/notebook/document";
19
26
  import { useCellFocusActions } from "@/core/cells/focus";
@@ -40,18 +47,21 @@ import type { HotkeyAction } from "@/core/hotkeys/hotkeys";
40
47
  import { parseShortcut } from "@/core/hotkeys/shortcuts";
41
48
  import { useSaveNotebook } from "@/core/saving/save-component";
42
49
  import { Events } from "@/utils/events";
43
- import type { CollapsibleTree } from "@/utils/id-tree";
44
50
  import type { CellActionsDropdownHandle } from "../cell/cell-actions";
45
51
  import { useDeleteManyCellsCallback } from "../cell/useDeleteCell";
46
52
  import { useRunCells } from "../cell/useRunCells";
47
53
  import { useCellClipboard } from "./clipboard";
48
- import { focusCell, focusCellEditor, raf2 } from "./focus-utils";
54
+ import {
55
+ focusCell,
56
+ focusCellEditor,
57
+ keepFocusedCellInView,
58
+ raf2,
59
+ } from "./focus-utils";
49
60
  import {
50
61
  getSelectedCells,
51
62
  useCellSelectionActions,
52
63
  useIsCellSelected,
53
64
  } from "./selection";
54
- import { useTemporarilyShownCodeActions } from "./state";
55
65
  import { handleVimKeybinding } from "./vim-bindings";
56
66
 
57
67
  interface HotkeyHandler {
@@ -105,8 +115,7 @@ function useCellFocusProps(
105
115
  editorView: React.RefObject<EditorView | null>,
106
116
  ) {
107
117
  const focusActions = useCellFocusActions();
108
- const actions = useCellActions();
109
- const temporarilyShownCodeActions = useTemporarilyShownCodeActions();
118
+ const hideShownCode = useSetAtom(hideShownCodeAtom);
110
119
 
111
120
  // This occurs at the cell level and descedants.
112
121
  const { focusWithinProps } = useFocusWithin({
@@ -131,9 +140,11 @@ function useCellFocusProps(
131
140
  return;
132
141
  }
133
142
 
134
- // On blur, hide the code if it was temporarily shown.
135
- temporarilyShownCodeActions.remove(cellId);
136
- actions.markTouched({ cellId });
143
+ // On blur, hide the code if it was showing for now. That shifts the
144
+ // layout, so the cell that took the focus is kept in view.
145
+ if (hideShownCode({ cellId })) {
146
+ keepFocusedCellInView();
147
+ }
137
148
  focusActions.blurCell();
138
149
  // Close signature help when clicking outside the cell
139
150
  if (editorView.current) {
@@ -195,7 +206,7 @@ export function useCellNavigationProps(
195
206
  const actions = useCellActions();
196
207
  const { createCell, moveToNextCell } = useCellActions();
197
208
  const store = useStore();
198
- const temporarilyShownCodeActions = useTemporarilyShownCodeActions();
209
+ const showHiddenCode = useSetAtom(showHiddenCodeAtom);
199
210
  const runCells = useRunCells();
200
211
  const keymapPreset = useAtomValue(keymapPresetAtom);
201
212
  const { copyCells, pasteAtCell, cutCells } = useCellClipboard();
@@ -268,12 +279,13 @@ export function useCellNavigationProps(
268
279
  // Move left across columns
269
280
  ArrowLeft: () => {
270
281
  if (canMoveX) {
271
- const notebook = store.get(notebookAtom);
272
- const column = notebook.cellIds.findWithId(cellId);
273
- const columnIndex = notebook.cellIds.indexOf(column);
274
- const leftColumn = notebook.cellIds.at(columnIndex - 1);
282
+ const layout = store.get(layoutAtom);
283
+ const columnIndex = layout.columns.indexOf(
284
+ columnOf(layout, cellId),
285
+ );
286
+ const leftColumn = layout.columns[columnIndex - 1];
275
287
 
276
- if (leftColumn && leftColumn.length > 0) {
288
+ if (leftColumn && leftColumn.visible.length > 0) {
277
289
  const leftCellId = findClosestAdjacentCell(cellId, leftColumn);
278
290
  actions.focusCell({ cellId: leftCellId, where: "exact" });
279
291
 
@@ -286,12 +298,13 @@ export function useCellNavigationProps(
286
298
  // Move right across columns
287
299
  ArrowRight: () => {
288
300
  if (canMoveX) {
289
- const notebook = store.get(notebookAtom);
290
- const column = notebook.cellIds.findWithId(cellId);
291
- const columnIndex = notebook.cellIds.indexOf(column);
292
- const rightColumn = notebook.cellIds.at(columnIndex + 1);
301
+ const layout = store.get(layoutAtom);
302
+ const columnIndex = layout.columns.indexOf(
303
+ columnOf(layout, cellId),
304
+ );
305
+ const rightColumn = layout.columns[columnIndex + 1];
293
306
 
294
- if (rightColumn && rightColumn.length > 0) {
307
+ if (rightColumn && rightColumn.visible.length > 0) {
295
308
  const rightCellId = findClosestAdjacentCell(cellId, rightColumn);
296
309
  actions.focusCell({ cellId: rightCellId, where: "exact" });
297
310
 
@@ -304,12 +317,12 @@ export function useCellNavigationProps(
304
317
  // Select up
305
318
  "Shift+ArrowUp": () => {
306
319
  // Select self
307
- const allCellIds = store.get(cellIdsAtom);
308
- selectionActions.extend({ cellId, allCellIds });
320
+ const layout = store.get(layoutAtom);
321
+ selectionActions.extend({ cellId, layout });
309
322
  // Select to where focus is going
310
- const beforeCellId = allCellIds.findWithId(cellId).before(cellId);
323
+ const beforeCellId = visibleNeighbor(layout, cellId, -1);
311
324
  if (beforeCellId) {
312
- selectionActions.extend({ cellId: beforeCellId, allCellIds });
325
+ selectionActions.extend({ cellId: beforeCellId, layout });
313
326
  }
314
327
  // Focus the cell
315
328
  actions.focusCell({ cellId, where: "before" });
@@ -318,12 +331,12 @@ export function useCellNavigationProps(
318
331
  // Select down
319
332
  "Shift+ArrowDown": () => {
320
333
  // Select self
321
- const allCellIds = store.get(cellIdsAtom);
322
- selectionActions.extend({ cellId, allCellIds });
334
+ const layout = store.get(layoutAtom);
335
+ selectionActions.extend({ cellId, layout });
323
336
  // Select to where focus is going
324
- const afterCellId = allCellIds.findWithId(cellId).after(cellId);
337
+ const afterCellId = visibleNeighbor(layout, cellId, 1);
325
338
  if (afterCellId) {
326
- selectionActions.extend({ cellId: afterCellId, allCellIds });
339
+ selectionActions.extend({ cellId: afterCellId, layout });
327
340
  }
328
341
  // Focus the cell
329
342
  actions.focusCell({ cellId, where: "after" });
@@ -345,7 +358,7 @@ export function useCellNavigationProps(
345
358
  },
346
359
  // Enter will focus the cell editor.
347
360
  Enter: () => {
348
- temporarilyShownCodeActions.add(cellId);
361
+ showHiddenCode({ cellId });
349
362
  focusCellEditor(store, cellId);
350
363
  selectionActions.clear();
351
364
  return true;
@@ -398,10 +411,8 @@ export function useCellNavigationProps(
398
411
  "cell.moveUp": addSingleHandler((cellIds) => {
399
412
  // If moving up, make sure the first cell is not at the top of the notebook
400
413
  const firstCellId = cellIds[0];
401
- const notebook = store.get(notebookAtom);
402
- const isFirst =
403
- notebook.cellIds.findWithId(firstCellId).first() === firstCellId;
404
- if (isFirst) {
414
+ const layout = store.get(layoutAtom);
415
+ if (columnOf(layout, firstCellId).visible[0] === firstCellId) {
405
416
  return false;
406
417
  }
407
418
 
@@ -413,10 +424,8 @@ export function useCellNavigationProps(
413
424
  "cell.moveDown": addSingleHandler((cellIds) => {
414
425
  // If moving down, make sure the last cell is not at the bottom of the notebook
415
426
  const lastCellId = cellIds[cellIds.length - 1];
416
- const notebook = store.get(notebookAtom);
417
- const isLast =
418
- notebook.cellIds.findWithId(lastCellId).last() === lastCellId;
419
- if (isLast) {
427
+ const layout = store.get(layoutAtom);
428
+ if (columnOf(layout, lastCellId).visible.at(-1) === lastCellId) {
420
429
  return false;
421
430
  }
422
431
 
@@ -693,7 +702,7 @@ export function useCellEditorNavigationProps(
693
702
  cellId: CellId,
694
703
  editorView: React.RefObject<EditorView | null>,
695
704
  ) {
696
- const temporarilyShownCodeActions = useTemporarilyShownCodeActions();
705
+ const hideShownCode = useSetAtom(hideShownCodeAtom);
697
706
  const keymapPreset = useAtomValue(keymapPresetAtom);
698
707
  const hotkeys = useAtomValue(hotkeysAtom);
699
708
 
@@ -703,7 +712,7 @@ export function useCellEditorNavigationProps(
703
712
  }, [hotkeys]);
704
713
 
705
714
  const exitToCommandMode = () => {
706
- temporarilyShownCodeActions.remove(cellId);
715
+ hideShownCode({ cellId });
707
716
  focusCell(cellId);
708
717
  // Scroll to cell in case it is not in view because of layout shifts.
709
718
  raf2(() => {
@@ -770,18 +779,18 @@ export function useCellEditorNavigationProps(
770
779
 
771
780
  function findClosestAdjacentCell(
772
781
  currentCellId: CellId,
773
- adjacentColumn: CollapsibleTree<CellId>,
782
+ adjacentColumn: LayoutColumn,
774
783
  ): CellId {
775
784
  const current = document.getElementById(HTMLCellId.create(currentCellId));
776
785
 
777
786
  if (!current) {
778
787
  // fallback to first
779
- return adjacentColumn.first();
788
+ return adjacentColumn.visible[0];
780
789
  }
781
790
 
782
791
  const currentRect = current.getBoundingClientRect();
783
792
  // first to to either overlap or contain the current cell's top edge
784
- for (const candidateId of adjacentColumn.topLevelIds) {
793
+ for (const candidateId of adjacentColumn.visible) {
785
794
  const candidate = document.getElementById(HTMLCellId.create(candidateId));
786
795
  if (candidate) {
787
796
  const candidateRect = candidate.getBoundingClientRect();
@@ -795,7 +804,7 @@ function findClosestAdjacentCell(
795
804
  }
796
805
 
797
806
  // no aligned cells (column beyond other), jump to last element
798
- return adjacentColumn.last();
807
+ return adjacentColumn.visible[adjacentColumn.visible.length - 1];
799
808
  }
800
809
 
801
810
  export function closeSignatureHelp(view: EditorView) {
@@ -2,8 +2,8 @@
2
2
  import { atom, type createStore, useAtomValue } from "jotai";
3
3
  import { useMemo } from "react";
4
4
  import type { CellId } from "@/core/cells/ids";
5
+ import type { Layout } from "@/core/cells/layout";
5
6
  import { createReducerAndAtoms } from "@/utils/createReducer";
6
- import type { MultiColumn } from "@/utils/id-tree";
7
7
 
8
8
  export interface CellSelectionState {
9
9
  selectionStart: CellId | null;
@@ -34,43 +34,44 @@ const {
34
34
  selected: new Set([payload.cellId]),
35
35
  }),
36
36
 
37
+ /**
38
+ * Extend the selection from where it started to `cellId`, over the cells
39
+ * the page shows between them. Cells a fold hides are stepped over, as
40
+ * the focus steps over them.
41
+ */
37
42
  extend: (
38
43
  state: CellSelectionState,
39
- payload: {
40
- cellId: CellId;
41
- allCellIds: MultiColumn<CellId>;
42
- },
44
+ payload: { cellId: CellId; layout: Layout },
43
45
  ): CellSelectionState => {
44
- if (!state.selectionStart) {
45
- // fallback to single select
46
- return {
47
- selectionStart: payload.cellId,
48
- selectionEnd: payload.cellId,
49
- selected: new Set([payload.cellId]),
50
- };
51
- }
52
- const { cellId, allCellIds } = payload;
53
-
54
- try {
55
- const column = allCellIds.findWithId(state.selectionStart);
56
- const startIdx = column.indexOfOrThrow(state.selectionStart);
57
- const endIdx = column.indexOfOrThrow(cellId);
58
- const [from, to] =
59
- startIdx < endIdx ? [startIdx, endIdx] : [endIdx, startIdx];
60
- const selected = column.slice(from, to + 1);
61
- return {
62
- selectionStart: state.selectionStart,
63
- selectionEnd: cellId,
64
- selected: new Set(selected),
65
- };
66
- } catch {
67
- // fallback to single select
46
+ const { cellId, layout } = payload;
47
+ const start = state.selectionStart;
48
+ const column =
49
+ start === null
50
+ ? undefined
51
+ : layout.columns.find((column) => column.visible.includes(start));
52
+ const startIdx =
53
+ start === null ? -1 : (column?.visible.indexOf(start) ?? -1);
54
+ const endIdx = column?.visible.indexOf(cellId) ?? -1;
55
+ if (
56
+ start === null ||
57
+ column === undefined ||
58
+ startIdx === -1 ||
59
+ endIdx === -1
60
+ ) {
61
+ // Nothing to extend from, or the cells are not on the page together.
68
62
  return {
69
63
  selectionStart: cellId,
70
64
  selectionEnd: cellId,
71
65
  selected: new Set([cellId]),
72
66
  };
73
67
  }
68
+ const [from, to] =
69
+ startIdx < endIdx ? [startIdx, endIdx] : [endIdx, startIdx];
70
+ return {
71
+ selectionStart: start,
72
+ selectionEnd: cellId,
73
+ selected: new Set(column.visible.slice(from, to + 1)),
74
+ };
74
75
  },
75
76
 
76
77
  clear: (state: CellSelectionState): CellSelectionState => {
@@ -16,7 +16,6 @@ import {
16
16
  useCallback,
17
17
  useEffect,
18
18
  useImperativeHandle,
19
- useMemo,
20
19
  useRef,
21
20
  useState,
22
21
  } from "react";
@@ -43,12 +42,15 @@ import { useResizeObserver } from "@/hooks/useResizeObserver";
43
42
  import { cn } from "@/utils/cn";
44
43
  import type { Milliseconds, Seconds } from "@/utils/time";
45
44
  import {
46
- createUntouchedCellAtom,
47
45
  useCellData,
48
46
  useCellHandle,
49
47
  useCellRuntime,
50
48
  } from "../../core/cells/cells";
51
49
  import { type CellActions, useCellActions } from "../../core/cells/actions";
50
+ import {
51
+ showHiddenCodeAtom,
52
+ useShowsHiddenCode,
53
+ } from "../../core/cells/editor";
52
54
  import { type CellId, SETUP_CELL_ID } from "../../core/cells/ids";
53
55
  import {
54
56
  cellNeedsRun,
@@ -85,10 +87,6 @@ import { useRunCell } from "./cell/useRunCells";
85
87
  import { cellDomProps } from "./common";
86
88
  import { SqlValidationErrorBanner } from "./errors/sql-validation-errors";
87
89
  import { useCellNavigationProps } from "./navigation/navigation";
88
- import {
89
- useTemporarilyShownCode,
90
- useTemporarilyShownCodeActions,
91
- } from "./navigation/state";
92
90
  import { type OnRefactorWithAI, OutputArea } from "./Output";
93
91
  import { ConsoleOutput } from "./output/console/ConsoleOutput";
94
92
  import { CellDragHandle, SortableCell } from "./SortableCell";
@@ -167,15 +165,12 @@ function useCellHiddenLogic({
167
165
  editorView: React.RefObject<EditorView | null>;
168
166
  editorViewParentRef: React.RefObject<HTMLDivElement | null>;
169
167
  }) {
170
- const temporarilyVisible = useTemporarilyShownCode(cellId);
171
- const temporarilyShownCodeActions = useTemporarilyShownCodeActions();
172
- const isUntouched = useAtomValue(
173
- useMemo(() => createUntouchedCellAtom(cellId), [cellId]),
174
- );
168
+ const showsHiddenCode = useShowsHiddenCode(cellId);
169
+ const showHiddenCodeNow = useSetAtom(showHiddenCodeAtom);
175
170
 
176
- // The cell code is shown if the cell is not configured to be hidden or if the code is temporarily visible (i.e. when focused).
177
- const isCellCodeShown =
178
- !cellConfig.hide_code || temporarilyVisible || isUntouched;
171
+ // The code is shown unless the cell hides it, and then still while it is
172
+ // being edited or was just created.
173
+ const isCellCodeShown = !cellConfig.hide_code || showsHiddenCode;
179
174
  const isMarkdown = languageAdapter === "markdown";
180
175
  const isMarkdownCodeHidden = isMarkdown && !isCellCodeShown;
181
176
 
@@ -188,7 +183,7 @@ function useCellHiddenLogic({
188
183
 
189
184
  // Default to true
190
185
  const focus = opts?.focus ?? true;
191
- temporarilyShownCodeActions.add(cellId);
186
+ showHiddenCodeNow({ cellId });
192
187
 
193
188
  if (focus) {
194
189
  editorView.current?.focus();
@@ -219,7 +214,6 @@ export type CellComponentActions = Pick<
219
214
  | "moveCell"
220
215
  | "collapseCell"
221
216
  | "expandCell"
222
- | "clearSerializedEditorState"
223
217
  | "setStdinResponse"
224
218
  | "clearCellConsoleOutput"
225
219
  | "sendToBottom"
@@ -704,7 +698,6 @@ const EditableCellComponent = ({
704
698
  code={cellData.code}
705
699
  config={cellData.config}
706
700
  status={cellRuntime.status}
707
- serializedEditorState={cellData.serializedEditorState}
708
701
  runCell={runCell}
709
702
  setEditorView={setEditorView}
710
703
  userConfig={userConfig}
@@ -1233,7 +1226,6 @@ const SetupCellComponent = ({
1233
1226
  code={cellData.code}
1234
1227
  config={cellData.config}
1235
1228
  status={cellRuntime.status}
1236
- serializedEditorState={cellData.serializedEditorState}
1237
1229
  runCell={runCell}
1238
1230
  setEditorView={setEditorView}
1239
1231
  userConfig={userConfig}