@marimo-team/frontend 0.19.3-dev8 → 0.19.4-dev0
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-BwPGnX3z.js → CellStatus--kUu6N2K.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-KlUs_Sz3.js → ConnectedDataExplorerComponent-BKJwCHu7.js} +1 -1
- package/dist/assets/{ErrorBoundary-Drf1manw.js → ErrorBoundary-C7JBxSzd.js} +1 -1
- package/dist/assets/{ImperativeModal-q6QlC2aZ.js → ImperativeModal-DVhvP4lH.js} +1 -1
- package/dist/assets/{JsonOutput--AuyEErr.js → JsonOutput-BSGE-MRo.js} +5 -5
- package/dist/assets/{LazyAnyLanguageCodeMirror-jpEDlD0M.js → LazyAnyLanguageCodeMirror-Cp2punaU.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-BZjY8e2w.js → MarimoErrorOutput-CX0SCJOZ.js} +2 -2
- package/dist/assets/{RenderHTML-BTLaM20B.js → RenderHTML-Do_PVqRy.js} +1 -1
- package/dist/assets/VisuallyHidden-B9t3FhTP.js +1 -0
- package/dist/assets/{add-cell-with-ai-BWWVs9qV.js → add-cell-with-ai-manh7kBT.js} +21 -21
- package/dist/assets/{add-database-form-Bw_YRH1r.js → add-database-form-CgkV0MRs.js} +2 -2
- package/dist/assets/agent-panel-D-OmT-rw.js +287 -0
- package/dist/assets/{ai-model-dropdown-BrUOgnWS.js → ai-model-dropdown-DzyBY5VA.js} +1 -1
- package/dist/assets/{alert-dialog-k5KxevGr.js → alert-dialog-jcHA5geR.js} +1 -1
- package/dist/assets/{any-language-editor-DQu1Tt2N.js → any-language-editor-Cm83E7D_.js} +1 -1
- package/dist/assets/{app-config-button-B8CXELx0.js → app-config-button-DC3alCuB.js} +1 -1
- package/dist/assets/button-B8cGZzP5.js +1 -0
- package/dist/assets/{cache-panel-C1So4Zu3.js → cache-panel-1FqnpB9y.js} +1 -1
- package/dist/assets/cell-editor-RHFZmO74.js +23 -0
- package/dist/assets/cell-link-Dqj_nfXA.js +1 -0
- package/dist/assets/{cells-DU3EySUd.js → cells-BNQUQiDS.js} +49 -49
- package/dist/assets/{chat-components-Bc9j9ls4.js → chat-components-CWiXtKu6.js} +1 -1
- package/dist/assets/{chat-display-BrTi6c8V.js → chat-display-CGnOamQG.js} +1 -1
- package/dist/assets/{chat-panel-8Dym5Gv3.js → chat-panel-Dh1M55c9.js} +2 -2
- package/dist/assets/client-CDjmJmVw.js +4 -0
- package/dist/assets/{column-preview-Ck6B_-sQ.js → column-preview-CKxT2s-S.js} +1 -1
- package/dist/assets/{command-B_minI8b.js → command-YPFTinLj.js} +1 -1
- package/dist/assets/{command-palette-BT3u6JBB.js → command-palette-7fVEhKGc.js} +1 -1
- package/dist/assets/common-DJkPpBxC.js +1 -0
- package/dist/assets/config-D6nhy4FA.js +1 -0
- package/dist/assets/context-DHfVoQfl.js +1 -0
- package/dist/assets/{copy-icon-B69c-352.js → copy-icon-jWsqdLn1.js} +1 -1
- package/dist/assets/{datasource-DCvPlnaJ.js → datasource-DerBLc6V.js} +2 -2
- package/dist/assets/{dependency-graph-panel-C9jYZ6pA.js → dependency-graph-panel-Vd-OsVLa.js} +4 -4
- package/dist/assets/{dialog-DUEuLcT2.js → dialog-CF5DtF1E.js} +1 -1
- package/dist/assets/{dist-DOFFh6Ii.js → dist-Dg7UO_Vw.js} +1 -1
- package/dist/assets/{documentation-panel-AsatrTfg.js → documentation-panel-xG2-zpwg.js} +1 -1
- package/dist/assets/{download-PR1bF3g_.js → download-B6EJS7Ar.js} +1 -1
- package/dist/assets/edit-page-7Hkti2j_.js +12 -0
- package/dist/assets/{error-banner-DU5Qb8a8.js → error-banner-DvT0IGDZ.js} +1 -1
- package/dist/assets/{error-panel-D_wVKV6I.js → error-panel-BxBpZYvt.js} +1 -1
- package/dist/assets/{es-CEE_7T0w.js → es-BoHEdemq.js} +1 -1
- package/dist/assets/{field-DDKGFzpC.js → field-Clr_fqUr.js} +1 -1
- package/dist/assets/{file-explorer-panel-DltK8JVp.js → file-explorer-panel-C9K0vIPl.js} +1 -1
- package/dist/assets/{floating-outline-BfdazXWm.js → floating-outline-DCrTuu2G.js} +1 -1
- package/dist/assets/{focus-CtlWIiQr.js → focus-DM53w5BH.js} +1 -1
- package/dist/assets/{form-Cy5TkLzh.js → form-BcKfhfZc.js} +2 -2
- package/dist/assets/{glide-data-editor-D_bRnWfy.js → glide-data-editor-CRb9AiCG.js} +1 -1
- package/dist/assets/{globals-BSLm1nlz.js → globals-Bf30kOQF.js} +1 -1
- package/dist/assets/{home-page-CEnaUutq.js → home-page-BRyNf7fl.js} +2 -2
- package/dist/assets/index-CBMqMxiq.js +43 -0
- package/dist/assets/index-DDc_1b-N.css +2 -0
- package/dist/assets/input-B80Yt1uu.js +1 -0
- package/dist/assets/{kiosk-mode-BnTZR6mM.js → kiosk-mode-P-NYHJID.js} +1 -1
- package/dist/assets/{label-qwandMoh.js → label-CNZLffHW.js} +1 -1
- package/dist/assets/{layout-BTiWDrbh.js → layout-DT91GUei.js} +4 -4
- package/dist/assets/links-D529u6GQ.js +1 -0
- package/dist/assets/{logs-panel-Cnp9tO_1.js → logs-panel-C2dfrRig.js} +1 -1
- package/dist/assets/{markdown-renderer-BSrbBHwX.js → markdown-renderer-BPnVa0ym.js} +2 -2
- package/dist/assets/{mermaid-BPkO79lo.js → mermaid--ZwxKP7u.js} +1 -1
- package/dist/assets/mode-Dq8MKjNR.js +1 -0
- package/dist/assets/{multi-map-fjX9ImVF.js → multi-map-CQd4MZr5.js} +1 -1
- package/dist/assets/name-cell-input-BaEPC7ON.js +1 -0
- package/dist/assets/{outline-panel-DCfj1bI-.js → outline-panel-Cca864H0.js} +1 -1
- package/dist/assets/{packages-panel-BiEckVdM.js → packages-panel-Cy_KAYmq.js} +1 -1
- package/dist/assets/{panels-BXRys72u.js → panels-BzlLZfye.js} +1 -1
- package/dist/assets/{process-output-wGlHkL-Q.js → process-output-Dn1rOp26.js} +1 -1
- package/dist/assets/{readonly-python-code-xbh7G2Y2.js → readonly-python-code-CXeF74Iq.js} +1 -1
- package/dist/assets/{renderShortcut-D0Pei-OA.js → renderShortcut-eU5Hsfml.js} +1 -1
- package/dist/assets/{run-page-BCwJRhCq.js → run-page-CM_n6pXD.js} +1 -1
- package/dist/assets/scratchpad-panel-XCkVY3Hp.js +1 -0
- package/dist/assets/{secrets-panel-CDWmmmBS.js → secrets-panel-BMY6PPth.js} +1 -1
- package/dist/assets/{select-D0g5GnIs.js → select-D9lTzMzP.js} +1 -1
- package/dist/assets/{session-panel-DuQl_oQp.js → session-panel-BDt6Y_mU.js} +1 -1
- package/dist/assets/{slides-component-MkPkpql1.js → slides-component-Dp0Yv5b0.js} +1 -1
- package/dist/assets/{snippets-panel-R_ql6HGu.js → snippets-panel-K-JKJQBf.js} +1 -1
- package/dist/assets/state-DWRZTH2y.js +1 -0
- package/dist/assets/state-JzO-Ni5T.js +1 -0
- package/dist/assets/{switch-CWzL-0WF.js → switch-RowEjq0T.js} +1 -1
- package/dist/assets/{terminal-BWM0fOMh.js → terminal-BhbNfCNw.js} +1 -1
- package/dist/assets/{textarea-CfvBt_Xm.js → textarea-Di1KKcL4.js} +1 -1
- package/dist/assets/{tracing-Kscqc1t3.js → tracing-nvbrZdpf.js} +1 -1
- package/dist/assets/{tracing-panel-BEzOflWc.js → tracing-panel-CTXJaO-A.js} +2 -2
- package/dist/assets/{types-DhuSHMNQ.js → types-CT2U5Ljy.js} +1 -1
- package/dist/assets/{useAddCell-C9lbOVO1.js → useAddCell-COb93CUl.js} +1 -1
- package/dist/assets/{useBoolean-B-A0dyIW.js → useBoolean-B_S7yTZz.js} +1 -1
- package/dist/assets/{useCellActionButton-fsh9MTAX.js → useCellActionButton-D5Zt1dDz.js} +1 -1
- package/dist/assets/{useDateFormatter-CV0QXb5P.js → useDateFormatter-DsANziQR.js} +1 -1
- package/dist/assets/useDeleteCell-DHF_xvAh.js +1 -0
- package/dist/assets/{useDependencyPanelTab-CngFbla0.js → useDependencyPanelTab-D59iW_MD.js} +1 -1
- package/dist/assets/useInterval-BGPIviJp.js +1 -0
- package/dist/assets/{useNotebookActions-D01w160c.js → useNotebookActions-DEl-rH-3.js} +1 -1
- package/dist/assets/{useNumberFormatter-D8ks3oPN.js → useNumberFormatter-FoXhpyAb.js} +1 -1
- package/dist/assets/usePress-DTwIUo40.js +7 -0
- package/dist/assets/useRunCells-CKEmgeKM.js +1 -0
- package/dist/assets/useSplitCell-D9YiO-z5.js +1 -0
- package/dist/assets/{useTheme-DfP1CWaW.js → useTheme-CNj0G_ol.js} +1 -1
- package/dist/assets/utilities.esm-DG4qccZc.js +3 -0
- package/dist/assets/utils-pfqq9IdB.js +1 -0
- package/dist/assets/{vega-component-B8ghmMYW.js → vega-component-C1voDf5W.js} +1 -1
- package/dist/assets/{write-secret-modal-CLm48gMe.js → write-secret-modal-hOetwavI.js} +1 -1
- package/dist/index.html +56 -56
- package/package.json +5 -5
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/mount.test.ts +128 -0
- package/src/components/app-config/__tests__/get-dirty-values.test.ts +1 -1
- package/src/components/app-config/ai-config.tsx +328 -28
- package/src/components/app-config/user-config-form.tsx +10 -3
- package/src/components/chat/acp/agent-panel.tsx +56 -43
- package/src/components/chat/chat-utils.ts +0 -19
- package/src/components/data-table/column-header.tsx +1 -1
- package/src/components/editor/KernelStartupErrorModal.tsx +2 -2
- package/src/components/editor/actions/name-cell-input.tsx +10 -4
- package/src/components/editor/ai/completion-handlers.tsx +1 -1
- package/src/components/editor/alerts/connecting-alert.tsx +33 -6
- package/src/components/editor/chrome/types.ts +2 -4
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -58
- package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +150 -96
- package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +27 -0
- package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +6 -0
- package/src/components/utils/lazy-mount.tsx +29 -8
- package/src/core/ai/ids/ids.ts +12 -4
- package/src/core/cells/cells.ts +2 -0
- package/src/core/cells/scrollCellIntoView.ts +3 -2
- package/src/core/codemirror/cm.ts +2 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +123 -0
- package/src/core/codemirror/lsp/notebook-lsp.ts +44 -4
- package/src/core/codemirror/misc/__tests__/string-braces.test.ts +200 -0
- package/src/core/codemirror/misc/string-braces.ts +37 -0
- package/src/core/config/__tests__/config-schema.test.ts +36 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/export/__tests__/hooks.test.ts +504 -0
- package/src/core/export/hooks.ts +93 -4
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/kernel/__tests__/handlers.test.ts +2 -2
- package/src/core/kernel/state.ts +1 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +1 -1
- package/src/core/network/__tests__/requests-network.test.ts +0 -18
- package/src/core/network/requests-lazy.ts +3 -2
- package/src/core/network/requests-network.ts +10 -7
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +2 -0
- package/src/core/wasm/bridge.ts +1 -0
- package/src/css/globals.css +2 -0
- package/src/hooks/__tests__/useInterval.test.tsx +104 -0
- package/src/hooks/useInterval.ts +32 -6
- package/src/mount.tsx +6 -0
- package/src/plugins/impl/chat/ChatPlugin.tsx +2 -4
- package/src/plugins/impl/chat/chat-ui.tsx +62 -191
- package/src/plugins/impl/chat/types.ts +5 -12
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +3 -1
- package/src/utils/events.ts +1 -0
- package/dist/assets/VisuallyHidden-BodIky8L.js +0 -1
- package/dist/assets/agent-panel-Bm-vW8YL.js +0 -287
- package/dist/assets/button-DuYGqRtX.js +0 -1
- package/dist/assets/cell-editor-Cdtc1m3g.js +0 -23
- package/dist/assets/cell-link-CFAPzUg5.js +0 -1
- package/dist/assets/client-DfkWorYM.js +0 -4
- package/dist/assets/common-jorbwXZC.js +0 -1
- package/dist/assets/config-Ba3eeYri.js +0 -1
- package/dist/assets/context-BAYdLMF_.js +0 -1
- package/dist/assets/edit-page-B1Ed6RKp.js +0 -12
- package/dist/assets/index-DNg7_e7t.js +0 -43
- package/dist/assets/index-__6MNWbe.css +0 -2
- package/dist/assets/input-CaEtLL8p.js +0 -1
- package/dist/assets/links-Bpd4gqTj.js +0 -1
- package/dist/assets/mode-yhfN-4ye.js +0 -1
- package/dist/assets/name-cell-input-CmuWqgFR.js +0 -1
- package/dist/assets/scratchpad-panel-C6PpCYtK.js +0 -1
- package/dist/assets/state-DEHWsmkM.js +0 -1
- package/dist/assets/state-DXAf-ejz.js +0 -1
- package/dist/assets/useDeleteCell-ByImoTpm.js +0 -1
- package/dist/assets/useInterval-DpipYmgs.js +0 -1
- package/dist/assets/usePress-C2LPFxyv.js +0 -7
- package/dist/assets/useRunCells-CmnSPQtM.js +0 -1
- package/dist/assets/useSplitCell-BTH64tve.js +0 -1
- package/dist/assets/utilities.esm-CMQs6YPp.js +0 -3
- package/dist/assets/utils-CJJIceVn.js +0 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {
|
|
3
|
+
Activity,
|
|
4
|
+
type ActivityProps,
|
|
5
|
+
type PropsWithChildren,
|
|
6
|
+
} from "react";
|
|
3
7
|
|
|
4
8
|
interface Props {
|
|
5
9
|
isOpen: boolean;
|
|
@@ -12,13 +16,30 @@ export const LazyMount: React.FC<PropsWithChildren<Props>> = ({
|
|
|
12
16
|
isOpen,
|
|
13
17
|
children,
|
|
14
18
|
}) => {
|
|
15
|
-
const [
|
|
19
|
+
const [hasMountedBefore, setHasMountedBefore] = React.useState(false);
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}, [isOpen, isMounted]);
|
|
21
|
+
if (isOpen && !hasMountedBefore) {
|
|
22
|
+
setHasMountedBefore(true);
|
|
23
|
+
}
|
|
22
24
|
|
|
23
|
-
return
|
|
25
|
+
return hasMountedBefore || isOpen ? children : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wraps a component in an Activity component. It is not mounted until it is open for the first time.
|
|
30
|
+
*/
|
|
31
|
+
export const LazyActivity: React.FC<PropsWithChildren<ActivityProps>> = (
|
|
32
|
+
props,
|
|
33
|
+
) => {
|
|
34
|
+
const [hasMountedBefore, setHasMountedBefore] = React.useState(false);
|
|
35
|
+
|
|
36
|
+
if (props.mode === "visible" && !hasMountedBefore) {
|
|
37
|
+
setHasMountedBefore(true);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hasMountedBefore) {
|
|
41
|
+
return <Activity {...props} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
24
45
|
};
|
package/src/core/ai/ids/ids.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { TypedString } from "@/utils/typed";
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const KNOWN_PROVIDERS = [
|
|
6
6
|
"openai",
|
|
7
7
|
"anthropic",
|
|
8
8
|
"google",
|
|
@@ -15,7 +15,13 @@ export const PROVIDERS = [
|
|
|
15
15
|
"wandb",
|
|
16
16
|
"marimo",
|
|
17
17
|
] as const;
|
|
18
|
-
export type
|
|
18
|
+
export type KnownProviderId = (typeof KNOWN_PROVIDERS)[number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provider ID can be a known provider or a custom string
|
|
22
|
+
* The (string & {}) pattern allows any string while still providing autocomplete for known providers
|
|
23
|
+
*/
|
|
24
|
+
export type ProviderId = KnownProviderId | (string & {});
|
|
19
25
|
|
|
20
26
|
export type ShortModelId = TypedString<"ShortModelId">;
|
|
21
27
|
|
|
@@ -68,6 +74,8 @@ function guessProviderId(id: string): ProviderId {
|
|
|
68
74
|
return "ollama";
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
export function isKnownAIProvider(
|
|
72
|
-
|
|
77
|
+
export function isKnownAIProvider(
|
|
78
|
+
providerId: string,
|
|
79
|
+
): providerId is KnownProviderId {
|
|
80
|
+
return (KNOWN_PROVIDERS as readonly string[]).includes(providerId);
|
|
73
81
|
}
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -1594,6 +1594,8 @@ const cellDataAtoms = splitAtom(
|
|
|
1594
1594
|
);
|
|
1595
1595
|
export const useCellDataAtoms = () => useAtom(cellDataAtoms);
|
|
1596
1596
|
|
|
1597
|
+
export const cellsRuntimeAtom = atom((get) => get(notebookAtom).cellRuntime);
|
|
1598
|
+
|
|
1597
1599
|
export const notebookIsRunningAtom = atom((get) =>
|
|
1598
1600
|
notebookIsRunning(get(notebookAtom)),
|
|
1599
1601
|
);
|
|
@@ -53,8 +53,9 @@ export function focusAndScrollCellIntoView({
|
|
|
53
53
|
Logger.warn("scrollCellIntoView: editor not found", cellId);
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
//
|
|
57
|
-
|
|
56
|
+
// Skip auto-focus if already focused, or if the document doesn't have
|
|
57
|
+
// focus to avoid stealing focus from outside (e.g., when embedded in an iframe)
|
|
58
|
+
if (editor.hasFocus || !document.hasFocus()) {
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -64,6 +64,7 @@ import { getCurrentLanguageAdapter } from "./language/commands";
|
|
|
64
64
|
import { adaptiveLanguageConfiguration } from "./language/extension";
|
|
65
65
|
import { dndBundle } from "./misc/dnd";
|
|
66
66
|
import { pasteBundle } from "./misc/paste";
|
|
67
|
+
import { stringsAutoCloseBraces } from "./misc/string-braces";
|
|
67
68
|
import { reactiveReferencesBundle } from "./reactive-references/extension";
|
|
68
69
|
import { darkTheme } from "./theme/dark";
|
|
69
70
|
import { lightTheme } from "./theme/light";
|
|
@@ -203,6 +204,7 @@ export const basicBundle = (opts: CodeMirrorSetupOpts): Extension[] => {
|
|
|
203
204
|
hintTooltip(lspConfig),
|
|
204
205
|
copilotBundle(completionConfig),
|
|
205
206
|
foldGutter(),
|
|
207
|
+
stringsAutoCloseBraces(),
|
|
206
208
|
closeBrackets(),
|
|
207
209
|
completionKeymap(),
|
|
208
210
|
// to avoid clash with charDeleteBackward keymap
|
|
@@ -756,6 +756,129 @@ describe("NotebookLanguageServerClient", () => {
|
|
|
756
756
|
// Verify that the import cell was not changed
|
|
757
757
|
expect(mockView1.state.doc.toString()).toBe("import marimo as mo");
|
|
758
758
|
});
|
|
759
|
+
|
|
760
|
+
it("should only rename private variables in the current cell (issue #7810)", async () => {
|
|
761
|
+
const props = {
|
|
762
|
+
workspaceFolders: null,
|
|
763
|
+
capabilities: {
|
|
764
|
+
textDocument: {
|
|
765
|
+
rename: {
|
|
766
|
+
prepareSupport: true,
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
languageId: "python",
|
|
771
|
+
transport: {
|
|
772
|
+
sendData: vi.fn(),
|
|
773
|
+
subscribe: vi.fn(),
|
|
774
|
+
connect: vi.fn(),
|
|
775
|
+
transportRequestManager: {
|
|
776
|
+
send: vi.fn(),
|
|
777
|
+
},
|
|
778
|
+
} as any,
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// Setup editor views - both cells have a private variable _x
|
|
782
|
+
const cell1Code = "_x = 1\nprint(_x)";
|
|
783
|
+
const cell2Code = "_x = 2\nprint(_x)";
|
|
784
|
+
|
|
785
|
+
const mockView1 = new EditorView({
|
|
786
|
+
doc: cell1Code,
|
|
787
|
+
extensions: [
|
|
788
|
+
languageAdapterState.init(() => new PythonLanguageAdapter()),
|
|
789
|
+
languageMetadataField.init(() => ({})),
|
|
790
|
+
languageServerWithClient({
|
|
791
|
+
client: mockClient as unknown as LanguageServerClient,
|
|
792
|
+
documentUri: CellDocumentUri.of(Cells.cell1),
|
|
793
|
+
...props,
|
|
794
|
+
}),
|
|
795
|
+
],
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const mockView2 = new EditorView({
|
|
799
|
+
doc: cell2Code,
|
|
800
|
+
extensions: [
|
|
801
|
+
languageAdapterState.init(() => new PythonLanguageAdapter()),
|
|
802
|
+
languageMetadataField.init(() => ({})),
|
|
803
|
+
languageServerWithClient({
|
|
804
|
+
client: mockClient as unknown as LanguageServerClient,
|
|
805
|
+
documentUri: CellDocumentUri.of(Cells.cell2),
|
|
806
|
+
...props,
|
|
807
|
+
}),
|
|
808
|
+
],
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
(notebookClient as any).getNotebookEditors = () => ({
|
|
812
|
+
[Cells.cell1]: mockView1,
|
|
813
|
+
[Cells.cell2]: mockView2,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Update the mock to return the correct codes
|
|
817
|
+
vi.spyOn(store, "get").mockImplementation((atom) => {
|
|
818
|
+
if (atom === topologicalCodesAtom) {
|
|
819
|
+
return {
|
|
820
|
+
cellIds: [Cells.cell1, Cells.cell2],
|
|
821
|
+
codes: {
|
|
822
|
+
[Cells.cell1]: cell1Code,
|
|
823
|
+
[Cells.cell2]: cell2Code,
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return undefined;
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// Setup rename params - renaming _x in cell1
|
|
831
|
+
const renameParams: LSP.RenameParams = {
|
|
832
|
+
textDocument: { uri: CellDocumentUri.of(Cells.cell1) },
|
|
833
|
+
position: { line: 0, character: 0 }, // position of '_x'
|
|
834
|
+
newName: "_y",
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// Open a document first to set up the lens
|
|
838
|
+
await notebookClient.textDocumentDidOpen({
|
|
839
|
+
textDocument: {
|
|
840
|
+
uri: CellDocumentUri.of(Cells.cell1),
|
|
841
|
+
languageId: "python",
|
|
842
|
+
version: 1,
|
|
843
|
+
text: cell1Code,
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// Mock the response from the language server
|
|
848
|
+
// The LSP server would rename _x in BOTH cells (since it sees the merged doc)
|
|
849
|
+
const mockRenameResponse: LSP.WorkspaceEdit = {
|
|
850
|
+
documentChanges: [
|
|
851
|
+
{
|
|
852
|
+
textDocument: {
|
|
853
|
+
uri: "file:///__marimo_notebook__.py",
|
|
854
|
+
version: 1,
|
|
855
|
+
},
|
|
856
|
+
edits: [
|
|
857
|
+
{
|
|
858
|
+
range: {
|
|
859
|
+
start: { line: 0, character: 0 },
|
|
860
|
+
end: { line: 3, character: 10 },
|
|
861
|
+
},
|
|
862
|
+
// LSP renames _x to _y in both cells
|
|
863
|
+
newText: "_y = 1\nprint(_y)\n_y = 2\nprint(_y)",
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
mockClient.textDocumentRename = vi
|
|
871
|
+
.fn()
|
|
872
|
+
.mockResolvedValue(mockRenameResponse);
|
|
873
|
+
|
|
874
|
+
// Call rename
|
|
875
|
+
await notebookClient.textDocumentRename(renameParams);
|
|
876
|
+
|
|
877
|
+
// The fix: only cell1 should be renamed, cell2 should remain unchanged
|
|
878
|
+
// because private variables are cell-local in marimo
|
|
879
|
+
expect(mockView1.state.doc.toString()).toBe("_y = 1\nprint(_y)");
|
|
880
|
+
expect(mockView2.state.doc.toString()).toBe("_x = 2\nprint(_x)");
|
|
881
|
+
});
|
|
759
882
|
});
|
|
760
883
|
|
|
761
884
|
describe("diagnostics handling", () => {
|
|
@@ -9,6 +9,7 @@ import { invariant } from "@/utils/invariant";
|
|
|
9
9
|
import { Logger } from "@/utils/Logger";
|
|
10
10
|
import { LRUCache } from "@/utils/lru";
|
|
11
11
|
import { Objects } from "@/utils/objects";
|
|
12
|
+
import { getPositionAtWordBounds } from "../completion/hints";
|
|
12
13
|
import { topologicalCodesAtom } from "../copilot/getCodes";
|
|
13
14
|
import {
|
|
14
15
|
getEditorCodeAsPython,
|
|
@@ -22,6 +23,14 @@ import {
|
|
|
22
23
|
} from "./types";
|
|
23
24
|
import { getLSPDocument } from "./utils";
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Check if a variable name is private (starts with underscore but not dunder).
|
|
28
|
+
* Private variables in marimo are cell-local and should not be renamed across cells.
|
|
29
|
+
*/
|
|
30
|
+
function isPrivateVariable(name: string): boolean {
|
|
31
|
+
return name.startsWith("_") && !name.startsWith("__");
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
class Snapshotter {
|
|
26
35
|
private documentVersion = 0;
|
|
27
36
|
private readonly getNotebookCode: () => {
|
|
@@ -433,15 +442,46 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
433
442
|
|
|
434
443
|
// Update the code in the plugins manually
|
|
435
444
|
const editors = this.getNotebookEditors();
|
|
436
|
-
|
|
437
|
-
|
|
445
|
+
|
|
446
|
+
// Check if this is a private variable rename (should only affect current cell)
|
|
447
|
+
// Private variables in marimo are cell-local and should not be renamed across cells
|
|
448
|
+
const originEditor = editors[cellId];
|
|
449
|
+
let isPrivateRename = false;
|
|
450
|
+
if (originEditor) {
|
|
451
|
+
// Convert LSP position (line, character) to CodeMirror position
|
|
452
|
+
const line = originEditor.state.doc.line(params.position.line + 1);
|
|
453
|
+
const cmPosition = line.from + params.position.character;
|
|
454
|
+
const { startToken, endToken } = getPositionAtWordBounds(
|
|
455
|
+
originEditor.state.doc,
|
|
456
|
+
cmPosition,
|
|
457
|
+
);
|
|
458
|
+
const originalName = originEditor.state.doc.sliceString(
|
|
459
|
+
startToken,
|
|
460
|
+
endToken,
|
|
461
|
+
);
|
|
462
|
+
isPrivateRename = isPrivateVariable(originalName);
|
|
463
|
+
if (isPrivateRename) {
|
|
464
|
+
Logger.debug(
|
|
465
|
+
"[lsp] Private variable rename detected, limiting to current cell",
|
|
466
|
+
originalName,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const [currentCellId, ev] of Objects.entries(editors)) {
|
|
472
|
+
// For private variable renames, only update the originating cell
|
|
473
|
+
if (isPrivateRename && currentCellId !== cellId) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const newCode = editsToNewCode.get(currentCellId);
|
|
438
478
|
if (newCode == null) {
|
|
439
|
-
Logger.warn("No new code for cell",
|
|
479
|
+
Logger.warn("No new code for cell", currentCellId);
|
|
440
480
|
continue;
|
|
441
481
|
}
|
|
442
482
|
|
|
443
483
|
if (!ev) {
|
|
444
|
-
Logger.warn("No view for plugin",
|
|
484
|
+
Logger.warn("No view for plugin", currentCellId);
|
|
445
485
|
continue;
|
|
446
486
|
}
|
|
447
487
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { python } from "@codemirror/lang-python";
|
|
4
|
+
import { EditorState } from "@codemirror/state";
|
|
5
|
+
import { EditorView } from "@codemirror/view";
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { stringBraceInputHandler } from "../string-braces";
|
|
8
|
+
|
|
9
|
+
function createEditor(
|
|
10
|
+
initialContent: string,
|
|
11
|
+
cursorPosition: number,
|
|
12
|
+
): EditorView {
|
|
13
|
+
const state = EditorState.create({
|
|
14
|
+
doc: initialContent,
|
|
15
|
+
selection: { anchor: cursorPosition },
|
|
16
|
+
extensions: [python()],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const view = new EditorView({
|
|
20
|
+
state,
|
|
21
|
+
parent: document.body,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return view;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("string brace auto-closing", () => {
|
|
28
|
+
let view: EditorView;
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (view) {
|
|
32
|
+
view.destroy();
|
|
33
|
+
if (document.body.contains(view.dom)) {
|
|
34
|
+
view.dom.remove();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should auto-close braces in f-strings", () => {
|
|
40
|
+
view = createEditor('f"hello ', 8);
|
|
41
|
+
const result = stringBraceInputHandler(view, 8, 8, "{");
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(true);
|
|
44
|
+
expect(view.state.doc.toString()).toBe('f"hello {}');
|
|
45
|
+
expect(view.state.selection.main.head).toBe(9);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should auto-close braces in regular double-quoted strings", () => {
|
|
49
|
+
view = createEditor('"hello ', 7);
|
|
50
|
+
const result = stringBraceInputHandler(view, 7, 7, "{");
|
|
51
|
+
|
|
52
|
+
expect(result).toBe(true);
|
|
53
|
+
expect(view.state.doc.toString()).toBe('"hello {}');
|
|
54
|
+
expect(view.state.selection.main.head).toBe(8);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should auto-close braces in rf-strings", () => {
|
|
58
|
+
view = createEditor('rf"hello ', 9);
|
|
59
|
+
const result = stringBraceInputHandler(view, 9, 9, "{");
|
|
60
|
+
|
|
61
|
+
expect(result).toBe(true);
|
|
62
|
+
expect(view.state.doc.toString()).toBe('rf"hello {}');
|
|
63
|
+
expect(view.state.selection.main.head).toBe(10);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should auto-close braces in fr-strings", () => {
|
|
67
|
+
view = createEditor('fr"hello ', 9);
|
|
68
|
+
const result = stringBraceInputHandler(view, 9, 9, "{");
|
|
69
|
+
|
|
70
|
+
expect(result).toBe(true);
|
|
71
|
+
expect(view.state.doc.toString()).toBe('fr"hello {}');
|
|
72
|
+
expect(view.state.selection.main.head).toBe(10);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should auto-close braces in single-quoted strings", () => {
|
|
76
|
+
view = createEditor("'hello ", 7);
|
|
77
|
+
const result = stringBraceInputHandler(view, 7, 7, "{");
|
|
78
|
+
|
|
79
|
+
expect(result).toBe(true);
|
|
80
|
+
expect(view.state.doc.toString()).toBe("'hello {}");
|
|
81
|
+
expect(view.state.selection.main.head).toBe(8);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should auto-close braces in uppercase F-strings", () => {
|
|
85
|
+
view = createEditor('F"hello ', 8);
|
|
86
|
+
const result = stringBraceInputHandler(view, 8, 8, "{");
|
|
87
|
+
|
|
88
|
+
expect(result).toBe(true);
|
|
89
|
+
expect(view.state.doc.toString()).toBe('F"hello {}');
|
|
90
|
+
expect(view.state.selection.main.head).toBe(9);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should auto-close braces in raw strings without f/t", () => {
|
|
94
|
+
view = createEditor('r"hello ', 8);
|
|
95
|
+
const result = stringBraceInputHandler(view, 8, 8, "{");
|
|
96
|
+
|
|
97
|
+
expect(result).toBe(true);
|
|
98
|
+
expect(view.state.doc.toString()).toBe('r"hello {}');
|
|
99
|
+
expect(view.state.selection.main.head).toBe(9);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Handled by other CodeMirror handler(s)
|
|
103
|
+
it("should NOT auto-close braces outside strings", () => {
|
|
104
|
+
view = createEditor("x = ", 4);
|
|
105
|
+
const result = stringBraceInputHandler(view, 4, 4, "{");
|
|
106
|
+
|
|
107
|
+
expect(result).toBe(false);
|
|
108
|
+
expect(view.state.doc.toString()).toBe("x = ");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should NOT auto-close braces when typing other characters", () => {
|
|
112
|
+
view = createEditor('f"hello ', 8);
|
|
113
|
+
const result = stringBraceInputHandler(view, 8, 8, "a");
|
|
114
|
+
|
|
115
|
+
expect(result).toBe(false);
|
|
116
|
+
expect(view.state.doc.toString()).toBe('f"hello ');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should handle braces at the start of string", () => {
|
|
120
|
+
view = createEditor('f"', 2);
|
|
121
|
+
const result = stringBraceInputHandler(view, 2, 2, "{");
|
|
122
|
+
|
|
123
|
+
expect(result).toBe(true);
|
|
124
|
+
expect(view.state.doc.toString()).toBe('f"{}');
|
|
125
|
+
expect(view.state.selection.main.head).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should handle braces in the middle of string content", () => {
|
|
129
|
+
view = createEditor('f"hello world ', 14);
|
|
130
|
+
const result = stringBraceInputHandler(view, 14, 14, "{");
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(true);
|
|
133
|
+
expect(view.state.doc.toString()).toBe('f"hello world {}');
|
|
134
|
+
expect(view.state.selection.main.head).toBe(15);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should handle multiple braces in string", () => {
|
|
138
|
+
view = createEditor('f"hello {} world', 16);
|
|
139
|
+
const result = stringBraceInputHandler(view, 16, 16, "{");
|
|
140
|
+
|
|
141
|
+
expect(result).toBe(true);
|
|
142
|
+
expect(view.state.doc.toString()).toBe('f"hello {} world{}');
|
|
143
|
+
expect(view.state.selection.main.head).toBe(17);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle empty string", () => {
|
|
147
|
+
view = createEditor('f""', 2);
|
|
148
|
+
const result = stringBraceInputHandler(view, 2, 2, "{");
|
|
149
|
+
|
|
150
|
+
expect(result).toBe(true);
|
|
151
|
+
expect(view.state.doc.toString()).toBe('f"{}"');
|
|
152
|
+
expect(view.state.selection.main.head).toBe(3);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should auto-close braces in triple-quoted strings", () => {
|
|
156
|
+
view = createEditor('"""hello ', 9);
|
|
157
|
+
const result = stringBraceInputHandler(view, 9, 9, "{");
|
|
158
|
+
|
|
159
|
+
expect(result).toBe(true);
|
|
160
|
+
expect(view.state.doc.toString()).toBe('"""hello {}');
|
|
161
|
+
expect(view.state.selection.main.head).toBe(10);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should auto-close braces in triple-quoted f-strings", () => {
|
|
165
|
+
view = createEditor('f"""hello ', 10);
|
|
166
|
+
const result = stringBraceInputHandler(view, 10, 10, "{");
|
|
167
|
+
|
|
168
|
+
expect(result).toBe(true);
|
|
169
|
+
expect(view.state.doc.toString()).toBe('f"""hello {}');
|
|
170
|
+
expect(view.state.selection.main.head).toBe(11);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should auto-close braces in triple single-quoted strings", () => {
|
|
174
|
+
view = createEditor("'''hello ", 9);
|
|
175
|
+
const result = stringBraceInputHandler(view, 9, 9, "{");
|
|
176
|
+
|
|
177
|
+
expect(result).toBe(true);
|
|
178
|
+
expect(view.state.doc.toString()).toBe("'''hello {}");
|
|
179
|
+
expect(view.state.selection.main.head).toBe(10);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should auto-close braces in triple single-quoted f-strings", () => {
|
|
183
|
+
view = createEditor("f'''hello ", 10);
|
|
184
|
+
const result = stringBraceInputHandler(view, 10, 10, "{");
|
|
185
|
+
|
|
186
|
+
expect(result).toBe(true);
|
|
187
|
+
expect(view.state.doc.toString()).toBe("f'''hello {}");
|
|
188
|
+
expect(view.state.selection.main.head).toBe(11);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should NOT auto-close braces when text is selected", () => {
|
|
192
|
+
view = createEditor('f"hello world"', 8);
|
|
193
|
+
// User has selected "world" (from position 8 to 13)
|
|
194
|
+
const result = stringBraceInputHandler(view, 8, 13, "{");
|
|
195
|
+
|
|
196
|
+
expect(result).toBe(false);
|
|
197
|
+
// Document should remain unchanged since we return false
|
|
198
|
+
expect(view.state.doc.toString()).toBe('f"hello world"');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
import type { Extension } from "@codemirror/state";
|
|
4
|
+
import { EditorView } from "@codemirror/view";
|
|
5
|
+
|
|
6
|
+
export function stringBraceInputHandler(
|
|
7
|
+
view: EditorView,
|
|
8
|
+
from: number,
|
|
9
|
+
to: number,
|
|
10
|
+
text: string,
|
|
11
|
+
): boolean {
|
|
12
|
+
if (text !== "{") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (from !== to) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tree = syntaxTree(view.state);
|
|
21
|
+
const node = tree.resolveInner(from, -1);
|
|
22
|
+
|
|
23
|
+
if (!node?.type.name.includes("String")) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
view.dispatch({
|
|
28
|
+
changes: { from, to, insert: "{}" },
|
|
29
|
+
selection: { anchor: from + 1 },
|
|
30
|
+
userEvent: "input.type",
|
|
31
|
+
});
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function stringsAutoCloseBraces(): Extension {
|
|
36
|
+
return EditorView.inputHandler.of(stringBraceInputHandler);
|
|
37
|
+
}
|
|
@@ -45,6 +45,7 @@ test("default UserConfig - empty", () => {
|
|
|
45
45
|
expect(defaultConfig).toMatchInlineSnapshot(`
|
|
46
46
|
{
|
|
47
47
|
"ai": {
|
|
48
|
+
"custom_providers": {},
|
|
48
49
|
"inline_tooltip": false,
|
|
49
50
|
"mode": "manual",
|
|
50
51
|
"models": {
|
|
@@ -114,6 +115,7 @@ test("default UserConfig - one level", () => {
|
|
|
114
115
|
expect(defaultConfig).toMatchInlineSnapshot(`
|
|
115
116
|
{
|
|
116
117
|
"ai": {
|
|
118
|
+
"custom_providers": {},
|
|
117
119
|
"inline_tooltip": false,
|
|
118
120
|
"mode": "manual",
|
|
119
121
|
"models": {
|
|
@@ -198,6 +200,40 @@ test("default UserConfig with additional information", () => {
|
|
|
198
200
|
);
|
|
199
201
|
});
|
|
200
202
|
|
|
203
|
+
test("UserConfig with custom_providers", () => {
|
|
204
|
+
const config = UserConfigSchema.parse({
|
|
205
|
+
ai: {
|
|
206
|
+
custom_providers: {
|
|
207
|
+
my_provider: {
|
|
208
|
+
api_key: "test-key",
|
|
209
|
+
base_url: "https://api.example.com/v1",
|
|
210
|
+
},
|
|
211
|
+
another_provider: {
|
|
212
|
+
base_url: "https://api.another.com/v1",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(config.ai?.custom_providers).toEqual({
|
|
219
|
+
my_provider: {
|
|
220
|
+
api_key: "test-key",
|
|
221
|
+
base_url: "https://api.example.com/v1",
|
|
222
|
+
},
|
|
223
|
+
another_provider: {
|
|
224
|
+
base_url: "https://api.another.com/v1",
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("UserConfig custom_providers defaults to empty object", () => {
|
|
230
|
+
const config = UserConfigSchema.parse({
|
|
231
|
+
ai: {},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(config.ai?.custom_providers).toEqual({});
|
|
235
|
+
});
|
|
236
|
+
|
|
201
237
|
test("resolvedMarimoConfigAtom overrides correctly and does not mutate the original array", () => {
|
|
202
238
|
const initialUserConfig = {
|
|
203
239
|
completion: {
|
|
@@ -175,6 +175,7 @@ export const UserConfigSchema = z
|
|
|
175
175
|
aws_secret_access_key: z.string().optional(),
|
|
176
176
|
})
|
|
177
177
|
.optional(),
|
|
178
|
+
custom_providers: z.record(z.string(), AiConfigSchema).prefault({}),
|
|
178
179
|
models: AiModelsSchema.prefault({
|
|
179
180
|
displayed_models: [],
|
|
180
181
|
custom_models: [],
|