@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
@@ -95,7 +95,6 @@ describe("getCellConfigs", () => {
95
95
  config: { hide_code: false, disabled: false },
96
96
  } as CellData,
97
97
  },
98
- untouchedNewCells: new Set(),
99
98
  };
100
99
 
101
100
  const result = getCellConfigs(mockState);
@@ -3,26 +3,54 @@
3
3
  /** Notebook operations that edit the document and then adjust the editor. */
4
4
 
5
5
  import { historyField } from "@codemirror/commands";
6
- import { atom, type Getter, type Setter, useSetAtom } from "jotai";
6
+ import { atom, type Getter, useSetAtom } from "jotai";
7
7
  import { useMemo } from "react";
8
8
  import { scrollAndHighlightCell } from "@/components/editor/links/cell-link";
9
- import type { DocumentModel, EditableDocument } from "@/core/notebook/document";
9
+ import type { EditableDocument } from "@/core/notebook/document";
10
10
  import type { CellColumnId } from "@/utils/id-tree";
11
11
  import { Logger } from "@/utils/Logger";
12
+ import { clamp } from "@/utils/math";
12
13
  import type { SerializedEditorState } from "../codemirror/types";
14
+ import { canCollapseOutline } from "../dom/outline";
13
15
  import type { CellConfig } from "../network/types";
14
16
  import { store } from "../state/jotai";
15
- import { activeDocumentModelAtom } from "../session/active-notebook";
17
+ import {
18
+ dropCellOverCellAtom,
19
+ dropCellOverColumnAtom,
20
+ moveCellAtom,
21
+ moveCellsRelativeToAtom,
22
+ moveCellToIndexAtom,
23
+ placeCells,
24
+ sendToBottomAtom,
25
+ sendToTopAtom,
26
+ } from "./arrange";
16
27
  import {
17
28
  type DeletedCell,
18
29
  notebookAtom,
19
- notebookReducer,
20
30
  releaseCellAtoms,
21
31
  useReducerActions,
22
32
  } from "./cells";
23
33
  import { resolveColumns } from "./columns";
