@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/edit-app.tsx
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
useCellActions,
|
|
35
35
|
} from "./cells/cells";
|
|
36
36
|
import type { AppConfig, UserConfig } from "./config/config-schema";
|
|
37
|
+
import { DocumentProvider } from "./notebook/document";
|
|
37
38
|
import { RuntimeState } from "./kernel/RuntimeState";
|
|
38
39
|
import { getSessionId } from "./kernel/session";
|
|
39
40
|
import { useTogglePresenting } from "./layout/useTogglePresenting";
|
|
@@ -83,7 +84,7 @@ export const EditApp: React.FC<AppProps> = ({
|
|
|
83
84
|
};
|
|
84
85
|
}, [sendComponentValues]);
|
|
85
86
|
|
|
86
|
-
const { connection, reconnect } = useNotebookSession({
|
|
87
|
+
const { connection, reconnect, documentModel } = useNotebookSession({
|
|
87
88
|
autoInstantiate: userConfig.runtime.auto_instantiate,
|
|
88
89
|
setCells: (cells, layout) => {
|
|
89
90
|
setCells(cells);
|
|
@@ -174,7 +175,7 @@ export const EditApp: React.FC<AppProps> = ({
|
|
|
174
175
|
);
|
|
175
176
|
|
|
176
177
|
return (
|
|
177
|
-
|
|
178
|
+
<DocumentProvider model={documentModel}>
|
|
178
179
|
<SandboxController onReconnect={reconnect} />
|
|
179
180
|
<AppContainer
|
|
180
181
|
connection={connection}
|
|
@@ -228,6 +229,6 @@ export const EditApp: React.FC<AppProps> = ({
|
|
|
228
229
|
/>
|
|
229
230
|
</TooltipProvider>
|
|
230
231
|
)}
|
|
231
|
-
|
|
232
|
+
</DocumentProvider>
|
|
232
233
|
);
|
|
233
234
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { flushDocument } from "@/core/cells/document";
|
|
4
4
|
import { getSerializedLayout } from "@/core/layout/layout";
|
|
5
5
|
|
|
6
6
|
export async function getExportLayout() {
|
|
7
|
-
await
|
|
7
|
+
await flushDocument();
|
|
8
8
|
return getSerializedLayout();
|
|
9
9
|
}
|
|
@@ -347,7 +347,6 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
347
347
|
public saveAppConfig = throwNotImplemented;
|
|
348
348
|
public saveCellConfig = throwNotImplemented;
|
|
349
349
|
public sendRestart = throwNotImplemented;
|
|
350
|
-
public sendDocumentTransaction = throwNotImplemented;
|
|
351
350
|
public readCode = throwNotImplemented;
|
|
352
351
|
public readSnippets = throwNotImplemented;
|
|
353
352
|
public previewDatasetColumn = throwNotImplemented;
|
|
@@ -144,7 +144,6 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
144
144
|
downloadStorage: "waitForConnectionOpen",
|
|
145
145
|
|
|
146
146
|
// Sync operations that wait for connection
|
|
147
|
-
sendDocumentTransaction: "waitForConnectionOpen",
|
|
148
147
|
sendCodeCompletionRequest: "waitForConnectionOpen",
|
|
149
148
|
};
|
|
150
149
|
|
|
@@ -70,15 +70,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
70
70
|
})
|
|
71
71
|
.then(handleResponseReturnNull);
|
|
72
72
|
},
|
|
73
|
-
sendDocumentTransaction: async (request) => {
|
|
74
|
-
await waitForConnectionOpen();
|
|
75
|
-
return getClient()
|
|
76
|
-
.POST("/api/document/transaction", {
|
|
77
|
-
body: request,
|
|
78
|
-
params: getParams(),
|
|
79
|
-
})
|
|
80
|
-
.then(handleResponseReturnNull);
|
|
81
|
-
},
|
|
82
73
|
sendRename: (request) => {
|
|
83
74
|
return getClient()
|
|
84
75
|
.POST("/api/kernel/rename", {
|
|
@@ -40,7 +40,6 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
40
40
|
return null;
|
|
41
41
|
},
|
|
42
42
|
sendRestart: throwNotInEditMode,
|
|
43
|
-
sendDocumentTransaction: throwNotInEditMode,
|
|
44
43
|
sendRun: throwNotInEditMode,
|
|
45
44
|
sendRunScratchpad: throwNotInEditMode,
|
|
46
45
|
sendRename: throwNotInEditMode,
|
|
@@ -21,7 +21,6 @@ export function createErrorToastingRequests(
|
|
|
21
21
|
sendInstantiate: "Failed to instantiate",
|
|
22
22
|
sendFunctionRequest: "Failed to send function request",
|
|
23
23
|
sendRestart: "Failed to restart",
|
|
24
|
-
sendDocumentTransaction: "Failed to sync document transaction",
|
|
25
24
|
sendRun: "Failed to run",
|
|
26
25
|
sendRunScratchpad: "Failed to run scratchpad",
|
|
27
26
|
sendRename: "Failed to rename",
|
|
@@ -117,8 +117,6 @@ export interface FileCreateInput {
|
|
|
117
117
|
}
|
|
118
118
|
export type UpdateUIElementRequest = schemas["UpdateUIElementRequest"];
|
|
119
119
|
export type ModelRequest = schemas["ModelRequest"];
|
|
120
|
-
export type NotebookDocumentTransactionRequest =
|
|
121
|
-
schemas["NotebookDocumentTransactionRequest"];
|
|
122
120
|
export type UpdateUserConfigRequest = schemas["UpdateUserConfigRequest"];
|
|
123
121
|
export type ShutdownSessionRequest = schemas["ShutdownSessionRequest"];
|
|
124
122
|
export type Snippet = schemas["Snippet"];
|
|
@@ -179,9 +177,6 @@ export interface EditRequests {
|
|
|
179
177
|
saveAppConfig: (request: SaveAppConfigurationRequest) => Promise<null>;
|
|
180
178
|
saveCellConfig: (request: SetCellConfigRequest) => Promise<null>;
|
|
181
179
|
sendRestart: () => Promise<null>;
|
|
182
|
-
sendDocumentTransaction: (
|
|
183
|
-
request: NotebookDocumentTransactionRequest,
|
|
184
|
-
) => Promise<null>;
|
|
185
180
|
sendInstallMissingPackages: (
|
|
186
181
|
request: InstallPackagesRequest,
|
|
187
182
|
) => Promise<null>;
|
|
@@ -84,14 +84,14 @@ function setup({
|
|
|
84
84
|
} = {}) {
|
|
85
85
|
const store = createStore();
|
|
86
86
|
const source = atom(initial);
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
lifetimes.push(controller);
|
|
89
87
|
const model = createDocumentModel({
|
|
90
88
|
store,
|
|
91
89
|
documentAtom: source,
|
|
92
90
|
backendAtom: atom(fakeBackend({ edit: () => new Promise(() => {}) })),
|
|
93
|
-
signal: controller.signal,
|
|
94
91
|
});
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
lifetimes.push(controller);
|
|
94
|
+
model.start({ signal: controller.signal });
|
|
95
95
|
const editor = (text = doc) => {
|
|
96
96
|
const view = new EditorView({
|
|
97
97
|
state: EditorState.create({
|
|
@@ -326,7 +326,7 @@ describe("cellCodeBinding", () => {
|
|
|
326
326
|
});
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
-
it("
|
|
329
|
+
it("keeps Python mode for host code without rewriting it", async () => {
|
|
330
330
|
const { store, source, view, code, status } = setup({ adapters: true });
|
|
331
331
|
await settle();
|
|
332
332
|
expect(language(view)).toBe("python");
|
|
@@ -337,12 +337,58 @@ describe("cellCodeBinding", () => {
|
|
|
337
337
|
);
|
|
338
338
|
await settle();
|
|
339
339
|
|
|
340
|
-
expect(language(view)).toBe("
|
|
341
|
-
expect(view.state.doc.toString()).toBe("hi");
|
|
340
|
+
expect(language(view)).toBe("python");
|
|
341
|
+
expect(view.state.doc.toString()).toBe('mo.md("hi")');
|
|
342
342
|
expect(code()).toBe('mo.md("hi")');
|
|
343
343
|
expect(status()).toEqual({ status: "idle" });
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
it("keeps Python mode while typing mo.md() and receiving its echo", async () => {
|
|
347
|
+
const { store, source, view, type } = setup({
|
|
348
|
+
adapters: true,
|
|
349
|
+
initial: withCells(document(), { c1: cell("c1", "") }),
|
|
350
|
+
});
|
|
351
|
+
await settle();
|
|
352
|
+
type("mo.md");
|
|
353
|
+
await settle();
|
|
354
|
+
type("()");
|
|
355
|
+
await settle();
|
|
356
|
+
store.set(source, withCells(document(), { c1: cell("c1", "mo.md()") }));
|
|
357
|
+
await settle();
|
|
358
|
+
|
|
359
|
+
expect(language(view)).toBe("python");
|
|
360
|
+
expect(view.state.doc.toString()).toBe("mo.md()");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("falls back to Python when host code no longer fits Markdown", async () => {
|
|
364
|
+
const { store, source, view, code, status } = setup({
|
|
365
|
+
adapters: true,
|
|
366
|
+
initial: withCells(document(), { c1: cell("c1", 'mo.md("hi")') }),
|
|
367
|
+
});
|
|
368
|
+
await settle();
|
|
369
|
+
expect(language(view)).toBe("markdown");
|
|
370
|
+
|
|
371
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 1") }));
|
|
372
|
+
await settle();
|
|
373
|
+
|
|
374
|
+
expect(language(view)).toBe("python");
|
|
375
|
+
expect(view.state.doc.toString()).toBe("x = 1");
|
|
376
|
+
expect(code()).toBe("x = 1");
|
|
377
|
+
expect(status()).toEqual({ status: "idle" });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("preserves Python when typing before the initial pull", async () => {
|
|
381
|
+
const { view, type } = setup({
|
|
382
|
+
adapters: true,
|
|
383
|
+
initial: withCells(document(), { c1: cell("c1", "") }),
|
|
384
|
+
});
|
|
385
|
+
type("mo.md()");
|
|
386
|
+
await settle();
|
|
387
|
+
|
|
388
|
+
expect(language(view)).toBe("python");
|
|
389
|
+
expect(view.state.doc.toString()).toBe("mo.md()");
|
|
390
|
+
});
|
|
391
|
+
|
|
346
392
|
it("writes changes to language metadata alone", async () => {
|
|
347
393
|
const { view, code } = setup({
|
|
348
394
|
adapters: true,
|
|
@@ -435,15 +481,12 @@ describe("cellCodeBinding", () => {
|
|
|
435
481
|
});
|
|
436
482
|
|
|
437
483
|
it("edits SQL as SQL and keeps its output name", async () => {
|
|
438
|
-
const {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
store.set(
|
|
442
|
-
source,
|
|
443
|
-
withCells(document(), {
|
|
484
|
+
const { view, code, type } = setup({
|
|
485
|
+
adapters: true,
|
|
486
|
+
initial: withCells(document(), {
|
|
444
487
|
c1: cell("c1", 'df = mo.sql(f"""SELECT 1""")'),
|
|
445
488
|
}),
|
|
446
|
-
);
|
|
489
|
+
});
|
|
447
490
|
await settle();
|
|
448
491
|
expect(language(view)).toBe("sql");
|
|
449
492
|
expect(view.state.doc.toString()).toBe("SELECT 1");
|
|
@@ -456,7 +499,12 @@ describe("cellCodeBinding", () => {
|
|
|
456
499
|
});
|
|
457
500
|
|
|
458
501
|
it("sets the SQL dialect from the host's engine", async () => {
|
|
459
|
-
const { store, source } = setup({
|
|
502
|
+
const { store, source } = setup({
|
|
503
|
+
adapters: true,
|
|
504
|
+
initial: withCells(document(), {
|
|
505
|
+
c1: cell("c1", 'df = mo.sql("SELECT 1")'),
|
|
506
|
+
}),
|
|
507
|
+
});
|
|
460
508
|
await settle();
|
|
461
509
|
const dialect = vi.spyOn(SCHEMA_CACHE, "getDialect");
|
|
462
510
|
try {
|
|
@@ -483,3 +531,17 @@ describe("cellCodeBinding", () => {
|
|
|
483
531
|
});
|
|
484
532
|
});
|
|
485
533
|
});
|
|
534
|
+
|
|
535
|
+
describe("cellCodeBinding before the cell has source", () => {
|
|
536
|
+
it("still infers the language when the source first arrives", async () => {
|
|
537
|
+
const { store, view, source, type } = setup({
|
|
538
|
+
initial: null,
|
|
539
|
+
adapters: true,
|
|
540
|
+
});
|
|
541
|
+
type("x");
|
|
542
|
+
await settle();
|
|
543
|
+
store.set(source, withCells(document(), { c1: cell("c1", 'mo.md("hi")') }));
|
|
544
|
+
await settle();
|
|
545
|
+
expect(view.state.field(languageAdapterState).type).toBe("markdown");
|
|
546
|
+
});
|
|
547
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom, createStore } from "jotai";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { Document } from "../../replica/resources/document";
|
|
6
|
+
import { createDocumentModel } from "..";
|
|
7
|
+
import { type FakeEdit, fakeBackend } from "../../replica/__tests__/fixtures";
|
|
8
|
+
import { document } from "./fixtures";
|
|
9
|
+
|
|
10
|
+
const lifetimes: AbortController[] = [];
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
lifetimes.splice(0).forEach((controller) => controller.abort());
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** The document after the host accepts one edit. */
|
|
17
|
+
function accepted(): Document {
|
|
18
|
+
const base = document();
|
|
19
|
+
return {
|
|
20
|
+
...base,
|
|
21
|
+
revision: { ...base.revision, version: 2, etag: "document-2" },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setup() {
|
|
26
|
+
const store = createStore();
|
|
27
|
+
const source = atom<Document | null>(document());
|
|
28
|
+
// Answer the edit, then publish the accepted document as the host would.
|
|
29
|
+
const edit = vi.fn<FakeEdit>().mockImplementation(async () => {
|
|
30
|
+
queueMicrotask(() => store.set(source, accepted()));
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
status: 200,
|
|
34
|
+
data: { document: accepted().revision, cells: {} },
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const model = createDocumentModel({
|
|
38
|
+
store,
|
|
39
|
+
documentAtom: source,
|
|
40
|
+
backendAtom: atom(fakeBackend({ edit })),
|
|
41
|
+
});
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
lifetimes.push(controller);
|
|
44
|
+
model.start({ signal: controller.signal });
|
|
45
|
+
return { store, model, edit, controller };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("document sync hold", () => {
|
|
49
|
+
it("keeps an edit local until released, then lets a waiting flush send it", async () => {
|
|
50
|
+
const { store, model, edit } = setup();
|
|
51
|
+
const release = model.hold();
|
|
52
|
+
store.set(model.editAtom, (tx) => tx.setCode("c1", "x = 2"));
|
|
53
|
+
const flushing = model.flush();
|
|
54
|
+
await Promise.resolve();
|
|
55
|
+
expect(edit).not.toHaveBeenCalled();
|
|
56
|
+
expect(store.get(model.syncStatusAtom)).toEqual({ status: "pending" });
|
|
57
|
+
|
|
58
|
+
release();
|
|
59
|
+
await flushing;
|
|
60
|
+
expect(edit).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("releases once the last of nested holds is released", async () => {
|
|
64
|
+
const { store, model, edit } = setup();
|
|
65
|
+
const outer = model.hold();
|
|
66
|
+
const inner = model.hold();
|
|
67
|
+
store.set(model.editAtom, (tx) => tx.setCode("c1", "x = 2"));
|
|
68
|
+
const flushing = model.flush();
|
|
69
|
+
|
|
70
|
+
inner();
|
|
71
|
+
inner();
|
|
72
|
+
await Promise.resolve();
|
|
73
|
+
expect(edit).not.toHaveBeenCalled();
|
|
74
|
+
|
|
75
|
+
outer();
|
|
76
|
+
await flushing;
|
|
77
|
+
expect(edit).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects a flush waiting on a hold when the model stops", async () => {
|
|
81
|
+
const { store, model, controller } = setup();
|
|
82
|
+
model.hold();
|
|
83
|
+
store.set(model.editAtom, (tx) => tx.setCode("c1", "x = 2"));
|
|
84
|
+
const flushing = model.flush();
|
|
85
|
+
controller.abort(new Error("session ended"));
|
|
86
|
+
await expect(flushing).rejects.toThrow("session ended");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -32,14 +32,14 @@ function setup(
|
|
|
32
32
|
const source = atom(initial);
|
|
33
33
|
const detached = vi.fn();
|
|
34
34
|
source.onMount = () => detached;
|
|
35
|
-
const controller = new AbortController();
|
|
36
|
-
lifetimes.push(controller);
|
|
37
35
|
const model = createDocumentModel({
|
|
38
36
|
store,
|
|
39
37
|
documentAtom: source,
|
|
40
38
|
backendAtom: atom(fakeBackend({ edit })),
|
|
41
|
-
signal: controller.signal,
|
|
42
39
|
});
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
lifetimes.push(controller);
|
|
42
|
+
model.start({ signal: controller.signal });
|
|
43
43
|
const wrapper = ({ children }: PropsWithChildren) => (
|
|
44
44
|
<Provider store={store}>
|
|
45
45
|
<DocumentProvider model={model}>{children}</DocumentProvider>
|
|
@@ -13,7 +13,7 @@ test("closing a session before the debounce prevents submission", async ({
|
|
|
13
13
|
await vi.advanceTimersByTimeAsync(1000);
|
|
14
14
|
expect(notebook.edit).not.toHaveBeenCalled();
|
|
15
15
|
expect(vi.getTimerCount()).toBe(0);
|
|
16
|
-
await expect(user.sync()).rejects.
|
|
16
|
+
await expect(user.sync()).rejects.toThrow("not started");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
test("closing a session before a queued flush starts prevents submission", async ({
|
|
@@ -55,7 +55,7 @@ test.for(["request", "confirmation", "retry"] as const)(
|
|
|
55
55
|
await vi.advanceTimersByTimeAsync(1000);
|
|
56
56
|
expect(notebook.edit).toHaveBeenCalledOnce();
|
|
57
57
|
expect(vi.getTimerCount()).toBe(0);
|
|
58
|
-
await expect(user.sync()).rejects.
|
|
59
|
-
await expect(user.retry()).rejects.
|
|
58
|
+
await expect(user.sync()).rejects.toThrow("not started");
|
|
59
|
+
await expect(user.retry()).rejects.toThrow("not started");
|
|
60
60
|
},
|
|
61
61
|
);
|
|
@@ -52,14 +52,14 @@ function setup({ connected = true } = {}) {
|
|
|
52
52
|
sent({ notebookId, changes, ...options }),
|
|
53
53
|
});
|
|
54
54
|
const backendAtom = atom<NotebookBackend | null>(connected ? backend : null);
|
|
55
|
-
const controller = new AbortController();
|
|
56
|
-
lifetimes.push(controller);
|
|
57
55
|
const model = createDocumentModel({
|
|
58
56
|
store,
|
|
59
57
|
documentAtom: source,
|
|
60
58
|
backendAtom,
|
|
61
|
-
signal: controller.signal,
|
|
62
59
|
});
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
lifetimes.push(controller);
|
|
62
|
+
model.start({ signal: controller.signal });
|
|
63
63
|
return {
|
|
64
64
|
store,
|
|
65
65
|
source,
|
|
@@ -85,28 +85,84 @@ afterEach(() => {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
describe("document synchronization", () => {
|
|
88
|
-
it("
|
|
88
|
+
it("subscribes to nothing until started", async () => {
|
|
89
89
|
const store = createStore();
|
|
90
90
|
const source = atom<Document | null>(document());
|
|
91
91
|
const mounted = vi.fn();
|
|
92
92
|
source.onMount = mounted;
|
|
93
|
-
const controller = new AbortController();
|
|
94
|
-
const reason = new Error("Session ended");
|
|
95
|
-
controller.abort(reason);
|
|
96
93
|
const edit = vi.fn<FakeEdit>();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
).
|
|
94
|
+
const model = createDocumentModel({
|
|
95
|
+
store,
|
|
96
|
+
documentAtom: source,
|
|
97
|
+
backendAtom: atom(fakeBackend({ edit })),
|
|
98
|
+
});
|
|
99
|
+
store.set(model.editAtom, (tx) => tx.setCode("c1", "local"));
|
|
100
|
+
await expect(model.flush()).rejects.toThrow("not started");
|
|
101
|
+
expect(store.get(model.documentAtom)).toBeNull();
|
|
102
|
+
expect(store.get(model.syncStatusAtom)).toEqual({ status: "unavailable" });
|
|
105
103
|
expect(mounted).not.toHaveBeenCalled();
|
|
106
104
|
expect(edit).not.toHaveBeenCalled();
|
|
107
105
|
expect(vi.getTimerCount()).toBe(0);
|
|
108
106
|
});
|
|
109
107
|
|
|
108
|
+
it("rejects an aborted signal before subscribing", () => {
|
|
109
|
+
const store = createStore();
|
|
110
|
+
const source = atom<Document | null>(document());
|
|
111
|
+
const mounted = vi.fn();
|
|
112
|
+
source.onMount = mounted;
|
|
113
|
+
const model = createDocumentModel({
|
|
114
|
+
store,
|
|
115
|
+
documentAtom: source,
|
|
116
|
+
backendAtom: atom(fakeBackend({ edit: vi.fn<FakeEdit>() })),
|
|
117
|
+
});
|
|
118
|
+
const reason = new Error("Session ended");
|
|
119
|
+
expect(() => model.start({ signal: AbortSignal.abort(reason) })).toThrow(
|
|
120
|
+
reason,
|
|
121
|
+
);
|
|
122
|
+
expect(mounted).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("starts again after a subscriber aborts during startup", async () => {
|
|
126
|
+
const store = createStore();
|
|
127
|
+
const model = createDocumentModel({
|
|
128
|
+
store,
|
|
129
|
+
documentAtom: atom(document()),
|
|
130
|
+
backendAtom: atom(null),
|
|
131
|
+
});
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
lifetimes.push(controller);
|
|
134
|
+
const unsubscribe = store.sub(model.documentAtom, () => {
|
|
135
|
+
if (store.get(model.documentAtom)) {
|
|
136
|
+
controller.abort();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
model.start({ signal: controller.signal });
|
|
140
|
+
unsubscribe();
|
|
141
|
+
expect(controller.signal.aborted).toBe(true);
|
|
142
|
+
expect(store.get(model.documentAtom)).toBeNull();
|
|
143
|
+
await expect(model.flush()).rejects.toThrow("not started");
|
|
144
|
+
|
|
145
|
+
const next = new AbortController();
|
|
146
|
+
lifetimes.push(next);
|
|
147
|
+
model.start({ signal: next.signal });
|
|
148
|
+
expect(store.get(model.documentAtom)?.cells.get("c1")?.code).toBe("x = 1");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("starts again without the drafts of a stopped run", () => {
|
|
152
|
+
const { store, model, controller, edit, value } = setup();
|
|
153
|
+
expect(() => model.start({ signal: new AbortController().signal })).toThrow(
|
|
154
|
+
"already started",
|
|
155
|
+
);
|
|
156
|
+
edit("local");
|
|
157
|
+
controller.abort();
|
|
158
|
+
const next = new AbortController();
|
|
159
|
+
lifetimes.push(next);
|
|
160
|
+
model.start({ signal: next.signal });
|
|
161
|
+
expect(value()?.code).toBe("x = 1");
|
|
162
|
+
store.set(model.editAtom, (tx) => tx.setCode("c1", "restarted"));
|
|
163
|
+
expect(value()?.code).toBe("restarted");
|
|
164
|
+
});
|
|
165
|
+
|
|
110
166
|
it("fails without repeating a request when the response names another notebook", async () => {
|
|
111
167
|
const { sent, replies, edit, model, status } = setup();
|
|
112
168
|
edit("local");
|
|
@@ -15,8 +15,8 @@ function createSession(initial: Document, controller: AbortController) {
|
|
|
15
15
|
store,
|
|
16
16
|
documentAtom: notebook.documentAtom,
|
|
17
17
|
backendAtom: notebook.backendAtom,
|
|
18
|
-
signal: controller.signal,
|
|
19
18
|
});
|
|
19
|
+
model.start({ signal: controller.signal });
|
|
20
20
|
return {
|
|
21
21
|
user: createUser(store, model),
|
|
22
22
|
host: control,
|