@marimo-team/islands 0.23.14-dev5 → 0.23.14-dev51
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-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DXPRhRO2.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-rxZi4Phe.js → code-visibility-BA5VF477.js} +1153 -946
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-7wxMGXjG.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-DOqQBSQc.js} +2367 -2301
- package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
- package/dist/main.js +2707 -1186
- package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-kGk2Jc2x.js} +1 -1
- package/dist/{reveal-component-sEb3Wd1x.js → reveal-component-BOL6pLBN.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
- package/package.json +3 -3
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/app-config/user-config-form.tsx +26 -0
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +139 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +100 -0
- package/src/core/codemirror/cells/extensions.ts +23 -4
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/useMarimoKernelConnection.tsx +17 -0
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +53 -2
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +52 -2
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/utils/errors.ts +15 -0
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
5
|
+
import { cellId } from "@/__tests__/branded";
|
|
6
|
+
import { store } from "@/core/state/jotai";
|
|
7
|
+
import {
|
|
8
|
+
breakpointsAtom,
|
|
9
|
+
clearCellBreakpoints,
|
|
10
|
+
createCellBreakpointsAtom,
|
|
11
|
+
createDebuggerLineAtom,
|
|
12
|
+
debuggerCurrentLineAtom,
|
|
13
|
+
resyncBreakpoints,
|
|
14
|
+
toggleBreakpoint,
|
|
15
|
+
} from "../debugger-state";
|
|
16
|
+
|
|
17
|
+
const mockRequestClient = MockRequestClient.create();
|
|
18
|
+
vi.mock("@/core/network/requests", () => ({
|
|
19
|
+
getRequestClient: () => mockRequestClient,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const cell1 = cellId("cell1");
|
|
23
|
+
const cell2 = cellId("cell2");
|
|
24
|
+
|
|
25
|
+
describe("debugger-state", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
store.set(breakpointsAtom, new Map());
|
|
28
|
+
store.set(debuggerCurrentLineAtom, null);
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("toggleBreakpoint", () => {
|
|
33
|
+
it("adds a breakpoint and syncs the full set to the kernel", () => {
|
|
34
|
+
toggleBreakpoint(cell1, 3);
|
|
35
|
+
|
|
36
|
+
expect(store.get(breakpointsAtom).get(cell1)).toEqual(new Set([3]));
|
|
37
|
+
expect(mockRequestClient.sendSetBreakpoints).toHaveBeenCalledWith({
|
|
38
|
+
breakpoints: { [cell1]: [3] },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("removes a breakpoint on second toggle and drops the cell once empty", () => {
|
|
43
|
+
toggleBreakpoint(cell1, 3);
|
|
44
|
+
toggleBreakpoint(cell1, 3);
|
|
45
|
+
|
|
46
|
+
expect(store.get(breakpointsAtom).has(cell1)).toBe(false);
|
|
47
|
+
expect(mockRequestClient.sendSetBreakpoints).toHaveBeenLastCalledWith({
|
|
48
|
+
breakpoints: {},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("sends lines sorted ascending for a cell with multiple breakpoints", () => {
|
|
53
|
+
toggleBreakpoint(cell1, 5);
|
|
54
|
+
toggleBreakpoint(cell1, 2);
|
|
55
|
+
|
|
56
|
+
expect(mockRequestClient.sendSetBreakpoints).toHaveBeenLastCalledWith({
|
|
57
|
+
breakpoints: { [cell1]: [2, 5] },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keeps breakpoints for other cells independent", () => {
|
|
62
|
+
toggleBreakpoint(cell1, 1);
|
|
63
|
+
toggleBreakpoint(cell2, 2);
|
|
64
|
+
|
|
65
|
+
expect(store.get(breakpointsAtom).get(cell1)).toEqual(new Set([1]));
|
|
66
|
+
expect(store.get(breakpointsAtom).get(cell2)).toEqual(new Set([2]));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("clearCellBreakpoints", () => {
|
|
71
|
+
it("removes all breakpoints for a cell and syncs", () => {
|
|
72
|
+
toggleBreakpoint(cell1, 1);
|
|
73
|
+
toggleBreakpoint(cell1, 2);
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
|
|
76
|
+
clearCellBreakpoints(cell1);
|
|
77
|
+
|
|
78
|
+
expect(store.get(breakpointsAtom).has(cell1)).toBe(false);
|
|
79
|
+
expect(mockRequestClient.sendSetBreakpoints).toHaveBeenCalledWith({
|
|
80
|
+
breakpoints: {},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is a no-op when the cell has no breakpoints", () => {
|
|
85
|
+
clearCellBreakpoints(cell1);
|
|
86
|
+
|
|
87
|
+
expect(mockRequestClient.sendSetBreakpoints).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("resyncBreakpoints", () => {
|
|
92
|
+
it("re-sends the current breakpoint set", () => {
|
|
93
|
+
toggleBreakpoint(cell1, 4);
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
|
|
96
|
+
resyncBreakpoints();
|
|
97
|
+
|
|
98
|
+
expect(mockRequestClient.sendSetBreakpoints).toHaveBeenCalledWith({
|
|
99
|
+
breakpoints: { [cell1]: [4] },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not send when there are no breakpoints", () => {
|
|
104
|
+
resyncBreakpoints();
|
|
105
|
+
|
|
106
|
+
expect(mockRequestClient.sendSetBreakpoints).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("createCellBreakpointsAtom", () => {
|
|
111
|
+
it("returns the empty set for a cell with no breakpoints", () => {
|
|
112
|
+
const atom = createCellBreakpointsAtom(cell1);
|
|
113
|
+
expect(store.get(atom)).toEqual(new Set());
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("reflects breakpoints for its own cell only", () => {
|
|
117
|
+
toggleBreakpoint(cell1, 7);
|
|
118
|
+
const cell1Atom = createCellBreakpointsAtom(cell1);
|
|
119
|
+
const cell2Atom = createCellBreakpointsAtom(cell2);
|
|
120
|
+
|
|
121
|
+
expect(store.get(cell1Atom)).toEqual(new Set([7]));
|
|
122
|
+
expect(store.get(cell2Atom)).toEqual(new Set());
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("createDebuggerLineAtom", () => {
|
|
127
|
+
it("returns null when no line is set", () => {
|
|
128
|
+
const atom = createDebuggerLineAtom(cell1);
|
|
129
|
+
expect(store.get(atom)).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns the line only for the matching cell", () => {
|
|
133
|
+
store.set(debuggerCurrentLineAtom, { cellId: cell1, line: 10 });
|
|
134
|
+
|
|
135
|
+
expect(store.get(createDebuggerLineAtom(cell1))).toBe(10);
|
|
136
|
+
expect(store.get(createDebuggerLineAtom(cell2))).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EditorState,
|
|
5
|
+
type Extension,
|
|
6
|
+
RangeSet,
|
|
7
|
+
RangeSetBuilder,
|
|
8
|
+
StateEffect,
|
|
9
|
+
StateField,
|
|
10
|
+
} from "@codemirror/state";
|
|
11
|
+
import {
|
|
12
|
+
Decoration,
|
|
13
|
+
type DecorationSet,
|
|
14
|
+
EditorView,
|
|
15
|
+
gutter,
|
|
16
|
+
GutterMarker,
|
|
17
|
+
type PluginValue,
|
|
18
|
+
ViewPlugin,
|
|
19
|
+
type ViewUpdate,
|
|
20
|
+
} from "@codemirror/view";
|
|
21
|
+
import type { CellId } from "@/core/cells/ids";
|
|
22
|
+
import type { Observable } from "@/core/state/observable";
|
|
23
|
+
import { Logger } from "@/utils/Logger";
|
|
24
|
+
import { toggleBreakpoint } from "./debugger-state";
|
|
25
|
+
|
|
26
|
+
// --- Current-line highlight -------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function createLineDecoration(
|
|
29
|
+
state: EditorState,
|
|
30
|
+
line: number | null,
|
|
31
|
+
): DecorationSet {
|
|
32
|
+
if (line == null || line < 1 || line > state.doc.lines) {
|
|
33
|
+
return Decoration.none;
|
|
34
|
+
}
|
|
35
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
36
|
+
try {
|
|
37
|
+
const doc = state.doc.line(line);
|
|
38
|
+
builder.add(
|
|
39
|
+
doc.from,
|
|
40
|
+
doc.from,
|
|
41
|
+
Decoration.line({ class: "cm-debugger-current-line" }),
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
Logger.debug("Invalid debugger line", { error });
|
|
45
|
+
}
|
|
46
|
+
return builder.finish();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class CurrentLineHighlighter implements PluginValue {
|
|
50
|
+
private unsubscribe: () => void;
|
|
51
|
+
decorations: DecorationSet;
|
|
52
|
+
|
|
53
|
+
constructor(view: EditorView, lineObservable: Observable<number | null>) {
|
|
54
|
+
this.decorations = createLineDecoration(view.state, lineObservable.get());
|
|
55
|
+
this.unsubscribe = lineObservable.sub((line) => {
|
|
56
|
+
this.decorations = createLineDecoration(view.state, line);
|
|
57
|
+
// Force a re-render; the decoration set changed out of band.
|
|
58
|
+
view.dispatch({ userEvent: "marimo.debugger-line-update" });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
update(update: ViewUpdate) {
|
|
63
|
+
this.decorations = this.decorations.map(update.changes);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy() {
|
|
67
|
+
this.unsubscribe();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Highlight the line a cell's frame watcher is currently executing. */
|
|
72
|
+
export function debuggerLineHighlighter(
|
|
73
|
+
lineObservable: Observable<number | null>,
|
|
74
|
+
): Extension {
|
|
75
|
+
return [
|
|
76
|
+
ViewPlugin.define(
|
|
77
|
+
(view) => new CurrentLineHighlighter(view, lineObservable),
|
|
78
|
+
{ decorations: (plugin) => plugin.decorations },
|
|
79
|
+
),
|
|
80
|
+
EditorView.theme({
|
|
81
|
+
".cm-debugger-current-line": {
|
|
82
|
+
backgroundColor: "color-mix(in srgb, var(--amber-4) 50%, transparent)",
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Breakpoint gutter ------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
class BreakpointMarker extends GutterMarker {
|
|
91
|
+
override toDOM() {
|
|
92
|
+
const dot = document.createElement("div");
|
|
93
|
+
dot.className = "cm-breakpoint-marker";
|
|
94
|
+
return dot;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const breakpointMarker = new BreakpointMarker();
|
|
99
|
+
|
|
100
|
+
const setBreakpointLines = StateEffect.define<ReadonlySet<number>>();
|
|
101
|
+
|
|
102
|
+
const breakpointLinesField = StateField.define<ReadonlySet<number>>({
|
|
103
|
+
create: () => new Set<number>(),
|
|
104
|
+
update(value, tr) {
|
|
105
|
+
for (const effect of tr.effects) {
|
|
106
|
+
if (effect.is(setBreakpointLines)) {
|
|
107
|
+
return effect.value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function buildBreakpointMarkers(state: EditorState): RangeSet<GutterMarker> {
|
|
115
|
+
const lines = state.field(breakpointLinesField);
|
|
116
|
+
if (lines.size === 0) {
|
|
117
|
+
return RangeSet.empty;
|
|
118
|
+
}
|
|
119
|
+
const builder = new RangeSetBuilder<GutterMarker>();
|
|
120
|
+
for (const lineNo of [...lines].toSorted((a, b) => a - b)) {
|
|
121
|
+
if (lineNo >= 1 && lineNo <= state.doc.lines) {
|
|
122
|
+
const line = state.doc.line(lineNo);
|
|
123
|
+
builder.add(line.from, line.from, breakpointMarker);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return builder.finish();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Keep `breakpointLinesField` in sync with the per-cell breakpoints atom. */
|
|
130
|
+
class BreakpointSync implements PluginValue {
|
|
131
|
+
private unsubscribe: () => void;
|
|
132
|
+
|
|
133
|
+
constructor(view: EditorView, observable: Observable<ReadonlySet<number>>) {
|
|
134
|
+
const apply = (lines: ReadonlySet<number>) => {
|
|
135
|
+
view.dispatch({ effects: setBreakpointLines.of(lines) });
|
|
136
|
+
};
|
|
137
|
+
// Dispatching during view construction is disallowed; defer the initial
|
|
138
|
+
// sync (e.g. for editors that mount after breakpoints already exist).
|
|
139
|
+
// Read the value fresh inside the microtask so a `sub` update that lands
|
|
140
|
+
// first isn't clobbered by a stale snapshot.
|
|
141
|
+
if (observable.get().size > 0) {
|
|
142
|
+
queueMicrotask(() => apply(observable.get()));
|
|
143
|
+
}
|
|
144
|
+
this.unsubscribe = observable.sub(apply);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
destroy() {
|
|
148
|
+
this.unsubscribe();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A clickable gutter for toggling breakpoints on a cell's lines. */
|
|
153
|
+
export function breakpointGutter(
|
|
154
|
+
cellId: CellId,
|
|
155
|
+
breakpointsObservable: Observable<ReadonlySet<number>>,
|
|
156
|
+
): Extension {
|
|
157
|
+
return [
|
|
158
|
+
breakpointLinesField,
|
|
159
|
+
ViewPlugin.define(
|
|
160
|
+
(view) => new BreakpointSync(view, breakpointsObservable),
|
|
161
|
+
),
|
|
162
|
+
gutter({
|
|
163
|
+
class: "cm-breakpoint-gutter",
|
|
164
|
+
markers: (view) => buildBreakpointMarkers(view.state),
|
|
165
|
+
initialSpacer: () => breakpointMarker,
|
|
166
|
+
domEventHandlers: {
|
|
167
|
+
mousedown: (view, line) => {
|
|
168
|
+
const lineNo = view.state.doc.lineAt(line.from).number;
|
|
169
|
+
toggleBreakpoint(cellId, lineNo);
|
|
170
|
+
return true;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
EditorView.theme({
|
|
175
|
+
".cm-breakpoint-gutter": {
|
|
176
|
+
width: "14px",
|
|
177
|
+
cursor: "pointer",
|
|
178
|
+
},
|
|
179
|
+
".cm-breakpoint-marker": {
|
|
180
|
+
width: "8px",
|
|
181
|
+
height: "8px",
|
|
182
|
+
borderRadius: "50%",
|
|
183
|
+
backgroundColor: "var(--red-9)",
|
|
184
|
+
margin: "4px auto",
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
];
|
|
188
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom } from "jotai";
|
|
4
|
+
import type { CellId } from "@/core/cells/ids";
|
|
5
|
+
import { getRequestClient } from "@/core/network/requests";
|
|
6
|
+
import { store } from "@/core/state/jotai";
|
|
7
|
+
import { Logger } from "@/utils/Logger";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* State for the experimental live debugger.
|
|
11
|
+
*
|
|
12
|
+
* - `debuggerCurrentLineAtom` holds the cell + line the kernel's frame watcher
|
|
13
|
+
* is currently executing (driven by `active-line` notifications). Only one
|
|
14
|
+
* cell runs at a time, so a single global value suffices.
|
|
15
|
+
* - `breakpointsAtom` holds the user's gutter breakpoints, session-only. It is
|
|
16
|
+
* the source of truth; mutations are mirrored to the kernel via
|
|
17
|
+
* `sendSetBreakpoints`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface DebuggerLine {
|
|
21
|
+
cellId: CellId;
|
|
22
|
+
line: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const debuggerCurrentLineAtom = atom<DebuggerLine | null>(null);
|
|
26
|
+
|
|
27
|
+
export const breakpointsAtom = atom<ReadonlyMap<CellId, ReadonlySet<number>>>(
|
|
28
|
+
new Map<CellId, ReadonlySet<number>>(),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const EMPTY_LINES: ReadonlySet<number> = new Set();
|
|
32
|
+
|
|
33
|
+
/** Per-cell derived atom: the current debug line for `cellId`, or `null`. */
|
|
34
|
+
export function createDebuggerLineAtom(cellId: CellId) {
|
|
35
|
+
return atom((get) => {
|
|
36
|
+
const current = get(debuggerCurrentLineAtom);
|
|
37
|
+
return current?.cellId === cellId ? current.line : null;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Per-cell derived atom: the breakpoint lines for `cellId`. */
|
|
42
|
+
export function createCellBreakpointsAtom(cellId: CellId) {
|
|
43
|
+
return atom((get) => get(breakpointsAtom).get(cellId) ?? EMPTY_LINES);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sendBreakpoints(map: ReadonlyMap<CellId, ReadonlySet<number>>): void {
|
|
47
|
+
const breakpoints: Record<string, number[]> = {};
|
|
48
|
+
for (const [cellId, lines] of map) {
|
|
49
|
+
if (lines.size > 0) {
|
|
50
|
+
breakpoints[cellId] = [...lines].toSorted((a, b) => a - b);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Fire-and-forget: the request layer rethrows on failure (e.g. static or
|
|
54
|
+
// wasm mode with no live kernel). Swallow it so toggling a breakpoint never
|
|
55
|
+
// surfaces as an unhandled promise rejection.
|
|
56
|
+
getRequestClient()
|
|
57
|
+
.sendSetBreakpoints({ breakpoints })
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
Logger.debug("Failed to sync breakpoints to kernel", { error });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Remove all breakpoints for a cell and sync the full set to the kernel. */
|
|
64
|
+
export function clearCellBreakpoints(cellId: CellId): void {
|
|
65
|
+
const prev = store.get(breakpointsAtom);
|
|
66
|
+
if (!prev.has(cellId)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const next = new Map(prev);
|
|
70
|
+
next.delete(cellId);
|
|
71
|
+
store.set(breakpointsAtom, next);
|
|
72
|
+
sendBreakpoints(next);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Re-push the current breakpoints to a freshly started kernel, whose `MarimoPdb` starts empty. */
|
|
76
|
+
export function resyncBreakpoints(): void {
|
|
77
|
+
const current = store.get(breakpointsAtom);
|
|
78
|
+
if (current.size > 0) {
|
|
79
|
+
sendBreakpoints(current);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Toggle a breakpoint at `(cellId, line)` and sync the full set to the kernel. */
|
|
84
|
+
export function toggleBreakpoint(cellId: CellId, line: number): void {
|
|
85
|
+
const prev = store.get(breakpointsAtom);
|
|
86
|
+
const next = new Map(prev);
|
|
87
|
+
const lines = new Set(next.get(cellId) ?? []);
|
|
88
|
+
if (lines.has(line)) {
|
|
89
|
+
lines.delete(line);
|
|
90
|
+
} else {
|
|
91
|
+
lines.add(line);
|
|
92
|
+
}
|
|
93
|
+
if (lines.size > 0) {
|
|
94
|
+
next.set(cellId, lines);
|
|
95
|
+
} else {
|
|
96
|
+
next.delete(cellId);
|
|
97
|
+
}
|
|
98
|
+
store.set(breakpointsAtom, next);
|
|
99
|
+
sendBreakpoints(next);
|
|
100
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { loroSyncAnnotation } from "@/core/codemirror/rtc/loro/sync";
|
|
18
18
|
import type { KeymapConfig } from "@/core/config/config-schema";
|
|
19
19
|
import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
|
|
20
|
+
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
20
21
|
import { duplicateWithCtrlModifier } from "@/core/hotkeys/shortcuts";
|
|
21
22
|
import { store } from "@/core/state/jotai";
|
|
22
23
|
import { createObservable } from "@/core/state/observable";
|
|
@@ -25,6 +26,14 @@ import { formattingChangeEffect } from "../format";
|
|
|
25
26
|
import { goToDefinitionAtCursorPosition } from "../go-to-definition/utils";
|
|
26
27
|
import { getEditorCodeAsPython } from "../language/utils";
|
|
27
28
|
import { isAtEndOfEditor, isAtStartOfEditor } from "../utils";
|
|
29
|
+
import {
|
|
30
|
+
breakpointGutter,
|
|
31
|
+
debuggerLineHighlighter,
|
|
32
|
+
} from "./debugger-decorations";
|
|
33
|
+
import {
|
|
34
|
+
createCellBreakpointsAtom,
|
|
35
|
+
createDebuggerLineAtom,
|
|
36
|
+
} from "./debugger-state";
|
|
28
37
|
import {
|
|
29
38
|
type CodemirrorCellActions,
|
|
30
39
|
cellActionsState,
|
|
@@ -106,11 +115,8 @@ function cellKeymaps({
|
|
|
106
115
|
keybindings.push(
|
|
107
116
|
{
|
|
108
117
|
key: hotkeys.getHotkey("cell.goToDefinition").key,
|
|
109
|
-
preventDefault: true,
|
|
110
|
-
stopPropagation: true,
|
|
111
118
|
run: (ev) => {
|
|
112
|
-
goToDefinitionAtCursorPosition(ev);
|
|
113
|
-
return true;
|
|
119
|
+
return goToDefinitionAtCursorPosition(ev);
|
|
114
120
|
},
|
|
115
121
|
},
|
|
116
122
|
{
|
|
@@ -431,6 +437,19 @@ export function cellBundle({
|
|
|
431
437
|
errorLineHighlighter(
|
|
432
438
|
createObservable(createTracebackInfoAtom(cellId), store),
|
|
433
439
|
),
|
|
440
|
+
// Experimental live debugger: clickable breakpoint gutter + current-line
|
|
441
|
+
// highlight. Gated so there is no gutter/overhead when disabled.
|
|
442
|
+
getFeatureFlag("debugger")
|
|
443
|
+
? [
|
|
444
|
+
breakpointGutter(
|
|
445
|
+
cellId,
|
|
446
|
+
createObservable(createCellBreakpointsAtom(cellId), store),
|
|
447
|
+
),
|
|
448
|
+
debuggerLineHighlighter(
|
|
449
|
+
createObservable(createDebuggerLineAtom(cellId), store),
|
|
450
|
+
),
|
|
451
|
+
]
|
|
452
|
+
: [],
|
|
434
453
|
];
|
|
435
454
|
}
|
|
436
455
|
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { python } from "@codemirror/lang-python";
|
|
4
4
|
import { EditorState } from "@codemirror/state";
|
|
5
|
-
import { EditorView } from "@codemirror/view";
|
|
6
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
5
|
+
import { EditorView, keymap } from "@codemirror/view";
|
|
6
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
7
7
|
import { cellId, variableName } from "@/__tests__/branded";
|
|
8
8
|
import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
|
|
9
9
|
import { store } from "@/core/state/jotai";
|
|
10
10
|
import { variablesAtom } from "@/core/variables/state";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
goToDefinitionAtCursorPosition,
|
|
13
|
+
goToDefinitionWithLspFallback,
|
|
14
|
+
requestLspGoToDefinition,
|
|
15
|
+
} from "../utils";
|
|
12
16
|
|
|
13
17
|
async function tick(): Promise<void> {
|
|
14
18
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
@@ -181,3 +185,71 @@ print(mymodule)`;
|
|
|
181
185
|
);
|
|
182
186
|
});
|
|
183
187
|
});
|
|
188
|
+
|
|
189
|
+
describe("goToDefinitionWithLspFallback", () => {
|
|
190
|
+
test("falls through to LSP when marimo cannot resolve the symbol", () => {
|
|
191
|
+
const lspGoToDefinition = vi.fn(() => true);
|
|
192
|
+
const view = new EditorView({
|
|
193
|
+
state: EditorState.create({
|
|
194
|
+
doc: "parser.add_argument('--foo')",
|
|
195
|
+
selection: { anchor: "parser.add_argument".indexOf("add_argument") },
|
|
196
|
+
extensions: [
|
|
197
|
+
python(),
|
|
198
|
+
keymap.of([{ key: "F12", run: lspGoToDefinition }]),
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
parent: document.body,
|
|
202
|
+
});
|
|
203
|
+
views.push(view);
|
|
204
|
+
|
|
205
|
+
const result = goToDefinitionWithLspFallback(view);
|
|
206
|
+
|
|
207
|
+
expect(result).toBe(true);
|
|
208
|
+
expect(lspGoToDefinition).toHaveBeenCalledOnce();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("does not invoke LSP when marimo resolves the symbol", async () => {
|
|
212
|
+
const lspGoToDefinition = vi.fn(() => true);
|
|
213
|
+
const code = "a = 10\nprint(a)";
|
|
214
|
+
const view = new EditorView({
|
|
215
|
+
state: EditorState.create({
|
|
216
|
+
doc: code,
|
|
217
|
+
selection: { anchor: code.indexOf("a", 3) },
|
|
218
|
+
extensions: [
|
|
219
|
+
python(),
|
|
220
|
+
keymap.of([{ key: "F12", run: lspGoToDefinition }]),
|
|
221
|
+
],
|
|
222
|
+
}),
|
|
223
|
+
parent: document.body,
|
|
224
|
+
});
|
|
225
|
+
views.push(view);
|
|
226
|
+
|
|
227
|
+
const result = goToDefinitionWithLspFallback(view);
|
|
228
|
+
|
|
229
|
+
expect(result).toBe(true);
|
|
230
|
+
expect(lspGoToDefinition).not.toHaveBeenCalled();
|
|
231
|
+
await tick();
|
|
232
|
+
expect(view.state.selection.main.head).toBe(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("falls through with a modified shortcut like Ctrl-F12", () => {
|
|
236
|
+
const lspGoToDefinition = vi.fn(() => true);
|
|
237
|
+
const view = new EditorView({
|
|
238
|
+
state: EditorState.create({
|
|
239
|
+
doc: "parser.add_argument('--foo')",
|
|
240
|
+
selection: { anchor: "parser.add_argument".indexOf("add_argument") },
|
|
241
|
+
extensions: [
|
|
242
|
+
python(),
|
|
243
|
+
keymap.of([{ key: "Ctrl-F12", run: lspGoToDefinition }]),
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
parent: document.body,
|
|
247
|
+
});
|
|
248
|
+
views.push(view);
|
|
249
|
+
|
|
250
|
+
const result = requestLspGoToDefinition(view, "Ctrl-F12");
|
|
251
|
+
|
|
252
|
+
expect(result).toBe(true);
|
|
253
|
+
expect(lspGoToDefinition).toHaveBeenCalledOnce();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { EditorView } from "@codemirror/view";
|
|
3
3
|
import { createUnderlinePlugin, underlineField } from "./underline";
|
|
4
|
-
import {
|
|
4
|
+
import { goToDefinitionWithLspFallback } from "./utils";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Create a go-to-definition extension.
|
|
@@ -9,8 +9,8 @@ import { goToDefinition } from "./utils";
|
|
|
9
9
|
export function goToDefinitionBundle() {
|
|
10
10
|
return [
|
|
11
11
|
underlineField,
|
|
12
|
-
createUnderlinePlugin((view
|
|
13
|
-
|
|
12
|
+
createUnderlinePlugin((view) => {
|
|
13
|
+
goToDefinitionWithLspFallback(view);
|
|
14
14
|
}),
|
|
15
15
|
EditorView.baseTheme({
|
|
16
16
|
".underline": {
|
|
@@ -55,12 +55,9 @@ class MetaUnderlineVariablePlugin {
|
|
|
55
55
|
private view: EditorView;
|
|
56
56
|
private commandClickMode: boolean;
|
|
57
57
|
private hoveredRange: { from: number; to: number; position: number } | null;
|
|
58
|
-
private onClick: (view: EditorView
|
|
58
|
+
private onClick: (view: EditorView) => void;
|
|
59
59
|
|
|
60
|
-
constructor(
|
|
61
|
-
view: EditorView,
|
|
62
|
-
onClick: (view: EditorView, variableName: string) => void,
|
|
63
|
-
) {
|
|
60
|
+
constructor(view: EditorView, onClick: (view: EditorView) => void) {
|
|
64
61
|
this.view = view;
|
|
65
62
|
this.commandClickMode = false;
|
|
66
63
|
this.hoveredRange = null;
|
|
@@ -191,20 +188,16 @@ class MetaUnderlineVariablePlugin {
|
|
|
191
188
|
// If we have a hovered range, go to it
|
|
192
189
|
private click = (event: MouseEvent) => {
|
|
193
190
|
if (this.hoveredRange) {
|
|
194
|
-
const variableName = this.view.state.doc.sliceString(
|
|
195
|
-
this.hoveredRange.from,
|
|
196
|
-
this.hoveredRange.to,
|
|
197
|
-
);
|
|
198
191
|
event.preventDefault();
|
|
199
192
|
event.stopPropagation();
|
|
200
|
-
|
|
201
|
-
// Move the cursor to the clicked position
|
|
193
|
+
// Move the cursor before resolving so LSP uses the clicked position.
|
|
202
194
|
this.view.dispatch({
|
|
203
195
|
selection: {
|
|
204
196
|
head: this.hoveredRange.position,
|
|
205
197
|
anchor: this.hoveredRange.position,
|
|
206
198
|
},
|
|
207
199
|
});
|
|
200
|
+
this.onClick(this.view);
|
|
208
201
|
}
|
|
209
202
|
};
|
|
210
203
|
|
|
@@ -217,7 +210,5 @@ class MetaUnderlineVariablePlugin {
|
|
|
217
210
|
}
|
|
218
211
|
}
|
|
219
212
|
|
|
220
|
-
export const createUnderlinePlugin = (
|
|
221
|
-
onClick: (view: EditorView, variableName: string) => void,
|
|
222
|
-
) =>
|
|
213
|
+
export const createUnderlinePlugin = (onClick: (view: EditorView) => void) =>
|
|
223
214
|
ViewPlugin.define((view) => new MetaUnderlineVariablePlugin(view, onClick));
|