@marimo-team/frontend 0.25.3-dev102 → 0.25.3-dev104
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/assets/{CellStatus-fK4n2NPc.js → CellStatus-DVrvejMC.js} +1 -1
- package/dist/assets/{ErrorBoundary-CAEGdoFW.js → ErrorBoundary-0akNKvJm.js} +2 -2
- package/dist/assets/{JsonOutput-CyAKmZDD.js → JsonOutput-DeUmpc_f.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-CgZvP0p0.js → MarimoErrorOutput-BazxeAYV.js} +1 -1
- package/dist/assets/{RenderHTML-DQvxDDNS.js → RenderHTML-CbAdeual.js} +1 -1
- package/dist/assets/{RunButton-BBDnDL6G.js → RunButton-CjKwgCFU.js} +1 -1
- package/dist/assets/{add-cell-with-ai-Clg5FELx.js → add-cell-with-ai-BKVEws3J.js} +1 -1
- package/dist/assets/{add-connection-dialog-BqdWmD-7.js → add-connection-dialog-DS7LTz1T.js} +1 -1
- package/dist/assets/{agent-panel-5RxG5Y82.js → agent-panel-B34QcdOK.js} +1 -1
- package/dist/assets/{ai-model-dropdown-Bvl1ktQq.js → ai-model-dropdown-DqFJCFVR.js} +1 -1
- package/dist/assets/{app-config-button-0QTeQ2Mv.js → app-config-button-CVh4UKi9.js} +1 -1
- package/dist/assets/{blob-5VcB8K_t.js → blob-Bw0ZQoLx.js} +1 -1
- package/dist/assets/{cell-editor-Byw5y-oI.js → cell-editor-DQ0io5Ye.js} +1 -1
- package/dist/assets/{cell-link-C5yYyzuF.js → cell-link-DvOIu6A3.js} +1 -1
- package/dist/assets/{cells-D_11MjVB.js → cells-Do-63MKT.js} +1 -1
- package/dist/assets/{chat-display-BCR_ySiV.js → chat-display-DnkhBxdh.js} +1 -1
- package/dist/assets/{chat-panel-RPRvBsaU.js → chat-panel-CN3qoX9G.js} +1 -1
- package/dist/assets/{chat-ui-CGviVaRA.js → chat-ui-B54K5qqr.js} +1 -1
- package/dist/assets/{column-preview-xe533-Cn.js → column-preview-DmxzHzaW.js} +1 -1
- package/dist/assets/{command-palette-BB9bWdWp.js → command-palette-CciCeRhy.js} +1 -1
- package/dist/assets/{common-CaeItbcI.js → common-BCyx7t1i.js} +1 -1
- package/dist/assets/{components-G6frbw0c.js → components-CDvWXfXq.js} +1 -1
- package/dist/assets/{datasource-deuXnlU-.js → datasource-DieftweT.js} +1 -1
- package/dist/assets/{dependency-graph-panel-CIykLnJe.js → dependency-graph-panel-IfcqTHgq.js} +1 -1
- package/dist/assets/{documentation-panel-2PQ8kQLa.js → documentation-panel-DeLCgVcq.js} +1 -1
- package/dist/assets/{download-dK52bLxZ.js → download-BzfVCqUq.js} +1 -1
- package/dist/assets/{edit-page-DrElzw10.js → edit-page-HMyk4cWw.js} +4 -4
- package/dist/assets/{error-entries-3GVk2KjS.js → error-entries-DjjWrbt4.js} +1 -1
- package/dist/assets/{error-panel-CzDRGPeC.js → error-panel-CiG3JJA3.js} +1 -1
- package/dist/assets/{feedback-button-C5NgWpFg.js → feedback-button-B8zVQEi6.js} +1 -1
- package/dist/assets/feedback-button-DQdMkSvR.js +1 -0
- package/dist/assets/{file-explorer-panel-B50xDnTZ.js → file-explorer-panel-BPD8Q0L-.js} +1 -1
- package/dist/assets/{file-name-input-DqwMSgGU.js → file-name-input-CdkF9K3I.js} +1 -1
- package/dist/assets/{floating-outline-ChmAuvxz.js → floating-outline-TfDWJXBm.js} +1 -1
- package/dist/assets/{gallery-page-CiL8aMS1.js → gallery-page-BJpenPdQ.js} +1 -1
- package/dist/assets/{glide-data-editor-CqRclBkp.js → glide-data-editor-DO15Nm-R.js} +1 -1
- package/dist/assets/{home-page-BgZVl4dP.js → home-page-mvqldGvD.js} +1 -1
- package/dist/assets/{hooks-DM5n7Osw.js → hooks-C4AJfqV0.js} +1 -1
- package/dist/assets/{index-EG2n8Oht.js → index-C2XzUsS7.js} +4 -4
- package/dist/assets/{layout-BpP8nTjb.js → layout-1kSh6fBF.js} +3 -3
- package/dist/assets/{logs-panel-B5eVAZtH.js → logs-panel--QbHo7uS.js} +1 -1
- package/dist/assets/{markdown-renderer-BWUzqkJM.js → markdown-renderer-DJ7H8Rps.js} +1 -1
- package/dist/assets/{name-cell-input-DXW68gRW.js → name-cell-input-6Gd19S8T.js} +1 -1
- package/dist/assets/{outline-panel-DPxJhJ_A.js → outline-panel-CjKvunvX.js} +1 -1
- package/dist/assets/{packages-panel-gBE2FWQr.js → packages-panel-Gyrqi-MZ.js} +1 -1
- package/dist/assets/{panel-accordion-state-CCF5Gxnc.js → panel-accordion-state-D49TNCLQ.js} +1 -1
- package/dist/assets/{panels-Dr6qMCvU.js → panels-DfbxoeGa.js} +1 -1
- package/dist/assets/{process-output-Cw9IDcKQ.js → process-output-CgkoeA1-.js} +1 -1
- package/dist/assets/{readonly-code-display-DYcZQ28T.js → readonly-code-display-BMbl_4BR.js} +1 -1
- package/dist/assets/{readonly-python-code-RFuQP50v.js → readonly-python-code-BnPfpOJM.js} +1 -1
- package/dist/assets/{reveal-component-yRrDmCQz.js → reveal-component-D84yAzG7.js} +1 -1
- package/dist/assets/{run-page-B69M4-zE.js → run-page-B10s8Pre.js} +1 -1
- package/dist/assets/{runtime-details-state-pobtu6TO.js → runtime-details-state-Bkmgb59p.js} +1 -1
- package/dist/assets/{scratchpad-panel-H44_pvb_.js → scratchpad-panel-DtyjJ3yN.js} +1 -1
- package/dist/assets/{session-panel-Bnd1iXkO.js → session-panel-Chaq1do8.js} +1 -1
- package/dist/assets/{snippets-panel-DKApxKx9.js → snippets-panel-BaZmddOW.js} +1 -1
- package/dist/assets/{state-Dv059B3A.js → state-CqnCwHhY.js} +1 -1
- package/dist/assets/{state-Bl9BiY_s.js → state-Db73TZVD.js} +1 -1
- package/dist/assets/{tracing-Clwy4SlW.js → tracing-C2vQMUI0.js} +1 -1
- package/dist/assets/{tracing-panel-BwjLG_aC.js → tracing-panel-B4L2S2ac.js} +2 -2
- package/dist/assets/{tree-actions-BZNTQ12N.js → tree-actions-JXDiioHK.js} +1 -1
- package/dist/assets/{useAddCell-Cjuqk2YR.js → useAddCell-CP3HIjGl.js} +1 -1
- package/dist/assets/{useCellActionButton-MoWLvuGe.js → useCellActionButton-yi1_-emd.js} +1 -1
- package/dist/assets/{useDeleteCell-BC-MW7d1.js → useDeleteCell-BPLwyClM.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZU7i-B3.js → useDependencyPanelTab-DmBiydXO.js} +1 -1
- package/dist/assets/{useNotebookActions-wFxVdRXQ.js → useNotebookActions-CfmgcJGx.js} +1 -1
- package/dist/assets/{useRunCells-ByaWxoIt.js → useRunCells-jF-7tJkw.js} +1 -1
- package/dist/assets/{useSplitCell-YJIystwv.js → useSplitCell-B6zWL8Zf.js} +1 -1
- package/dist/index.html +20 -20
- package/package.json +1 -1
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +15 -0
- package/src/core/codemirror/cells/extensions.ts +7 -3
- package/src/core/document/__tests__/codemirror.test.ts +483 -0
- package/src/core/document/__tests__/hooks.test.tsx +386 -0
- package/src/core/document/codemirror.ts +311 -0
- package/src/core/document/hooks.tsx +118 -0
- package/src/core/document/index.ts +10 -0
- package/src/core/document/select.ts +9 -0
- package/dist/assets/feedback-button-CONwLFpw.js +0 -1
package/dist/index.html
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
|
|
67
67
|
<!-- /TODO -->
|
|
68
68
|
<title>{{ title }}</title>
|
|
69
|
-
<script type="module" crossorigin src="./assets/index-
|
|
69
|
+
<script type="module" crossorigin src="./assets/index-C2XzUsS7.js"></script>
|
|
70
70
|
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-B0Z9INg1.js">
|
|
71
71
|
<link rel="modulepreload" crossorigin href="./assets/react-DVplv7_D.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="./assets/compiler-runtime-BrajqUja.js">
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
<link rel="modulepreload" crossorigin href="./assets/useTheme-BAA27lh7.js">
|
|
140
140
|
<link rel="modulepreload" crossorigin href="./assets/useAsyncData-qvoPYHtx.js">
|
|
141
141
|
<link rel="modulepreload" crossorigin href="./assets/purify.es--06brOip.js">
|
|
142
|
-
<link rel="modulepreload" crossorigin href="./assets/cells-
|
|
142
|
+
<link rel="modulepreload" crossorigin href="./assets/cells-Do-63MKT.js">
|
|
143
143
|
<link rel="modulepreload" crossorigin href="./assets/coerce-DWjFeXOY.js">
|
|
144
144
|
<link rel="modulepreload" crossorigin href="./assets/get-DjNf4SIX.js">
|
|
145
145
|
<link rel="modulepreload" crossorigin href="./assets/_hasPath-BmRQipI0.js">
|
|
@@ -154,40 +154,40 @@
|
|
|
154
154
|
<link rel="modulepreload" crossorigin href="./assets/copy-KlhazlhB.js">
|
|
155
155
|
<link rel="modulepreload" crossorigin href="./assets/micromark-factory-space-D9Dr-k4J.js">
|
|
156
156
|
<link rel="modulepreload" crossorigin href="./assets/chunk-YOKDWASO-BIpRENEC.js">
|
|
157
|
-
<link rel="modulepreload" crossorigin href="./assets/markdown-renderer-
|
|
157
|
+
<link rel="modulepreload" crossorigin href="./assets/markdown-renderer-DJ7H8Rps.js">
|
|
158
158
|
<link rel="modulepreload" crossorigin href="./assets/copy-Djr5m5ac.js">
|
|
159
159
|
<link rel="modulepreload" crossorigin href="./assets/copy-icon-8-a5vovc.js">
|
|
160
|
-
<link rel="modulepreload" crossorigin href="./assets/useRunCells-
|
|
161
|
-
<link rel="modulepreload" crossorigin href="./assets/RenderHTML-
|
|
162
|
-
<link rel="modulepreload" crossorigin href="./assets/blob-
|
|
160
|
+
<link rel="modulepreload" crossorigin href="./assets/useRunCells-jF-7tJkw.js">
|
|
161
|
+
<link rel="modulepreload" crossorigin href="./assets/RenderHTML-CbAdeual.js">
|
|
162
|
+
<link rel="modulepreload" crossorigin href="./assets/blob-Bw0ZQoLx.js">
|
|
163
163
|
<link rel="modulepreload" crossorigin href="./assets/kbd-pdKBv96t.js">
|
|
164
164
|
<link rel="modulepreload" crossorigin href="./assets/useInstallPackage-wcDq9hYT.js">
|
|
165
165
|
<link rel="modulepreload" crossorigin href="./assets/share-BZW3tzEX.js">
|
|
166
166
|
<link rel="modulepreload" crossorigin href="./assets/events-CbeVJ6OD.js">
|
|
167
|
-
<link rel="modulepreload" crossorigin href="./assets/state-
|
|
168
|
-
<link rel="modulepreload" crossorigin href="./assets/ErrorBoundary-
|
|
167
|
+
<link rel="modulepreload" crossorigin href="./assets/state-Db73TZVD.js">
|
|
168
|
+
<link rel="modulepreload" crossorigin href="./assets/ErrorBoundary-0akNKvJm.js">
|
|
169
169
|
<link rel="modulepreload" crossorigin href="./assets/chevron-right-eAtVhbNZ.js">
|
|
170
170
|
<link rel="modulepreload" crossorigin href="./assets/dropdown-menu-DS6umAnH.js">
|
|
171
171
|
<link rel="modulepreload" crossorigin href="./assets/html-to-image-D_0rd3_B.js">
|
|
172
|
-
<link rel="modulepreload" crossorigin href="./assets/download-
|
|
172
|
+
<link rel="modulepreload" crossorigin href="./assets/download-BzfVCqUq.js">
|
|
173
173
|
<link rel="modulepreload" crossorigin href="./assets/switch-DYjBfpNJ.js">
|
|
174
174
|
<link rel="modulepreload" crossorigin href="./assets/tabs-CY8E4r9U.js">
|
|
175
175
|
<link rel="modulepreload" crossorigin href="./assets/table-DG7Ap4H4.js">
|
|
176
176
|
<link rel="modulepreload" crossorigin href="./assets/eye-off-CxVozGAC.js">
|
|
177
177
|
<link rel="modulepreload" crossorigin href="./assets/refresh-cw-Qw1f8Jhy.js">
|
|
178
|
-
<link rel="modulepreload" crossorigin href="./assets/tree-actions-
|
|
178
|
+
<link rel="modulepreload" crossorigin href="./assets/tree-actions-JXDiioHK.js">
|
|
179
179
|
<link rel="modulepreload" crossorigin href="./assets/ellipsis-63DyBLHQ.js">
|
|
180
180
|
<link rel="modulepreload" crossorigin href="./assets/external-link-UrX9KO36.js">
|
|
181
181
|
<link rel="modulepreload" crossorigin href="./assets/spinner-DyNE-o8o.js">
|
|
182
182
|
<link rel="modulepreload" crossorigin href="./assets/message-circle-C3rKm8_a.js">
|
|
183
183
|
<link rel="modulepreload" crossorigin href="./assets/hat-glasses-C8RhSVii.js">
|
|
184
184
|
<link rel="modulepreload" crossorigin href="./assets/package-GSoSTiIK.js">
|
|
185
|
-
<link rel="modulepreload" crossorigin href="./assets/datasource-
|
|
186
|
-
<link rel="modulepreload" crossorigin href="./assets/error-entries-
|
|
185
|
+
<link rel="modulepreload" crossorigin href="./assets/datasource-DieftweT.js">
|
|
186
|
+
<link rel="modulepreload" crossorigin href="./assets/error-entries-DjjWrbt4.js">
|
|
187
187
|
<link rel="modulepreload" crossorigin href="./assets/alert-DTaqCu_d.js">
|
|
188
|
-
<link rel="modulepreload" crossorigin href="./assets/state-
|
|
189
|
-
<link rel="modulepreload" crossorigin href="./assets/cell-link-
|
|
190
|
-
<link rel="modulepreload" crossorigin href="./assets/MarimoErrorOutput-
|
|
188
|
+
<link rel="modulepreload" crossorigin href="./assets/state-CqnCwHhY.js">
|
|
189
|
+
<link rel="modulepreload" crossorigin href="./assets/cell-link-DvOIu6A3.js">
|
|
190
|
+
<link rel="modulepreload" crossorigin href="./assets/MarimoErrorOutput-BazxeAYV.js">
|
|
191
191
|
<link rel="modulepreload" crossorigin href="./assets/useDateFormatter-DA-62E-Y.js">
|
|
192
192
|
<link rel="modulepreload" crossorigin href="./assets/en-US-Buu61juQ.js">
|
|
193
193
|
<link rel="modulepreload" crossorigin href="./assets/isValid-q_Ad80ug.js">
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
<link rel="modulepreload" crossorigin href="./assets/table-vrvqpuKJ.js">
|
|
208
208
|
<link rel="modulepreload" crossorigin href="./assets/react-resizable-panels.browser.esm-Dbe0MrFr.js">
|
|
209
209
|
<link rel="modulepreload" crossorigin href="./assets/useStateForKey-CGMJQT8x.js">
|
|
210
|
-
<link rel="modulepreload" crossorigin href="./assets/JsonOutput-
|
|
210
|
+
<link rel="modulepreload" crossorigin href="./assets/JsonOutput-DeUmpc_f.js">
|
|
211
211
|
<link rel="modulepreload" crossorigin href="./assets/chart-no-axes-column-C4_v6fQW.js">
|
|
212
212
|
<link rel="modulepreload" crossorigin href="./assets/square-function-_W_Lo0P4.js">
|
|
213
213
|
<link rel="modulepreload" crossorigin href="./assets/spec-uCpVvaAR.js">
|
|
@@ -221,11 +221,11 @@
|
|
|
221
221
|
<link rel="modulepreload" crossorigin href="./assets/file-icons-CdJgUy-I.js">
|
|
222
222
|
<link rel="modulepreload" crossorigin href="./assets/globe-UdPdIbF7.js">
|
|
223
223
|
<link rel="modulepreload" crossorigin href="./assets/esm-CI_o7NhW.js">
|
|
224
|
-
<link rel="modulepreload" crossorigin href="./assets/useAddCell-
|
|
225
|
-
<link rel="modulepreload" crossorigin href="./assets/readonly-python-code-
|
|
224
|
+
<link rel="modulepreload" crossorigin href="./assets/useAddCell-CP3HIjGl.js">
|
|
225
|
+
<link rel="modulepreload" crossorigin href="./assets/readonly-python-code-BnPfpOJM.js">
|
|
226
226
|
<link rel="modulepreload" crossorigin href="./assets/data-grid-types-DgHVN_Zz.js">
|
|
227
227
|
<link rel="modulepreload" crossorigin href="./assets/glide-utils-HFqhvWHI.js">
|
|
228
|
-
<link rel="modulepreload" crossorigin href="./assets/column-preview-
|
|
228
|
+
<link rel="modulepreload" crossorigin href="./assets/column-preview-DmxzHzaW.js">
|
|
229
229
|
<link rel="modulepreload" crossorigin href="./assets/square-BPmgQFnh.js">
|
|
230
230
|
<link rel="modulepreload" crossorigin href="./assets/triangle-alert-DmIqR8sY.js">
|
|
231
231
|
<link rel="modulepreload" crossorigin href="./assets/prop-types-Djf4O40e.js">
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
<link rel="modulepreload" crossorigin href="./assets/useDeepCompareMemoize-B2DVK2De.js">
|
|
239
239
|
<link rel="modulepreload" crossorigin href="./assets/useDelayElapsed-BlsDT4TV.js">
|
|
240
240
|
<link rel="modulepreload" crossorigin href="./assets/pathUtils-DPmlLd-r.js">
|
|
241
|
-
<link rel="modulepreload" crossorigin href="./assets/floating-outline-
|
|
241
|
+
<link rel="modulepreload" crossorigin href="./assets/floating-outline-TfDWJXBm.js">
|
|
242
242
|
<link rel="stylesheet" crossorigin href="./assets/cells-0PFKL3SH.css">
|
|
243
243
|
<link rel="stylesheet" crossorigin href="./assets/markdown-renderer-DjIHoF9Z.css">
|
|
244
244
|
<link rel="stylesheet" crossorigin href="./assets/JsonOutput-B0GmlMnR.css">
|
package/package.json
CHANGED
|
@@ -68,6 +68,21 @@ describe("shouldAutorunMarkdownUpdate", () => {
|
|
|
68
68
|
).toBe(false);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it("ignores remote transactions while focused", () => {
|
|
72
|
+
const transaction = createTransaction({
|
|
73
|
+
changes: { from: 0, insert: "#" },
|
|
74
|
+
annotations: [Transaction.remote.of(true)],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(
|
|
78
|
+
shouldAutorunMarkdownUpdate({
|
|
79
|
+
docChanged: transaction.docChanged,
|
|
80
|
+
transactions: [transaction],
|
|
81
|
+
hasFocus: true,
|
|
82
|
+
}),
|
|
83
|
+
).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
71
86
|
it("ignores programmatic doc changes without a user event", () => {
|
|
72
87
|
const transaction = createTransaction({
|
|
73
88
|
changes: { from: 0, insert: "#" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { closeCompletion, completionStatus } from "@codemirror/autocomplete";
|
|
4
|
-
import { type Extension, Prec } from "@codemirror/state";
|
|
4
|
+
import { type Extension, Prec, Transaction } from "@codemirror/state";
|
|
5
5
|
import {
|
|
6
6
|
EditorView,
|
|
7
7
|
type KeyBinding,
|
|
@@ -382,8 +382,12 @@ function shouldAutorunMarkdownUpdate({
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
return transactions.some((tr) => {
|
|
385
|
-
// Ignore
|
|
386
|
-
|
|
385
|
+
// Ignore remote changes to avoid duplicate runs. The client that made the
|
|
386
|
+
// edit runs it.
|
|
387
|
+
if (
|
|
388
|
+
tr.annotation(Transaction.remote) ||
|
|
389
|
+
tr.annotation(loroSyncAnnotation) !== undefined
|
|
390
|
+
) {
|
|
387
391
|
return false;
|
|
388
392
|
}
|
|
389
393
|
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { history, undo } from "@codemirror/commands";
|
|
4
|
+
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
5
|
+
import { EditorView } from "@codemirror/view";
|
|
6
|
+
import type { CellId } from "@marimo-team/client/protocol";
|
|
7
|
+
import { atom, createStore } from "jotai/vanilla";
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { cellId as brandedCellId } from "@/__tests__/branded";
|
|
10
|
+
import { createNotebookActions } from "@/core/cells/cells";
|
|
11
|
+
import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
|
|
12
|
+
import { cellActionsState } from "../../codemirror/cells/state";
|
|
13
|
+
import { cellConfigExtension } from "../../codemirror/config/extension";
|
|
14
|
+
import {
|
|
15
|
+
adaptiveLanguageConfiguration,
|
|
16
|
+
languageAdapterState,
|
|
17
|
+
switchLanguage,
|
|
18
|
+
} from "../../codemirror/language/extension";
|
|
19
|
+
import {
|
|
20
|
+
languageMetadataField,
|
|
21
|
+
setLanguageMetadata,
|
|
22
|
+
} from "../../codemirror/language/metadata";
|
|
23
|
+
import { SCHEMA_CACHE } from "../../codemirror/language/languages/sql/completion-store";
|
|
24
|
+
import type { Document } from "../../host/resources/document";
|
|
25
|
+
import { cellCodeBinding } from "../codemirror";
|
|
26
|
+
import { createDocumentModel } from "../model";
|
|
27
|
+
import { cell, document, withCells } from "./fixtures";
|
|
28
|
+
|
|
29
|
+
const views: EditorView[] = [];
|
|
30
|
+
const lifetimes: AbortController[] = [];
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
for (const view of views.splice(0)) {
|
|
34
|
+
view.destroy();
|
|
35
|
+
}
|
|
36
|
+
lifetimes.splice(0).forEach((controller) => controller.abort());
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const completionConfig = {
|
|
40
|
+
activate_on_typing: true,
|
|
41
|
+
signature_hint_on_typing: false,
|
|
42
|
+
copilot: false,
|
|
43
|
+
codeium_api_key: null,
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
/** The language configuration a marimo cell editor installs. */
|
|
47
|
+
function languages(id: CellId) {
|
|
48
|
+
const options = {
|
|
49
|
+
cellId: brandedCellId(id),
|
|
50
|
+
completionConfig,
|
|
51
|
+
hotkeys: new OverridingHotkeyProvider({}),
|
|
52
|
+
placeholderType: "marimo-import",
|
|
53
|
+
lspConfig: {},
|
|
54
|
+
} as const;
|
|
55
|
+
return [
|
|
56
|
+
cellActionsState.of({
|
|
57
|
+
...createNotebookActions(vi.fn()),
|
|
58
|
+
toggleHideCode: () => false,
|
|
59
|
+
aiCellCompletion: () => false,
|
|
60
|
+
createManyBelow: vi.fn(),
|
|
61
|
+
addOrAppendSetupCell: vi.fn(),
|
|
62
|
+
onRun: vi.fn(),
|
|
63
|
+
deleteCell: vi.fn(),
|
|
64
|
+
afterToggleMarkdown: vi.fn(),
|
|
65
|
+
afterToggleSQL: vi.fn(),
|
|
66
|
+
saveNotebook: vi.fn(),
|
|
67
|
+
}),
|
|
68
|
+
adaptiveLanguageConfiguration(options),
|
|
69
|
+
cellConfigExtension({ ...options, diagnosticsConfig: {} }),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setup({
|
|
74
|
+
initial = document(),
|
|
75
|
+
cellId = "c1",
|
|
76
|
+
doc = "",
|
|
77
|
+
adapters = false,
|
|
78
|
+
}: {
|
|
79
|
+
initial?: Document | null;
|
|
80
|
+
cellId?: CellId;
|
|
81
|
+
doc?: string;
|
|
82
|
+
adapters?: boolean;
|
|
83
|
+
} = {}) {
|
|
84
|
+
const store = createStore();
|
|
85
|
+
const source = atom(initial);
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
lifetimes.push(controller);
|
|
88
|
+
const model = createDocumentModel({
|
|
89
|
+
store,
|
|
90
|
+
notebook: { documentAtom: source, edit: () => new Promise(() => {}) },
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
});
|
|
93
|
+
const editor = (text = doc) => {
|
|
94
|
+
const view = new EditorView({
|
|
95
|
+
state: EditorState.create({
|
|
96
|
+
doc: text,
|
|
97
|
+
extensions: [
|
|
98
|
+
history(),
|
|
99
|
+
adapters ? languages(cellId) : [],
|
|
100
|
+
cellCodeBinding({ store, model, cellId }),
|
|
101
|
+
],
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
views.push(view);
|
|
105
|
+
return view;
|
|
106
|
+
};
|
|
107
|
+
const view = editor();
|
|
108
|
+
return {
|
|
109
|
+
store,
|
|
110
|
+
source,
|
|
111
|
+
model,
|
|
112
|
+
view,
|
|
113
|
+
editor,
|
|
114
|
+
code: () => store.get(model.documentAtom)?.cells.get(cellId)?.code,
|
|
115
|
+
status: () => store.get(model.syncStatusAtom),
|
|
116
|
+
type: (insert: string) =>
|
|
117
|
+
view.dispatch({
|
|
118
|
+
changes: { from: view.state.doc.length, insert },
|
|
119
|
+
userEvent: "input.type",
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Inbound changes apply in a microtask.
|
|
125
|
+
const settle = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
126
|
+
|
|
127
|
+
describe("cellCodeBinding", () => {
|
|
128
|
+
it("seeds the editor from the cell outside undo history", async () => {
|
|
129
|
+
const { view, status } = setup();
|
|
130
|
+
await settle();
|
|
131
|
+
expect(view.state.doc.toString()).toBe("x = 1");
|
|
132
|
+
expect(undo(view)).toBe(false);
|
|
133
|
+
expect(status()).toEqual({ status: "idle" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("writes local changes to the draft before the update returns", async () => {
|
|
137
|
+
const { code, status, type } = setup();
|
|
138
|
+
await settle();
|
|
139
|
+
type(" + 1");
|
|
140
|
+
expect(code()).toBe("x = 1 + 1");
|
|
141
|
+
expect(status()).toEqual({ status: "pending" });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("shows a change another subscriber makes while a push notifies", async () => {
|
|
145
|
+
const { store, model, view, type } = setup();
|
|
146
|
+
await settle();
|
|
147
|
+
// Another subscriber normalizes code synchronously, inside the push.
|
|
148
|
+
const unsubscribe = store.sub(model.documentAtom, () => {
|
|
149
|
+
const code = store.get(model.documentAtom)?.cells.get("c1")?.code;
|
|
150
|
+
if (code?.includes(" + ")) {
|
|
151
|
+
const normalized = code.replaceAll(" + ", "+");
|
|
152
|
+
store.set(model.editAtom, (tx) => tx.setCode("c1", normalized));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
type(" + 1");
|
|
156
|
+
await settle();
|
|
157
|
+
expect(view.state.doc.toString()).toBe("x = 1+1");
|
|
158
|
+
unsubscribe();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps the selection between separate host edits", async () => {
|
|
162
|
+
const { store, source, view } = setup({
|
|
163
|
+
initial: withCells(document(), { c1: cell("c1", "a = 1\nb = 2\nc = 3") }),
|
|
164
|
+
});
|
|
165
|
+
await settle();
|
|
166
|
+
view.dispatch({ selection: EditorSelection.cursor(8) });
|
|
167
|
+
|
|
168
|
+
store.set(
|
|
169
|
+
source,
|
|
170
|
+
withCells(document(), { c1: cell("c1", "A = 1\nb = 2\nC = 3") }),
|
|
171
|
+
);
|
|
172
|
+
await settle();
|
|
173
|
+
|
|
174
|
+
expect(view.state.doc.toString()).toBe("A = 1\nb = 2\nC = 3");
|
|
175
|
+
expect(view.state.selection.main.head).toBe(8);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("waits for composition to end before applying host changes", async () => {
|
|
179
|
+
const { store, source, view } = setup();
|
|
180
|
+
await settle();
|
|
181
|
+
let composing = true;
|
|
182
|
+
Object.defineProperty(view, "composing", { get: () => composing });
|
|
183
|
+
|
|
184
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 2") }));
|
|
185
|
+
await settle();
|
|
186
|
+
expect(view.state.doc.toString()).toBe("x = 1");
|
|
187
|
+
|
|
188
|
+
// A composition can end without an update, so the binding watches for it.
|
|
189
|
+
composing = false;
|
|
190
|
+
view.contentDOM.dispatchEvent(new Event("compositionend"));
|
|
191
|
+
await settle();
|
|
192
|
+
expect(view.state.doc.toString()).toBe("x = 2");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("applies a waiting host change on the first update after composition", async () => {
|
|
196
|
+
const { store, source, view } = setup();
|
|
197
|
+
await settle();
|
|
198
|
+
let composing = true;
|
|
199
|
+
Object.defineProperty(view, "composing", { get: () => composing });
|
|
200
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 2") }));
|
|
201
|
+
await settle();
|
|
202
|
+
|
|
203
|
+
composing = false;
|
|
204
|
+
view.dispatch({ selection: EditorSelection.cursor(0) });
|
|
205
|
+
await settle();
|
|
206
|
+
expect(view.state.doc.toString()).toBe("x = 2");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("keeps two editors on one cell in step", async () => {
|
|
210
|
+
const { editor, type } = setup();
|
|
211
|
+
const other = editor();
|
|
212
|
+
await settle();
|
|
213
|
+
|
|
214
|
+
type("!");
|
|
215
|
+
await settle();
|
|
216
|
+
|
|
217
|
+
expect(other.state.doc.toString()).toBe("x = 1!");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("shows host code that returns to what this editor wrote", async () => {
|
|
221
|
+
const { store, source, view, type } = setup();
|
|
222
|
+
await settle();
|
|
223
|
+
type("!");
|
|
224
|
+
// The host confirms the edit, which releases the draft.
|
|
225
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 1!") }));
|
|
226
|
+
await settle();
|
|
227
|
+
|
|
228
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 2") }));
|
|
229
|
+
await settle();
|
|
230
|
+
expect(view.state.doc.toString()).toBe("x = 2");
|
|
231
|
+
|
|
232
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 1!") }));
|
|
233
|
+
await settle();
|
|
234
|
+
expect(view.state.doc.toString()).toBe("x = 1!");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("follows a cell once its code is available", async () => {
|
|
238
|
+
const absent = setup({ cellId: "c3" });
|
|
239
|
+
const withheld = setup({
|
|
240
|
+
initial: withCells(document(), { c1: cell("c1", "", { code: null }) }),
|
|
241
|
+
});
|
|
242
|
+
await settle();
|
|
243
|
+
|
|
244
|
+
absent.store.set(
|
|
245
|
+
absent.source,
|
|
246
|
+
withCells(document(), { c3: cell("c3", "new") }),
|
|
247
|
+
);
|
|
248
|
+
withheld.store.set(
|
|
249
|
+
withheld.source,
|
|
250
|
+
withCells(document(), { c1: cell("c1", "revealed") }),
|
|
251
|
+
);
|
|
252
|
+
await settle();
|
|
253
|
+
|
|
254
|
+
expect(absent.view.state.doc.toString()).toBe("new");
|
|
255
|
+
expect(withheld.view.state.doc.toString()).toBe("revealed");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("shows the host's code after typing while the document was unavailable", async () => {
|
|
259
|
+
const { store, source, view, type, code } = setup();
|
|
260
|
+
await settle();
|
|
261
|
+
|
|
262
|
+
// All in one task, so no pull runs while the document is away.
|
|
263
|
+
store.set(source, null);
|
|
264
|
+
type(" + 1");
|
|
265
|
+
store.set(source, document());
|
|
266
|
+
await settle();
|
|
267
|
+
|
|
268
|
+
expect(view.state.doc.toString()).toBe(code());
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("leaves the editor alone for an absent cell or withheld source", async () => {
|
|
272
|
+
const absent = setup({ cellId: "c9", doc: "local" });
|
|
273
|
+
const withheld = setup({
|
|
274
|
+
initial: withCells(document(), { c1: cell("c1", "", { code: null }) }),
|
|
275
|
+
doc: "local",
|
|
276
|
+
});
|
|
277
|
+
await settle();
|
|
278
|
+
|
|
279
|
+
for (const { view, type, status } of [absent, withheld]) {
|
|
280
|
+
type("!");
|
|
281
|
+
expect(view.state.doc.toString()).toBe("local!");
|
|
282
|
+
expect(status()).toEqual({ status: "idle" });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("stops following the cell when the view is destroyed", async () => {
|
|
287
|
+
const { store, source, view } = setup();
|
|
288
|
+
await settle();
|
|
289
|
+
|
|
290
|
+
// The change arrives before the view goes, and would apply just after.
|
|
291
|
+
store.set(source, withCells(document(), { c1: cell("c1", "x = 2") }));
|
|
292
|
+
view.destroy();
|
|
293
|
+
await settle();
|
|
294
|
+
|
|
295
|
+
expect(view.state.doc.toString()).toBe("x = 1");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("with language adapters", () => {
|
|
299
|
+
const language = (view: EditorView) =>
|
|
300
|
+
view.state.field(languageAdapterState).type;
|
|
301
|
+
/** What the editor's adapter reads back from cell code. */
|
|
302
|
+
const readBack = (view: EditorView, code: string | null | undefined) => {
|
|
303
|
+
const [text, , metadata] = view.state
|
|
304
|
+
.field(languageAdapterState)
|
|
305
|
+
.transformIn(code ?? "");
|
|
306
|
+
return { text, metadata };
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
it("edits markdown as markdown and writes Python with its metadata", async () => {
|
|
310
|
+
const { view, code, type } = setup({
|
|
311
|
+
adapters: true,
|
|
312
|
+
initial: withCells(document(), {
|
|
313
|
+
c1: cell("c1", 'mo.md(f"""hi {x}""")'),
|
|
314
|
+
}),
|
|
315
|
+
});
|
|
316
|
+
await settle();
|
|
317
|
+
expect(language(view)).toBe("markdown");
|
|
318
|
+
expect(view.state.doc.toString()).toBe("hi {x}");
|
|
319
|
+
|
|
320
|
+
type("!");
|
|
321
|
+
expect(readBack(view, code())).toMatchObject({
|
|
322
|
+
text: "hi {x}!",
|
|
323
|
+
metadata: { quotePrefix: "f" },
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("switches language for host code without rewriting it", async () => {
|
|
328
|
+
const { store, source, view, code, status } = setup({ adapters: true });
|
|
329
|
+
await settle();
|
|
330
|
+
expect(language(view)).toBe("python");
|
|
331
|
+
|
|
332
|
+
store.set(
|
|
333
|
+
source,
|
|
334
|
+
withCells(document(), { c1: cell("c1", 'mo.md("hi")') }),
|
|
335
|
+
);
|
|
336
|
+
await settle();
|
|
337
|
+
|
|
338
|
+
expect(language(view)).toBe("markdown");
|
|
339
|
+
expect(view.state.doc.toString()).toBe("hi");
|
|
340
|
+
expect(code()).toBe('mo.md("hi")');
|
|
341
|
+
expect(status()).toEqual({ status: "idle" });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("writes changes to language metadata alone", async () => {
|
|
345
|
+
const { view, code } = setup({
|
|
346
|
+
adapters: true,
|
|
347
|
+
initial: withCells(document(), {
|
|
348
|
+
c1: cell("c1", 'mo.md("""\n# Title\n""")'),
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
351
|
+
await settle();
|
|
352
|
+
|
|
353
|
+
view.dispatch({
|
|
354
|
+
effects: setLanguageMetadata.of({
|
|
355
|
+
...view.state.field(languageMetadataField),
|
|
356
|
+
quotePrefix: "r",
|
|
357
|
+
}),
|
|
358
|
+
});
|
|
359
|
+
expect(readBack(view, code())).toMatchObject({
|
|
360
|
+
text: "# Title",
|
|
361
|
+
metadata: { quotePrefix: "r" },
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("applies a host change to language metadata alone", async () => {
|
|
366
|
+
const { store, source, view } = setup({
|
|
367
|
+
adapters: true,
|
|
368
|
+
initial: withCells(document(), {
|
|
369
|
+
c1: cell("c1", 'mo.md(r"""hi""")'),
|
|
370
|
+
}),
|
|
371
|
+
});
|
|
372
|
+
await settle();
|
|
373
|
+
|
|
374
|
+
store.set(
|
|
375
|
+
source,
|
|
376
|
+
withCells(document(), { c1: cell("c1", 'mo.md(f"""hi""")') }),
|
|
377
|
+
);
|
|
378
|
+
await settle();
|
|
379
|
+
|
|
380
|
+
expect(view.state.doc.toString()).toBe("hi");
|
|
381
|
+
expect(view.state.field(languageMetadataField)).toMatchObject({
|
|
382
|
+
quotePrefix: "f",
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("does not rewrite code when the editor switches language as it opens", async () => {
|
|
387
|
+
const original = 'mo.md("hello")';
|
|
388
|
+
const { view, code, status } = setup({
|
|
389
|
+
adapters: true,
|
|
390
|
+
doc: original,
|
|
391
|
+
initial: withCells(document(), { c1: cell("c1", original) }),
|
|
392
|
+
});
|
|
393
|
+
// The cell editor switches to the language its code implies on mount.
|
|
394
|
+
switchLanguage(view, { language: "markdown" });
|
|
395
|
+
await settle();
|
|
396
|
+
|
|
397
|
+
expect(code()).toBe(original);
|
|
398
|
+
expect(status()).toEqual({ status: "idle" });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("keeps typed text that the adapter would normalize", async () => {
|
|
402
|
+
const { view, type } = setup({
|
|
403
|
+
adapters: true,
|
|
404
|
+
initial: withCells(document(), {
|
|
405
|
+
c1: cell("c1", 'mo.md("""\nhi\n""")'),
|
|
406
|
+
}),
|
|
407
|
+
});
|
|
408
|
+
await settle();
|
|
409
|
+
|
|
410
|
+
type("\n");
|
|
411
|
+
await settle();
|
|
412
|
+
type(" ");
|
|
413
|
+
await settle();
|
|
414
|
+
|
|
415
|
+
expect(view.state.doc.toString()).toBe("hi\n ");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("keeps the language a person chose when the cell changes otherwise", async () => {
|
|
419
|
+
const { store, model, view } = setup({
|
|
420
|
+
adapters: true,
|
|
421
|
+
initial: withCells(document(), {
|
|
422
|
+
c1: cell("c1", 'mo.md("""\nhi\n""")'),
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
425
|
+
await settle();
|
|
426
|
+
switchLanguage(view, { language: "python" });
|
|
427
|
+
await settle();
|
|
428
|
+
|
|
429
|
+
store.set(model.editAtom, (tx) => tx.setName("c1", "renamed"));
|
|
430
|
+
await settle();
|
|
431
|
+
|
|
432
|
+
expect(language(view)).toBe("python");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("edits SQL as SQL and keeps its output name", async () => {
|
|
436
|
+
const { store, source, view, code, type } = setup({ adapters: true });
|
|
437
|
+
await settle();
|
|
438
|
+
|
|
439
|
+
store.set(
|
|
440
|
+
source,
|
|
441
|
+
withCells(document(), {
|
|
442
|
+
c1: cell("c1", 'df = mo.sql(f"""SELECT 1""")'),
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
await settle();
|
|
446
|
+
expect(language(view)).toBe("sql");
|
|
447
|
+
expect(view.state.doc.toString()).toBe("SELECT 1");
|
|
448
|
+
|
|
449
|
+
type(" LIMIT 1");
|
|
450
|
+
expect(readBack(view, code())).toMatchObject({
|
|
451
|
+
text: "SELECT 1 LIMIT 1",
|
|
452
|
+
metadata: { dataframeName: "df" },
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("sets the SQL dialect from the host's engine", async () => {
|
|
457
|
+
const { store, source } = setup({ adapters: true });
|
|
458
|
+
await settle();
|
|
459
|
+
const dialect = vi.spyOn(SCHEMA_CACHE, "getDialect");
|
|
460
|
+
try {
|
|
461
|
+
store.set(
|
|
462
|
+
source,
|
|
463
|
+
withCells(document(), {
|
|
464
|
+
c1: cell("c1", '_df = mo.sql(f"""SELECT 1""", engine=warehouse)'),
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
await settle();
|
|
468
|
+
expect(dialect).toHaveBeenLastCalledWith("warehouse");
|
|
469
|
+
|
|
470
|
+
store.set(
|
|
471
|
+
source,
|
|
472
|
+
withCells(document(), {
|
|
473
|
+
c1: cell("c1", '_df = mo.sql(f"""SELECT 1""", engine=lake)'),
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
await settle();
|
|
477
|
+
expect(dialect).toHaveBeenLastCalledWith("lake");
|
|
478
|
+
} finally {
|
|
479
|
+
dialect.mockRestore();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|