@marimo-team/islands 0.25.3-dev169 → 0.25.3-dev170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-BbDrlHvG.js → ConnectedDataExplorerComponent-BbSXV-Xa.js} +8 -8
- package/dist/{ErrorBoundary-RaIwGWT3.js → ErrorBoundary-NE86qEWZ.js} +4 -4
- package/dist/{abnfDiagram-VCTEODGH-Dr-fo8_z.js → abnfDiagram-VCTEODGH-D7n07nXf.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-ByKIl9GG.js → architectureDiagram-5GKGNRK7-D6CjRib5.js} +1 -1
- package/dist/{chat-ui-BsV2WH-4.js → chat-ui-CC7GekpG.js} +34 -34
- package/dist/{chunk-IMKFNOWR-IKDP18MQ.js → chunk-IMKFNOWR-6dtPz7uS.js} +1 -1
- package/dist/{chunk-SHT3W25Y-LkIgegwh.js → chunk-SHT3W25Y-BwnFNKDA.js} +1 -1
- package/dist/{chunk-SVP7TREG-DReBYtnB.js → chunk-SVP7TREG-D2eV4vBJ.js} +1 -1
- package/dist/{chunk-TICWLB2K-DQ0A0-1D.js → chunk-TICWLB2K-BDs4-_-r.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-CwvXdCNE.js → classDiagram-ZZMXUADV-DCJnf_8v.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-D0XAxDsc.js → classDiagram-v2-VYDZK3BY-BOmSzPBD.js} +2 -2
- package/dist/{code-visibility-BMi0KLAQ.js → code-visibility-Bzvc6ajl.js} +23 -23
- package/dist/{common-BMkJrZsD.js → common-Ciw4zkh2.js} +3412 -3412
- package/dist/{cynefinDiagram-5FMLGOSQ-BFN68nHr.js → cynefinDiagram-5FMLGOSQ-Bln_Soky.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CZt4rtLq.js → dagre-GXQ25YYZ-D-nf7WzW.js} +1 -1
- package/dist/{data-grid-overlay-editor-CCc1P42P.js → data-grid-overlay-editor-AQREmxI7.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-D5cOz47_.js → diagram-S7CK7UJ4-PsgSMkY2.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-Qgdppb4a.js → diagram-UQ7AKVKN-DfwfUuSw.js} +1 -1
- package/dist/{diagram-VX7I27RA-BsDCvily.js → diagram-VX7I27RA-C9_e-oMc.js} +1 -1
- package/dist/{diagram-Z3DM3KII-igTkGjKI.js → diagram-Z3DM3KII-CWLa5tuY.js} +1 -1
- package/dist/{dropdown-menu-BWnnAstC.js → dropdown-menu-B6ftyzOg.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-BHuZGjIr.js → ebnfDiagram-PWID7BFC-B1QRM58v.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DKObXTVE.js → erDiagram-RLTQ6QDP-DJK_FqvW.js} +1 -1
- package/dist/{error-entries-BpUJSLUZ.js → error-entries-DgK3wUJR.js} +5640 -5591
- package/dist/{feedback-button-CMVkEos_.js → feedback-button-8a6SETmT.js} +24 -24
- package/dist/{flowDiagram-HODETNUW-DZI_kit0.js → flowDiagram-HODETNUW-Uxc25XSJ.js} +2 -2
- package/dist/{formats-CwJHWasD.js → formats-DQCyPd5Z.js} +1 -1
- package/dist/{glide-data-editor-BA2pcHSV.js → glide-data-editor-B70oke_v.js} +5 -5
- package/dist/{globals-B5jjJ0ms.js → globals-Cxd5hNlI.js} +2 -2
- package/dist/{html-to-image-CQr2-IDA.js → html-to-image-D4yG5Ru0.js} +25 -25
- package/dist/{infoDiagram-27XIBGKW-DdPl-7-n.js → infoDiagram-27XIBGKW-DmjxqwL0.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DbyW6l59.js → ishikawaDiagram-5VMMS53U-NgCLHqQi.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BMX9SyBS.js → kanban-definition-UXKFOSKX-CZaG-iam.js} +1 -1
- package/dist/main.js +187 -188
- package/dist/{mermaid-C8W8mTbA.js → mermaid-CSvw7c71.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX-Zd_tAxWL.js → mindmap-definition-YA3MSWOX-DJYQBDH_.js} +1 -1
- package/dist/{number-overlay-editor-BIWiV-TU.js → number-overlay-editor-ChK-SKUk.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-DjYydPdo.js → pegDiagram-XKGWAZYB-CHKO_dAe.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-R5LKjO2n.js → pieDiagram-E7YTZNPT-DVevbMAo.js} +1 -1
- package/dist/{process-output-Uw-VJE5V.js → process-output-DxvBAeL3.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-C7uNKtfm.js → railroadDiagram-O6MQD6OU-Detj1kcB.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CxAIzQfl.js → requirementDiagram-BXWQKSXE-BLFD0T_Z.js} +1 -1
- package/dist/{reveal-component-XiecZ3Ua.js → reveal-component-CXMXeDZD.js} +6 -6
- package/dist/{sequenceDiagram-WJ2MYXX4-B5tmlPd5.js → sequenceDiagram-WJ2MYXX4-BZBgatQV.js} +1 -1
- package/dist/{spec-45FnujvG.js → spec-Mnkn3XEr.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-C1hc5H7Y.js → stateDiagram-D77RDMKH-4BZW9-6_.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-DRWeqQTn.js → stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-NTLas--s.js → swimlanes-42K2YHIH-SL5yeGe2.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-ChPggljD.js → swimlanesDiagram-VR7AAH4N-DqPmAYUv.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B4CZ9srJ.js → timeline-definition-24CTP7MA-BfXUMWkq.js} +1 -1
- package/dist/{toDate-rySyYxqk.js → toDate-TMubKIVL.js} +22 -22
- package/dist/{useAsyncData-B7MiltyV.js → useAsyncData-DjMGFzFy.js} +1 -1
- package/dist/{useDeepCompareMemoize-DZmHBD_1.js → useDeepCompareMemoize-BQR5TJot.js} +1 -1
- package/dist/{useLifecycle-DRbRhzxg.js → useLifecycle-C_GvKWbQ.js} +26 -23
- package/dist/{useTheme-TLkqJsvo.js → useTheme-Bns2gJZ6.js} +179 -178
- package/dist/{vega-component-DC6P127L.js → vega-component-BsVptuhz.js} +5 -5
- package/dist/{vennDiagram-4TSXK5OY-DJIvMt44.js → vennDiagram-4TSXK5OY-CH65znGa.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-45H0zg0u.js → wardleyDiagram-VM6X3IG4-z9pWKb2_.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-ZT-XzcPN.js → xychartDiagram-S5SC5T6Z-MxyaWjjq.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +0 -1
- package/src/components/editor/cell/StagedAICell.tsx +10 -2
- package/src/components/editor/cell/code/cell-editor.tsx +7 -14
- package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
- package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/core/ai/__tests__/staged-cells.test.ts +8 -5
- package/src/core/ai/staged-cells.ts +7 -2
- package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +3 -15
- package/src/core/ai/tools/edit-notebook-tool.ts +14 -3
- package/src/core/cells/__tests__/cells.test.ts +0 -297
- package/src/core/cells/__tests__/document.test.ts +410 -0
- package/src/core/cells/__tests__/flush-document.test.ts +40 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +1 -6
- package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
- package/src/core/cells/cells.ts +27 -158
- package/src/core/cells/columns.ts +38 -0
- package/src/core/cells/document.ts +360 -0
- package/src/core/cells/types.ts +12 -0
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +27 -1
- package/src/core/codemirror/cells/extensions.ts +14 -0
- package/src/core/codemirror/format.ts +6 -5
- package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
- package/src/core/codemirror/language/cell-code.ts +114 -0
- package/src/core/codemirror/language/extension.ts +47 -30
- package/src/core/codemirror/language/languages/sql/sql.ts +7 -17
- package/src/core/codemirror/language/utils.ts +9 -39
- package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
- package/src/core/codemirror/rtc/extension.ts +0 -1
- package/src/core/edit-app.tsx +4 -3
- package/src/core/export/layout.ts +2 -2
- package/src/core/islands/bridge.ts +0 -1
- package/src/core/network/requests-lazy.ts +0 -1
- package/src/core/network/requests-network.ts +0 -9
- package/src/core/network/requests-static.ts +0 -1
- package/src/core/network/requests-toasting.tsx +0 -1
- package/src/core/network/types.ts +0 -5
- package/src/core/notebook/document/__tests__/codemirror.test.ts +76 -14
- package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
- package/src/core/notebook/document/__tests__/hooks.test.tsx +3 -3
- package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +3 -3
- package/src/core/notebook/document/__tests__/model.test.ts +71 -15
- package/src/core/notebook/document/__tests__/scenario-fixture.ts +1 -1
- package/src/core/notebook/document/codemirror.ts +27 -122
- package/src/core/notebook/document/hooks.tsx +2 -2
- package/src/core/notebook/document/index.ts +9 -1
- package/src/core/notebook/document/model.ts +74 -24
- package/src/core/notebook/document/sync.ts +53 -4
- package/src/core/run-app.tsx +44 -41
- package/src/core/saving/save-component.tsx +2 -5
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +63 -3
- package/src/core/session/active-notebook.ts +4 -0
- package/src/core/session/useNotebookSession.tsx +39 -19
- package/src/core/wasm/__tests__/bridge.test.ts +3 -3
- package/src/core/wasm/bridge.ts +5 -12
- package/src/core/wasm/worker/worker.ts +0 -9
- package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +28 -0
- package/src/utils/createReducer.ts +42 -21
- package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
- package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
- package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
- package/src/core/cells/document-changes.ts +0 -944
package/src/core/cells/cells.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { historyField } from "@codemirror/commands";
|
|
4
|
-
import { dequal as isEqual } from "dequal";
|
|
5
4
|
import { type Atom, atom, useAtom, useAtomValue } from "jotai";
|
|
6
5
|
import { atomFamily, selectAtom, splitAtom } from "jotai/utils";
|
|
7
6
|
import { createRef, type ReducerWithoutAction } from "react";
|
|
8
7
|
import type { CellHandle } from "@/components/editor/notebook-cell";
|
|
8
|
+
import type { EditableDocument } from "@/core/notebook/document";
|
|
9
9
|
import type { CollapsibleTree } from "@/utils/id-tree";
|
|
10
10
|
import {
|
|
11
11
|
type CellColumnId,
|
|
@@ -20,20 +20,17 @@ import { extractAllTracebackInfo, type TracebackInfo } from "@/utils/traceback";
|
|
|
20
20
|
import { createReducerAndAtoms } from "../../utils/createReducer";
|
|
21
21
|
import { foldAllBulk, unfoldAllBulk } from "../codemirror/editing/commands";
|
|
22
22
|
import { editorMountScheduler } from "../codemirror/editor-mount-scheduler";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
updateEditorCodeFromPython,
|
|
26
|
-
} from "../codemirror/language/utils";
|
|
23
|
+
import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
|
|
24
|
+
import { splitEditor } from "../codemirror/language/utils";
|
|
27
25
|
import type { SerializedEditorState } from "../codemirror/types";
|
|
28
26
|
import { findCollapseRange, mergeOutlines } from "../dom/outline";
|
|
29
27
|
import type { CellMessage } from "../kernel/messages";
|
|
30
28
|
import { isErrorMime } from "../mime";
|
|
31
29
|
import type { CellConfig } from "../network/types";
|
|
32
|
-
import { canUseRtc, isRtcEnabled } from "../rtc/state";
|
|
33
30
|
import { createDeepEqualAtom, store } from "../state/jotai";
|
|
34
31
|
import { isWasm } from "../wasm/utils";
|
|
35
32
|
import { prepareCellForExecution, transitionCell } from "./cell";
|
|
36
|
-
import {
|
|
33
|
+
import { applyDocument, documentEditMiddleware } from "./document";
|
|
37
34
|
import { CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "./ids";
|
|
38
35
|
import { type CellLog, getCellLogsForMessage } from "./logs";
|
|
39
36
|
import {
|
|
@@ -939,158 +936,19 @@ const {
|
|
|
939
936
|
cellLogs: [...state.cellLogs, ...action.logs],
|
|
940
937
|
};
|
|
941
938
|
},
|
|
942
|
-
setCellIds: (state, action: { cellIds: CellId[] }) => {
|
|
943
|
-
const isTheSame = isEqual(state.cellIds.inOrderIds, action.cellIds);
|
|
944
|
-
if (isTheSame) {
|
|
945
|
-
return state;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Create new cell data and runtime states for the new cell IDs
|
|
949
|
-
const nextCellData = { ...state.cellData };
|
|
950
|
-
const nextCellRuntime = { ...state.cellRuntime };
|
|
951
|
-
const nextCellHandles = { ...state.cellHandles };
|
|
952
|
-
|
|
953
|
-
for (const cellId of action.cellIds) {
|
|
954
|
-
if (!(cellId in state.cellData)) {
|
|
955
|
-
nextCellData[cellId] = createCell({ id: cellId });
|
|
956
|
-
}
|
|
957
|
-
if (!(cellId in state.cellRuntime)) {
|
|
958
|
-
nextCellRuntime[cellId] = createCellRuntimeState();
|
|
959
|
-
}
|
|
960
|
-
if (!(cellId in state.cellHandles)) {
|
|
961
|
-
nextCellHandles[cellId] = createRef();
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
return {
|
|
966
|
-
...state,
|
|
967
|
-
cellIds: MultiColumn.fromWithPreviousShape(action.cellIds, state.cellIds),
|
|
968
|
-
cellData: nextCellData,
|
|
969
|
-
cellRuntime: nextCellRuntime,
|
|
970
|
-
cellHandles: nextCellHandles,
|
|
971
|
-
};
|
|
972
|
-
},
|
|
973
939
|
/**
|
|
974
|
-
*
|
|
940
|
+
* Follow the host document's cells and order.
|
|
975
941
|
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
* `config.column == null` inherit the column of the previous cell in the
|
|
979
|
-
* given order (see `MultiColumn.fromIdsAndColumns`), which lets the server
|
|
980
|
-
* send column changes only at column boundaries.
|
|
942
|
+
* Dispatched without middleware, since the document already has these
|
|
943
|
+
* changes.
|
|
981
944
|
*/
|
|
982
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
]),
|
|
988
|
-
);
|
|
989
|
-
return { ...state, cellIds: newCellIds };
|
|
990
|
-
},
|
|
991
|
-
setCellCodes: (
|
|
992
|
-
state,
|
|
993
|
-
action: {
|
|
994
|
-
codes: string[];
|
|
995
|
-
ids: CellId[];
|
|
996
|
-
codeIsStale: boolean;
|
|
997
|
-
names?: string[];
|
|
998
|
-
configs?: CellConfig[];
|
|
999
|
-
},
|
|
1000
|
-
) => {
|
|
1001
|
-
invariant(
|
|
1002
|
-
action.codes.length === action.ids.length,
|
|
1003
|
-
"Expected codes and ids to have the same length",
|
|
1004
|
-
);
|
|
1005
|
-
|
|
1006
|
-
let nextState = { ...state };
|
|
1007
|
-
|
|
1008
|
-
const cellReducer = ({
|
|
1009
|
-
cell,
|
|
1010
|
-
code,
|
|
1011
|
-
cellId,
|
|
1012
|
-
name,
|
|
1013
|
-
config,
|
|
1014
|
-
}: {
|
|
1015
|
-
cell: CellData | undefined;
|
|
1016
|
-
code: string;
|
|
1017
|
-
cellId: CellId;
|
|
1018
|
-
name?: string;
|
|
1019
|
-
config?: CellConfig;
|
|
1020
|
-
}) => {
|
|
1021
|
-
if (!cell) {
|
|
1022
|
-
return createCell({
|
|
1023
|
-
id: cellId,
|
|
1024
|
-
code,
|
|
1025
|
-
name: name,
|
|
1026
|
-
config: config,
|
|
1027
|
-
lastCodeRun: action.codeIsStale ? null : code,
|
|
1028
|
-
edited: action.codeIsStale && code.trim().length > 0,
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// If code is stale, we don't promote it to lastCodeRun
|
|
1033
|
-
const lastCodeRun = action.codeIsStale ? cell.lastCodeRun : code;
|
|
1034
|
-
|
|
1035
|
-
// Mark as edited if the code has changed
|
|
1036
|
-
const edited = lastCodeRun
|
|
1037
|
-
? lastCodeRun.trim() !== code.trim()
|
|
1038
|
-
: Boolean(code);
|
|
1039
|
-
|
|
1040
|
-
// No change
|
|
1041
|
-
if (cell.code.trim() === code.trim()) {
|
|
1042
|
-
return {
|
|
1043
|
-
...cell,
|
|
1044
|
-
code: code,
|
|
1045
|
-
edited,
|
|
1046
|
-
lastCodeRun,
|
|
1047
|
-
...(name !== undefined && { name }),
|
|
1048
|
-
...(config !== undefined && { config }),
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Update codemirror if mounted. RTC-backed cells sync via Loro; others
|
|
1053
|
-
// (e.g. scratchpad) still need an explicit editor update.
|
|
1054
|
-
if (!isRtcEnabled() || !canUseRtc(cellId)) {
|
|
1055
|
-
const cellHandle = nextState.cellHandles[cellId].current;
|
|
1056
|
-
if (cellHandle?.editorViewOrNull) {
|
|
1057
|
-
updateEditorCodeFromPython(cellHandle.editorViewOrNull, code);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return {
|
|
1062
|
-
...cell,
|
|
1063
|
-
code: code,
|
|
1064
|
-
edited,
|
|
1065
|
-
lastCodeRun,
|
|
1066
|
-
...(name !== undefined && { name }),
|
|
1067
|
-
...(config !== undefined && { config }),
|
|
1068
|
-
};
|
|
1069
|
-
};
|
|
1070
|
-
|
|
1071
|
-
for (let i = 0; i < action.ids.length; i++) {
|
|
1072
|
-
const cellId = action.ids[i];
|
|
1073
|
-
const code = action.codes[i];
|
|
1074
|
-
if (cellId === undefined || code === undefined) {
|
|
1075
|
-
continue;
|
|
945
|
+
reconcileDocument: (state, document: EditableDocument) => {
|
|
946
|
+
const nextState = applyDocument(state, document);
|
|
947
|
+
for (const cellId of state.cellIds.inOrderIds) {
|
|
948
|
+
if (!(cellId in nextState.cellData)) {
|
|
949
|
+
releaseCellAtoms(cellId);
|
|
1076
950
|
}
|
|
1077
|
-
const name = action.names?.[i];
|
|
1078
|
-
const config = action.configs?.[i];
|
|
1079
|
-
nextState = {
|
|
1080
|
-
...nextState,
|
|
1081
|
-
cellData: {
|
|
1082
|
-
...nextState.cellData,
|
|
1083
|
-
[cellId]: cellReducer({
|
|
1084
|
-
cell: nextState.cellData[cellId],
|
|
1085
|
-
code,
|
|
1086
|
-
cellId,
|
|
1087
|
-
name,
|
|
1088
|
-
config,
|
|
1089
|
-
}),
|
|
1090
|
-
},
|
|
1091
|
-
};
|
|
1092
951
|
}
|
|
1093
|
-
|
|
1094
952
|
return nextState;
|
|
1095
953
|
},
|
|
1096
954
|
setStdinResponse: (
|
|
@@ -1440,7 +1298,14 @@ const {
|
|
|
1440
1298
|
cellHandle.editorView,
|
|
1441
1299
|
);
|
|
1442
1300
|
|
|
1443
|
-
|
|
1301
|
+
const update = prepareCellCodeUpdate(
|
|
1302
|
+
cellHandle.editorView.state,
|
|
1303
|
+
beforeCursorCode,
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
if (update) {
|
|
1307
|
+
cellHandle.editorView.dispatch(update);
|
|
1308
|
+
}
|
|
1444
1309
|
|
|
1445
1310
|
const newCellId = CellId.create();
|
|
1446
1311
|
|
|
@@ -1488,7 +1353,11 @@ const {
|
|
|
1488
1353
|
return state;
|
|
1489
1354
|
}
|
|
1490
1355
|
|
|
1491
|
-
|
|
1356
|
+
const update = prepareCellCodeUpdate(cellHandle.editorView.state, snapshot);
|
|
1357
|
+
|
|
1358
|
+
if (update) {
|
|
1359
|
+
cellHandle.editorView.dispatch(update);
|
|
1360
|
+
}
|
|
1492
1361
|
|
|
1493
1362
|
return {
|
|
1494
1363
|
...state,
|
|
@@ -1599,9 +1468,9 @@ const {
|
|
|
1599
1468
|
});
|
|
1600
1469
|
|
|
1601
1470
|
// We apply the middleware here (rather than inline in createReducerAndAtoms)
|
|
1602
|
-
// so that the document
|
|
1471
|
+
// so that the document edit middleware can import CellActions and
|
|
1603
1472
|
// strictly type the dispatched actions without creating a circular dependency.
|
|
1604
|
-
addMiddleware(
|
|
1473
|
+
addMiddleware(documentEditMiddleware);
|
|
1605
1474
|
|
|
1606
1475
|
function isCellCodeHidden(state: NotebookState, cellId: CellId): boolean {
|
|
1607
1476
|
return (
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/** How a document's cells fall into columns. */
|
|
4
|
+
|
|
5
|
+
import type { EditableDocument } from "@/core/notebook/document";
|
|
6
|
+
import type { CellId } from "./ids";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Each cell's column: its `column` config, or the column of the cell before
|
|
10
|
+
* it. The first cell starts in column 0.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveColumns(
|
|
13
|
+
document: EditableDocument,
|
|
14
|
+
): Map<CellId, number> {
|
|
15
|
+
const columns = new Map<CellId, number>();
|
|
16
|
+
let current = 0;
|
|
17
|
+
for (const id of document.order) {
|
|
18
|
+
const column = document.cells.get(id)?.config.column;
|
|
19
|
+
if (column != null && column >= 0) {
|
|
20
|
+
current = column;
|
|
21
|
+
}
|
|
22
|
+
// The protocol's ids are plain strings; the notebook brands them.
|
|
23
|
+
columns.set(id as CellId, current);
|
|
24
|
+
}
|
|
25
|
+
return columns;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The cells of each column in order, with empty columns for gaps. */
|
|
29
|
+
export function groupByColumn(document: EditableDocument): CellId[][] {
|
|
30
|
+
const columns: CellId[][] = [[]];
|
|
31
|
+
for (const [id, column] of resolveColumns(document)) {
|
|
32
|
+
while (columns.length <= column) {
|
|
33
|
+
columns.push([]);
|
|
34
|
+
}
|
|
35
|
+
columns[column].push(id);
|
|
36
|
+
}
|
|
37
|
+
return columns;
|
|
38
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keep the notebook state and the host document in step while cells live
|
|
5
|
+
* in both.
|
|
6
|
+
*
|
|
7
|
+
* TODO(MO-8028): Delete this module once components read cells from the
|
|
8
|
+
* document model and `notebookAtom` holds only runtime and editor state.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
12
|
+
import { dequal as isEqual } from "dequal";
|
|
13
|
+
import { createRef, type RefObject } from "react";
|
|
14
|
+
import type { CellHandle } from "@/components/editor/notebook-cell";
|
|
15
|
+
import type {
|
|
16
|
+
DocumentEdit,
|
|
17
|
+
DocumentEditor,
|
|
18
|
+
EditableCell,
|
|
19
|
+
EditableDocument,
|
|
20
|
+
} from "@/core/notebook/document";
|
|
21
|
+
import type { DispatchedActionOf } from "@/utils/createReducer";
|
|
22
|
+
import { CollapsibleTree, MultiColumn } from "@/utils/id-tree";
|
|
23
|
+
import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
|
|
24
|
+
import type { CellConfig } from "../network/types";
|
|
25
|
+
import { canUseRtc, isRtcEnabled } from "../rtc/state";
|
|
26
|
+
import { store } from "../state/jotai";
|
|
27
|
+
import { activeDocumentModelAtom } from "../session/active-notebook";
|
|
28
|
+
import type { CellActions, NotebookState } from "./cells";
|
|
29
|
+
import { groupByColumn } from "./columns";
|
|
30
|
+
import type { CellId } from "./ids";
|
|
31
|
+
import {
|
|
32
|
+
type CellData,
|
|
33
|
+
createCell,
|
|
34
|
+
createCellConfig,
|
|
35
|
+
createCellRuntimeState,
|
|
36
|
+
isEdited,
|
|
37
|
+
} from "./types";
|
|
38
|
+
|
|
39
|
+
type CellAction = DispatchedActionOf<CellActions>;
|
|
40
|
+
|
|
41
|
+
// Following the document ----------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** Rebuild the tree for `columns`, keeping column ids and collapsed sections. */
|
|
44
|
+
function reconcileColumns(
|
|
45
|
+
current: MultiColumn<CellId>,
|
|
46
|
+
columns: CellId[][],
|
|
47
|
+
): MultiColumn<CellId> {
|
|
48
|
+
const previous = current.getColumns();
|
|
49
|
+
// The document cannot hold an empty column, so keep the ones this client has.
|
|
50
|
+
while (previous[columns.length]?.length === 0) {
|
|
51
|
+
columns.push([]);
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
columns.length === previous.length &&
|
|
55
|
+
columns.every((ids, index) => isEqual(ids, previous[index].inOrderIds))
|
|
56
|
+
) {
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
return new MultiColumn(
|
|
60
|
+
columns.map((ids, index) =>
|
|
61
|
+
CollapsibleTree.fromWithPreviousShape(ids, previous[index]),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The cell's data with the document's fields, and its editor brought along. */
|
|
67
|
+
function followCell(
|
|
68
|
+
current: CellData,
|
|
69
|
+
cell: EditableCell,
|
|
70
|
+
handle: RefObject<CellHandle | null> | undefined,
|
|
71
|
+
): CellData {
|
|
72
|
+
// Withheld source keeps the code this client has.
|
|
73
|
+
const code = cell.code ?? current.code;
|
|
74
|
+
const config = isEqual(current.config, cell.config)
|
|
75
|
+
? current.config
|
|
76
|
+
: createCellConfig(cell.config);
|
|
77
|
+
if (
|
|
78
|
+
code === current.code &&
|
|
79
|
+
cell.name === current.name &&
|
|
80
|
+
config === current.config
|
|
81
|
+
) {
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
if (code === current.code) {
|
|
85
|
+
return { ...current, name: cell.name, config };
|
|
86
|
+
}
|
|
87
|
+
// RTC cells follow Loro. Other editors take the host's code; one that
|
|
88
|
+
// shows it already, as the editor this client types in does, is left alone.
|
|
89
|
+
if (!isRtcEnabled() || !canUseRtc(current.id)) {
|
|
90
|
+
const view = handle?.current?.editorViewOrNull;
|
|
91
|
+
const update = view && prepareCellCodeUpdate(view.state, code);
|
|
92
|
+
if (update) {
|
|
93
|
+
view.dispatch(update);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...current,
|
|
98
|
+
code,
|
|
99
|
+
name: cell.name,
|
|
100
|
+
config,
|
|
101
|
+
edited: isEdited(current.lastCodeRun, code),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Make the notebook's cells and order follow the document.
|
|
107
|
+
*
|
|
108
|
+
* Returns the same state when nothing differs, so a local edit that the
|
|
109
|
+
* document already reflects changes nothing.
|
|
110
|
+
*
|
|
111
|
+
* TODO(MO-8028): Delete with `reconcileDocument` once cells are read from
|
|
112
|
+
* the document model.
|
|
113
|
+
*/
|
|
114
|
+
export function applyDocument(
|
|
115
|
+
state: NotebookState,
|
|
116
|
+
document: EditableDocument,
|
|
117
|
+
): NotebookState {
|
|
118
|
+
const cellIds = reconcileColumns(state.cellIds, groupByColumn(document));
|
|
119
|
+
let { cellData, cellRuntime, cellHandles } = state;
|
|
120
|
+
for (const id of cellIds.inOrderIds) {
|
|
121
|
+
const cell = document.cells.get(id);
|
|
122
|
+
const current = state.cellData[id];
|
|
123
|
+
if (cell === undefined) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (current === undefined) {
|
|
127
|
+
// A new cell has no run to be edited from.
|
|
128
|
+
cellData = {
|
|
129
|
+
...cellData,
|
|
130
|
+
[id]: createCell({
|
|
131
|
+
id,
|
|
132
|
+
code: cell.code ?? "",
|
|
133
|
+
name: cell.name,
|
|
134
|
+
config: createCellConfig(cell.config),
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
cellRuntime = { ...cellRuntime, [id]: createCellRuntimeState() };
|
|
138
|
+
cellHandles = { ...cellHandles, [id]: createRef() };
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const next = followCell(current, cell, state.cellHandles[id]);
|
|
142
|
+
if (next !== current) {
|
|
143
|
+
cellData = { ...cellData, [id]: next };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const present = new Set(cellIds.inOrderIds);
|
|
147
|
+
const removed = state.cellIds.inOrderIds.filter((id) => !present.has(id));
|
|
148
|
+
if (removed.length > 0) {
|
|
149
|
+
const gone = new Set(removed);
|
|
150
|
+
const keep = <V>(record: Record<CellId, V>): Record<CellId, V> =>
|
|
151
|
+
Object.fromEntries(
|
|
152
|
+
Object.entries(record).filter(([id]) => !gone.has(id as CellId)),
|
|
153
|
+
) as Record<CellId, V>;
|
|
154
|
+
cellData = keep(cellData);
|
|
155
|
+
cellRuntime = keep(cellRuntime);
|
|
156
|
+
cellHandles = keep(cellHandles);
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
cellIds === state.cellIds &&
|
|
160
|
+
cellData === state.cellData &&
|
|
161
|
+
removed.length === 0
|
|
162
|
+
) {
|
|
163
|
+
return state;
|
|
164
|
+
}
|
|
165
|
+
return { ...state, cellIds, cellData, cellRuntime, cellHandles };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Editing the document ------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const CONFIG_FIELDS = ["disabled", "hide_code", "expand_output"] as const;
|
|
171
|
+
|
|
172
|
+
/** The fields of `after` that differ from `before`, leaving `column` to the tree. */
|
|
173
|
+
function configPatch(
|
|
174
|
+
before: CellConfig,
|
|
175
|
+
after: CellConfig,
|
|
176
|
+
): Partial<Protocol.CellConfig> | null {
|
|
177
|
+
const patch: Partial<Protocol.CellConfig> = {};
|
|
178
|
+
for (const field of CONFIG_FIELDS) {
|
|
179
|
+
if (before[field] !== after[field]) {
|
|
180
|
+
patch[field] = after[field] ?? false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return Object.keys(patch).length === 0 ? null : patch;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The `column` configs that make the document's columns match the tree's.
|
|
188
|
+
*
|
|
189
|
+
* A cell without a column follows the cell before it, so only cells whose
|
|
190
|
+
* resolved column differs from their position need one.
|
|
191
|
+
*/
|
|
192
|
+
function columnFixes(state: NotebookState): Array<[CellId, number | null]> {
|
|
193
|
+
const fixes: Array<[CellId, number | null]> = [];
|
|
194
|
+
const columns = state.cellIds.getColumns();
|
|
195
|
+
if (columns.length === 1) {
|
|
196
|
+
for (const id of columns[0].inOrderIds) {
|
|
197
|
+
const column = state.cellData[id].config.column;
|
|
198
|
+
if (column != null && column !== 0) {
|
|
199
|
+
fixes.push([id, null]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return fixes;
|
|
203
|
+
}
|
|
204
|
+
let inherited = 0;
|
|
205
|
+
columns.forEach((column, target) => {
|
|
206
|
+
for (const id of column.inOrderIds) {
|
|
207
|
+
const configured = state.cellData[id].config.column;
|
|
208
|
+
const resolved =
|
|
209
|
+
configured != null && configured >= 0 ? configured : inherited;
|
|
210
|
+
if (resolved !== target) {
|
|
211
|
+
fixes.push([id, target]);
|
|
212
|
+
}
|
|
213
|
+
inherited = target;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return fixes;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The edit that makes the document match `next`, or null when it already does. */
|
|
220
|
+
function diffNotebook(
|
|
221
|
+
prev: NotebookState,
|
|
222
|
+
next: NotebookState,
|
|
223
|
+
): DocumentEdit | null {
|
|
224
|
+
const steps: Array<(tx: DocumentEditor) => void> = [];
|
|
225
|
+
const prevOrder = prev.cellIds.inOrderIds;
|
|
226
|
+
const nextOrder = next.cellIds.inOrderIds;
|
|
227
|
+
const prevIds = new Set(prevOrder);
|
|
228
|
+
const nextIds = new Set(nextOrder);
|
|
229
|
+
for (const id of prevOrder) {
|
|
230
|
+
if (!nextIds.has(id)) {
|
|
231
|
+
steps.push((tx) => tx.deleteCell(id));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const created: CellId[] = [];
|
|
235
|
+
for (const id of nextOrder) {
|
|
236
|
+
const cell = next.cellData[id];
|
|
237
|
+
const before = prev.cellData[id];
|
|
238
|
+
if (!prevIds.has(id) || before === undefined) {
|
|
239
|
+
created.push(id);
|
|
240
|
+
steps.push((tx) =>
|
|
241
|
+
tx.createCell({
|
|
242
|
+
id,
|
|
243
|
+
code: cell.code,
|
|
244
|
+
name: cell.name,
|
|
245
|
+
config: cell.config,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (before === cell) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (before.code !== cell.code) {
|
|
254
|
+
steps.push((tx) => tx.setCode(id, cell.code));
|
|
255
|
+
}
|
|
256
|
+
if (before.name !== cell.name) {
|
|
257
|
+
steps.push((tx) => tx.setName(id, cell.name));
|
|
258
|
+
}
|
|
259
|
+
const patch = configPatch(before.config, cell.config);
|
|
260
|
+
if (patch !== null) {
|
|
261
|
+
steps.push((tx) => tx.updateConfig(id, patch));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const [id, column] of columnFixes(next)) {
|
|
265
|
+
steps.push((tx) => tx.updateConfig(id, { column }));
|
|
266
|
+
}
|
|
267
|
+
// Created cells go to the end, so the order is set when they belong elsewhere.
|
|
268
|
+
const survivors = prevOrder.filter((id) => nextIds.has(id));
|
|
269
|
+
if (!isEqual(nextOrder, [...survivors, ...created])) {
|
|
270
|
+
steps.push((tx) => tx.setOrder(nextOrder));
|
|
271
|
+
}
|
|
272
|
+
if (steps.length === 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return (tx) => {
|
|
276
|
+
for (const step of steps) {
|
|
277
|
+
step(tx);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Middleware for the notebook reducer. Writes what an action changed about
|
|
284
|
+
* the cells and their order into the document model as one edit.
|
|
285
|
+
*
|
|
286
|
+
* TODO(MO-8028): Each action family that moves onto the model removes its
|
|
287
|
+
* part of `diffNotebook`. The middleware goes with the last one, code.
|
|
288
|
+
*/
|
|
289
|
+
export function documentEditMiddleware(
|
|
290
|
+
prev: NotebookState,
|
|
291
|
+
next: NotebookState,
|
|
292
|
+
action: CellAction,
|
|
293
|
+
): void {
|
|
294
|
+
// The kernel's cells replace the notebook rather than edit it.
|
|
295
|
+
if (action.type === "setCells") {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (prev.cellIds === next.cellIds && prev.cellData === next.cellData) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const model = store.get(activeDocumentModelAtom);
|
|
302
|
+
if (model === null) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const edit = diffNotebook(prev, next);
|
|
306
|
+
if (edit !== null) {
|
|
307
|
+
store.set(model.editAtom, edit);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Commands that read the document on the host -------------------------------
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Send this client's edits to the host before a command that reads them
|
|
315
|
+
* there. Without a document or a backend, as on a static page, there is
|
|
316
|
+
* nothing to send and the command proceeds.
|
|
317
|
+
*/
|
|
318
|
+
export async function flushDocument(): Promise<void> {
|
|
319
|
+
const model = store.get(activeDocumentModelAtom);
|
|
320
|
+
if (
|
|
321
|
+
model === null ||
|
|
322
|
+
store.get(model.syncStatusAtom).status === "unavailable"
|
|
323
|
+
) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
await model.flush();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run a legacy save after this client's edits reach the host, and keep
|
|
331
|
+
* later edits local until it finishes. The save carries the whole
|
|
332
|
+
* notebook and the host takes it as the document, so an edit sent
|
|
333
|
+
* meanwhile would be overwritten. When the edits cannot be sent the save
|
|
334
|
+
* still runs, and the model resumes against the saved document.
|
|
335
|
+
*
|
|
336
|
+
* TODO(MO-8028): Goes with the legacy save, along with the hold.
|
|
337
|
+
*/
|
|
338
|
+
export async function withDocumentSave<T>(save: () => Promise<T>): Promise<T> {
|
|
339
|
+
const model = store.get(activeDocumentModelAtom);
|
|
340
|
+
if (model === null) {
|
|
341
|
+
return save();
|
|
342
|
+
}
|
|
343
|
+
let flushed = true;
|
|
344
|
+
try {
|
|
345
|
+
await model.flush();
|
|
346
|
+
} catch {
|
|
347
|
+
flushed = false;
|
|
348
|
+
}
|
|
349
|
+
const release = model.hold();
|
|
350
|
+
let result: T;
|
|
351
|
+
try {
|
|
352
|
+
result = await save();
|
|
353
|
+
} finally {
|
|
354
|
+
release();
|
|
355
|
+
}
|
|
356
|
+
if (!flushed) {
|
|
357
|
+
void model.retry().catch(() => undefined);
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
package/src/core/cells/types.ts
CHANGED
|
@@ -8,6 +8,18 @@ import type { CellId } from "./ids";
|
|
|
8
8
|
import { DEFAULT_CELL_NAME } from "./names";
|
|
9
9
|
import type { Outline } from "./outline";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Whether a cell's code has changed since it last ran. A cell that never ran
|
|
13
|
+
* is edited once it has code. Whitespace at the ends does not count.
|
|
14
|
+
*
|
|
15
|
+
* TODO(MO-8028): Temporary until staleness comes from the host's executions.
|
|
16
|
+
*/
|
|
17
|
+
export function isEdited(lastCodeRun: string | null, code: string): boolean {
|
|
18
|
+
return lastCodeRun === null
|
|
19
|
+
? code.trim().length > 0
|
|
20
|
+
: lastCodeRun.trim() !== code.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
/**
|
|
12
24
|
* Create a new cell with default state.
|
|
13
25
|
*/
|
|
@@ -11,7 +11,8 @@ import { formattingChangeEffect } from "../../format";
|
|
|
11
11
|
import { loroSyncAnnotation } from "../../rtc/loro/sync";
|
|
12
12
|
import { exportedForTesting } from "../extensions";
|
|
13
13
|
|
|
14
|
-
const { shouldAutorunMarkdownUpdate } =
|
|
14
|
+
const { isCollaboratorUpdate, shouldAutorunMarkdownUpdate } =
|
|
15
|
+
exportedForTesting;
|
|
15
16
|
|
|
16
17
|
function createTransaction(spec: TransactionSpec) {
|
|
17
18
|
return EditorState.create({ doc: "" }).update(spec);
|
|
@@ -125,3 +126,28 @@ describe("shouldAutorunMarkdownUpdate", () => {
|
|
|
125
126
|
).toBe(false);
|
|
126
127
|
});
|
|
127
128
|
});
|
|
129
|
+
|
|
130
|
+
describe("isCollaboratorUpdate", () => {
|
|
131
|
+
it("recognizes an update made only of Loro sync transactions", () => {
|
|
132
|
+
const transaction = createTransaction({
|
|
133
|
+
changes: { from: 0, insert: "#" },
|
|
134
|
+
annotations: [loroSyncAnnotation.of(true)],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(isCollaboratorUpdate([transaction])).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("treats typing beside a Loro sync transaction as local", () => {
|
|
141
|
+
const remote = createTransaction({
|
|
142
|
+
changes: { from: 0, insert: "#" },
|
|
143
|
+
annotations: [loroSyncAnnotation.of(true)],
|
|
144
|
+
});
|
|
145
|
+
const local = createTransaction({
|
|
146
|
+
changes: { from: 0, insert: "x" },
|
|
147
|
+
annotations: [Transaction.userEvent.of("input.type")],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(isCollaboratorUpdate([remote, local])).toBe(false);
|
|
151
|
+
expect(isCollaboratorUpdate([])).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|