34
+ import { documentOf, focusWhenMounted, modelOf } from "./edit";
35
+ import {
36
+ isCodeHidden,
37
+ restoredEditorStatesAtom,
38
+ shownHiddenCodeAtom,
39
+ } from "./editor";
24
40
  import { CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "./ids";
25
- import { focusAndScrollCellIntoView } from "./scrollCellIntoView";
41
+ import {
42
+ collapsedAtom,
43
+ columnOf,
44
+ foldOf,
45
+ foldsAfter,
46
+ foldsHiding,
47
+ layoutAtom,
48
+ } from "./layout";
49
+ import {
50
+ focusAndScrollCellIntoView,
51
+ scrollToBottom,
52
+ scrollToTop,
53
+ } from "./scrollCellIntoView";
26
54
 
27
55
  export interface CreateCellOptions {
28
56
  /** Place the cell after this one. */
@@ -45,22 +73,6 @@ export interface CreateCellOptions {
45
73
 
46
74
  export type CreateCell = (options?: CreateCellOptions) => CellId | null;
47
75
 
48
- function modelOf(get: Getter): DocumentModel {
49
- const model = get(activeDocumentModelAtom);
50
- if (!model) {
51
- throw new Error("The notebook has no document to edit");
52
- }
53
- return model;
54
- }
55
-
56
- function documentOf(get: Getter): EditableDocument {
57
- const document = get(modelOf(get).documentAtom);
58
- if (!document) {
59
- throw new Error("The notebook has no document to edit");
60
- }
61
- return document;
62
- }
63
-
64
76
  function hasCellWithCode(document: EditableDocument, code: string): boolean {
65
77
  for (const cell of document.cells.values()) {
66
78
  if (cell.code === code) {
@@ -70,11 +82,6 @@ function hasCellWithCode(document: EditableDocument, code: string): boolean {
70
82
  return false;
71
83
  }
72
84
 
73
- /** Scroll to and focus the cell once it mounts. */
74
- function focusWhenMounted(set: Setter, cellId: CellId): void {
75
- set(notebookAtom, (state) => ({ ...state, scrollKey: cellId }));
76
- }
77
-
78
85
  /** Create a cell. Returns its id, or null when it was skipped. */
79
86
  export const createCellAtom = atom(
80
87
  null,
@@ -91,12 +98,19 @@ export const createCellAtom = atom(
91
98
  // column, so a cell placed first in a later column names its column.
92
99
  let placement: { after?: CellId; before?: CellId } = {};
93
100
  let column: number | null = null;
101
+ // The cell the new one comes right after, whose folds would hide it.
102
+ let follows: CellId | undefined;
103
+ const layout = get(layoutAtom);
94
104
  if (options.after) {
95
- placement = { after: options.after };
105
+ // A folded header hides the cells after it, so a cell placed after
106
+ // the header goes after its fold.
107
+ follows = foldOf(layout, options.after)?.at(-1) ?? options.after;
108
+ placement = { after: follows };
96
109
  } else if (options.before) {
97
110
  const tree = cellIds.findWithId(options.before);
98
111
  const index = cellIds.indexOf(tree);
99
112
  placement = { before: options.before };
113
+ follows = tree.before(options.before);
100
114
  if (index > 0 && tree.indexOfOrThrow(options.before) === 0) {
101
115
  column = index;
102
116
  }
@@ -104,7 +118,8 @@ export const createCellAtom = atom(
104
118
  const tree =
105
119
  (options.endOf && cellIds.get(options.endOf)) ?? cellIds.atOrThrow(0);
106
120
  if (tree.length > 0) {
107
- placement = { after: tree.last() };
121
+ follows = tree.last();
122
+ placement = { after: follows };
108
123
  } else if (cellIds.colLength > 1) {
109
124
  column = cellIds.indexOf(tree);
110
125
  }
@@ -120,13 +135,22 @@ export const createCellAtom = atom(
120
135
  }
121
136
  });
122
137
  if (focus) {
138
+ // The new cell has no heading, so every section that runs past the
139
+ // cell before it holds it too. Those open, so the cell is on the page.
140
+ const hiding = follows === undefined ? [] : foldsAfter(layout, follows);
141
+ if (hiding.length > 0) {
142
+ set(collapsedAtom, (collapsed) => {
143
+ const next = new Set(collapsed);
144
+ for (const header of hiding) {
145
+ next.delete(header);
146
+ }
147
+ return next;
148
+ });
149
+ }
123
150
  focusWhenMounted(set, id);
124
- // Hidden code stays visible until the cell is blurred for the first time.
151
+ // Hidden code stays showing until the cell is blurred for the first time.
125
152
  if (config.hide_code) {
126
- set(notebookAtom, (state) => ({
127
- ...state,
128
- untouchedNewCells: new Set([...state.untouchedNewCells, id]),
129
- }));
153
+ set(shownHiddenCodeAtom, (cells) => new Set([...cells, id]));
130
154
  }
131
155
  }
132
156
  return id;
@@ -151,26 +175,140 @@ export const moveToNextCellAtom = atom(
151
175
  if (cellId === SCRATCH_CELL_ID) {
152
176
  return;
153
177
  }
154
- const notebook = get(notebookAtom);
155
- const column = notebook.cellIds.findWithId(cellId);
156
- const index = column.indexOfOrThrow(cellId);
178
+ const { visible } = columnOf(get(layoutAtom), cellId);
179
+ const index = visible.indexOf(cellId);
157
180
  const nextIndex = before ? index - 1 : index + 1;
158
- if (nextIndex < 0 || nextIndex >= column.length) {
181
+ if (index === -1 || nextIndex < 0 || nextIndex >= visible.length) {
159
182
  if (create) {
160
183
  set(createCellAtom, before ? { before: cellId } : { after: cellId });
161
184
  }
162
185
  return;
163
186
  }
164
- const nextCellId = column.atOrThrow(nextIndex);
165
- const cell = notebook.cellData[nextCellId];
166
- focusAndScrollCellIntoView({
167
- cellId: nextCellId,
168
- cell: notebook.cellHandles[nextCellId],
169
- isCodeHidden:
170
- Boolean(cell.config.hide_code) &&
171
- !notebook.untouchedNewCells.has(nextCellId),
172
- codeFocus: before ? "bottom" : "top",
173
- variableName: undefined,
187
+ focusOnPage(get, visible[nextIndex], before ? "bottom" : "top");
188
+ },
189
+ );
190
+
191
+ /** Focus a cell on the page, scrolling to it. */
192
+ function focusOnPage(
193
+ get: Getter,
194
+ cellId: CellId,
195
+ codeFocus: "top" | "bottom" | undefined,
196
+ ): void {
197
+ const notebook = get(notebookAtom);
198
+ focusAndScrollCellIntoView({
199
+ cellId,
200
+ cell: notebook.cellHandles[cellId],
201
+ isCodeHidden: isCodeHidden(
202
+ notebook.cellData[cellId]?.config.hide_code,
203
+ get(shownHiddenCodeAtom),
204
+ cellId,
205
+ ),
206
+ codeFocus,
207
+ variableName: undefined,
208
+ });
209
+ }
210
+
211
+ /** Focus the visible cell before, after, or at `cellId` within its column. */
212
+ export const focusCellAtom = atom(
213
+ null,
214
+ (
215
+ get,
216
+ _set,
217
+ { cellId, where }: { cellId: CellId; where: "before" | "after" | "exact" },
218
+ ): void => {
219
+ const { visible } = columnOf(get(layoutAtom), cellId);
220
+ const index = visible.indexOf(cellId);
221
+ if (index === -1) {
222
+ return;
223
+ }
224
+ const offset = where === "before" ? -1 : where === "after" ? 1 : 0;
225
+ const target = clamp(index + offset, 0, visible.length - 1);
226
+ focusOnPage(get, visible[target], where === "after" ? "top" : "bottom");
227
+ },
228
+ );
229
+
230
+ /** Focus the first visible cell of the first column. */
231
+ export const focusTopCellAtom = atom(null, (get): void => {
232
+ const first = get(layoutAtom).columns[0]?.visible[0];
233
+ if (first === undefined) {
234
+ return;
235
+ }
236
+ focusOnPage(get, first, undefined);
237
+ scrollToTop();
238
+ });
239
+
240
+ /** Focus the last visible cell of the last column. */
241
+ export const focusBottomCellAtom = atom(null, (get): void => {
242
+ const last = get(layoutAtom).columns.at(-1)?.visible.at(-1);
243
+ if (last === undefined) {
244
+ return;
245
+ }
246
+ focusOnPage(get, last, undefined);
247
+ scrollToBottom();
248
+ });
249
+
250
+ // Folding ---------------------------------------------------------------------
251
+ //
252
+ // A folded header hides the cells of its section. Which headers are folded is
253
+ // editor state; the section each one hides derives from the outlines.
254
+
255
+ /** Fold the section under `cellId`, if its output has a heading. */
256
+ export const collapseCellAtom = atom(
257
+ null,
258
+ (get, set, { cellId }: { cellId: CellId }): void => {
259
+ if (!canCollapseOutline(get(notebookAtom).cellRuntime[cellId]?.outline)) {
260
+ return;
261
+ }
262
+ set(collapsedAtom, (collapsed) => new Set([...collapsed, cellId]));
263
+ focusWhenMounted(set, cellId);
264
+ },
265
+ );
266
+
267
+ /** Unfold the section under `cellId`. */
268
+ export const expandCellAtom = atom(
269
+ null,
270
+ (_get, set, { cellId }: { cellId: CellId }): void => {
271
+ set(collapsedAtom, (collapsed) => {
272
+ const next = new Set(collapsed);
273
+ next.delete(cellId);
274
+ return next;
275
+ });
276
+ focusWhenMounted(set, cellId);
277
+ },
278
+ );
279
+
280
+ /** Fold every section in the notebook. */
281
+ export const collapseAllCellsAtom = atom(null, (get, set): void => {
282
+ const { cellIds, cellRuntime } = get(notebookAtom);
283
+ set(
284
+ collapsedAtom,
285
+ new Set(
286
+ cellIds.inOrderIds.filter((id) =>
287
+ canCollapseOutline(cellRuntime[id]?.outline),
288
+ ),
289
+ ),
290
+ );
291
+ });
292
+
293
+ /** Unfold every section. */
294
+ export const expandAllCellsAtom = atom(null, (_get, set): void => {
295
+ set(collapsedAtom, new Set());
296
+ });
297
+
298
+ /** Unfold whatever hides `cellId`, so it is on the page. */
299
+ export const showCellIfHiddenAtom = atom(
300
+ null,
301
+ (get, set, { cellId }: { cellId: CellId }): void => {
302
+ const hiding = foldsHiding(get(layoutAtom), cellId);
303
+ if (hiding.length === 0) {
304
+ return;
305
+ }
306
+ set(collapsedAtom, (collapsed) => {
307
+ const next = new Set(collapsed);
308
+ for (const header of hiding) {
309
+ next.delete(header);
310
+ }
311
+ return next;
174
312
  });
175
313
  },
176
314
  );
@@ -247,9 +385,10 @@ export const deleteCellsAtom = atom(
247
385
  };
248
386
  });
249
387
 
250
- // Focus the nearest survivor in the column, looking up before down.
388
+ // Focus the nearest visible survivor in the column, looking up before down.
251
389
  const first = deleting[0].id as CellId;
252
- const ids = notebook.cellIds.findWithId(first).inOrderIds;
390
+ const column = columnOf(get(layoutAtom), first);
391
+ const ids = column.visible.includes(first) ? column.visible : column.ids;
253
392
  const at = ids.indexOf(first);
254
393
  const scrollKey =
255
394
  ids.slice(0, at).findLast((id) => !gone.has(id)) ??
@@ -285,12 +424,19 @@ export const deleteCellsAtom = atom(
285
424
  for (const id of gone) {
286
425
  releaseCellAtoms(id);
287
426
  }
427
+ set(shownHiddenCodeAtom, (cells) =>
428
+ cells.size === 0
429
+ ? cells
430
+ : new Set([...cells].filter((id) => !gone.has(id))),
431
+ );
432
+ set(restoredEditorStatesAtom, (states) =>
433
+ [...states.keys()].some((id) => gone.has(id))
434
+ ? new Map([...states].filter(([id]) => !gone.has(id)))
435
+ : states,
436
+ );
288
437
  set(notebookAtom, (state) => ({
289
438
  ...state,
290
439
  history: [...state.history, { type: "delete", cells }],
291
- untouchedNewCells: new Set(
292
- [...state.untouchedNewCells].filter((id) => !gone.has(id)),
293
- ),
294
440
  scrollKey,
295
441
  }));
296
442
  return true;
@@ -304,12 +450,12 @@ export const undoDeleteCellAtom = atom(null, (get, set): void => {
304
450
  return;
305
451
  }
306
452
  if (last.type === "move") {
307
- set(notebookAtom, (state) =>
308
- notebookReducer(
309
- { ...state, history: state.history.slice(0, -1) },
310
- { type: "undoMoveCells", payload: last },
311
- ),
312
- );
453
+ set(notebookAtom, (state) => ({
454
+ ...state,
455
+ history: state.history.slice(0, -1),
456
+ }));
457
+ placeCells(get, set, last.cellIds, last.placements);
458
+ focusWhenMounted(set, last.cellIds[0]);
313
459
  return;
314
460
  }
315
461
  const model = modelOf(get);
@@ -328,32 +474,21 @@ export const undoDeleteCellAtom = atom(null, (get, set): void => {
328
474
  restored.add(cell.id);
329
475
  }
330
476
  });
331
- // Subscribers hear of the edit after the outermost set, so the notebook
332
- // has not followed the document yet. Catch it up to reach the new cells.
333
- const document = get(model.documentAtom);
334
- if (document !== null) {
335
- set(notebookAtom, (state) =>
336
- notebookReducer(state, { type: "reconcileDocument", payload: document }),
337
- );
338
- }
339
- set(notebookAtom, (state) => {
340
- let { cellData } = state;
477
+ // The editors take their history back when they mount.
478
+ set(restoredEditorStatesAtom, (states) => {
479
+ const next = new Map(states);
341
480
  for (const { cell, serializedEditorState } of last.cells) {
342
- const id = cell.id as CellId;
343
- if (serializedEditorState !== null && cellData[id] !== undefined) {
344
- cellData = {
345
- ...cellData,
346
- [id]: { ...cellData[id], serializedEditorState },
347
- };
481
+ if (serializedEditorState !== null) {
482
+ next.set(cell.id as CellId, serializedEditorState);
348
483
  }
349
484
  }
350
- return {
351
- ...state,
352
- cellData,
353
- history: state.history.slice(0, -1),
354
- scrollKey: last.cells[0].cell.id as CellId,
355
- };
485
+ return next;
356
486
  });
487
+ set(notebookAtom, (state) => ({
488
+ ...state,
489
+ history: state.history.slice(0, -1),
490
+ scrollKey: last.cells[0].cell.id as CellId,
491
+ }));
357
492
  });
358
493
 
359
494
  /**
@@ -378,18 +513,48 @@ export function useCellActions() {
378
513
  const addSetupCell = useSetAtom(addSetupCellAtom);
379
514
  const deleteCells = useSetAtom(deleteCellsAtom);
380
515
  const undoDeleteCell = useSetAtom(undoDeleteCellAtom);
516
+ const focusCell = useSetAtom(focusCellAtom);
517
+ const focusTopCell = useSetAtom(focusTopCellAtom);
518
+ const focusBottomCell = useSetAtom(focusBottomCellAtom);
519
+ const collapseCell = useSetAtom(collapseCellAtom);
520
+ const expandCell = useSetAtom(expandCellAtom);
521
+ const collapseAllCells = useSetAtom(collapseAllCellsAtom);
522
+ const expandAllCells = useSetAtom(expandAllCellsAtom);
523
+ const showCellIfHidden = useSetAtom(showCellIfHiddenAtom);
524
+ const moveCell = useSetAtom(moveCellAtom);
525
+ const moveCellToIndex = useSetAtom(moveCellToIndexAtom);
526
+ const moveCellsRelativeTo = useSetAtom(moveCellsRelativeToAtom);
527
+ const dropCellOverCell = useSetAtom(dropCellOverCellAtom);
528
+ const dropCellOverColumn = useSetAtom(dropCellOverColumnAtom);
529
+ const sendToTop = useSetAtom(sendToTopAtom);
530
+ const sendToBottom = useSetAtom(sendToBottomAtom);
381
531
  return useMemo(
382
532
  () => ({
383
533
  ...operations,
534
+ moveCell,
535
+ moveCellToIndex,
536
+ moveCellsRelativeTo,
537
+ dropCellOverCell,
538
+ dropCellOverColumn,
539
+ sendToTop,
540
+ sendToBottom,
384
541
  createCell,
385
542
  moveToNextCell,
386
543
  addSetupCell,
387
544
  deleteCells,
388
545
  undoDeleteCell,
546
+ focusCell,
547
+ focusTopCell,
548
+ focusBottomCell,
549
+ collapseCell,
550
+ expandCell,
551
+ collapseAllCells,
552
+ expandAllCells,
553
+ showCellIfHidden,
389
554
  }),
390
- // The reducer's actions are stable for the store's lifetime.
555
+ // The reducer's actions and the atom setters are stable for the store's lifetime.
391
556
  // oxlint-disable-next-line react-hooks/exhaustive-deps
392
- [createCell, moveToNextCell, addSetupCell, deleteCells, undoDeleteCell],
557
+ [],
393
558
  );
394
559
  }
395
560