@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61
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-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- 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-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.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-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -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/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- 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/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- 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/components/tracing/tracing.tsx +2 -1
- 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 +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- 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/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- 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/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- 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/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- 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/mount.tsx +3 -3
- 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 +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- 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/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- 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
- package/src/plugins/impl/anywidget/schemas.ts +0 -32
|
@@ -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,133 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom } from "jotai";
|
|
4
|
+
import type { CellId } from "@/core/cells/ids";
|
|
5
|
+
import type { ActiveLineInfo } from "@/core/codemirror/cells/line-timing-decorations";
|
|
6
|
+
import { getRequestClient } from "@/core/network/requests";
|
|
7
|
+
import { store } from "@/core/state/jotai";
|
|
8
|
+
import { Logger } from "@/utils/Logger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* State for the experimental live debugger and line-timing highlight.
|
|
12
|
+
*
|
|
13
|
+
* - `activeLineAtom` holds the cell + line the kernel's frame watcher is
|
|
14
|
+
* currently executing (driven by `active-line` notifications), plus when
|
|
15
|
+
* the frontend first saw execution on that line. Only one cell runs at a
|
|
16
|
+
* time, so a single global value suffices.
|
|
17
|
+
* - `breakpointsAtom` holds the user's gutter breakpoints, session-only. It is
|
|
18
|
+
* the source of truth; mutations are mirrored to the kernel via
|
|
19
|
+
* `sendSetBreakpoints`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface ActiveLine {
|
|
23
|
+
cellId: CellId;
|
|
24
|
+
line: number;
|
|
25
|
+
/** When the frontend first saw execution on this (cellId, line). */
|
|
26
|
+
startedAtMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const activeLineAtom = atom<ActiveLine | null>(null);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update the active line, stamping `startedAtMs` only when the (cellId, line)
|
|
33
|
+
* actually changes, so duplicate notifications (e.g. reconnects) never reset
|
|
34
|
+
* the elapsed-time baseline.
|
|
35
|
+
*/
|
|
36
|
+
export function setActiveLine(
|
|
37
|
+
next: { cellId: CellId; line: number } | null,
|
|
38
|
+
): void {
|
|
39
|
+
const prev = store.get(activeLineAtom);
|
|
40
|
+
if (next === null) {
|
|
41
|
+
store.set(activeLineAtom, null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (prev?.cellId === next.cellId && prev.line === next.line) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
store.set(activeLineAtom, { ...next, startedAtMs: Date.now() });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const breakpointsAtom = atom<ReadonlyMap<CellId, ReadonlySet<number>>>(
|
|
51
|
+
new Map<CellId, ReadonlySet<number>>(),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const EMPTY_LINES: ReadonlySet<number> = new Set();
|
|
55
|
+
|
|
56
|
+
/** Per-cell derived atom: the current debug line for `cellId`, or `null`. */
|
|
57
|
+
export function createDebuggerLineAtom(cellId: CellId) {
|
|
58
|
+
return atom((get) => {
|
|
59
|
+
const current = get(activeLineAtom);
|
|
60
|
+
return current?.cellId === cellId ? current.line : null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Per-cell derived atom: the active line + start time for `cellId`, or `null`. */
|
|
65
|
+
export function createActiveLineInfoAtom(cellId: CellId) {
|
|
66
|
+
return atom((get): ActiveLineInfo | null => {
|
|
67
|
+
const current = get(activeLineAtom);
|
|
68
|
+
return current?.cellId === cellId
|
|
69
|
+
? { line: current.line, startedAtMs: current.startedAtMs }
|
|
70
|
+
: null;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Per-cell derived atom: the breakpoint lines for `cellId`. */
|
|
75
|
+
export function createCellBreakpointsAtom(cellId: CellId) {
|
|
76
|
+
return atom((get) => get(breakpointsAtom).get(cellId) ?? EMPTY_LINES);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sendBreakpoints(map: ReadonlyMap<CellId, ReadonlySet<number>>): void {
|
|
80
|
+
const breakpoints: Record<string, number[]> = {};
|
|
81
|
+
for (const [cellId, lines] of map) {
|
|
82
|
+
if (lines.size > 0) {
|
|
83
|
+
breakpoints[cellId] = [...lines].toSorted((a, b) => a - b);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Fire-and-forget: the request layer rethrows on failure (e.g. static or
|
|
87
|
+
// wasm mode with no live kernel). Swallow it so toggling a breakpoint never
|
|
88
|
+
// surfaces as an unhandled promise rejection.
|
|
89
|
+
getRequestClient()
|
|
90
|
+
.sendSetBreakpoints({ breakpoints })
|
|
91
|
+
.catch((error) => {
|
|
92
|
+
Logger.debug("Failed to sync breakpoints to kernel", { error });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Remove all breakpoints for a cell and sync the full set to the kernel. */
|
|
97
|
+
export function clearCellBreakpoints(cellId: CellId): void {
|
|
98
|
+
const prev = store.get(breakpointsAtom);
|
|
99
|
+
if (!prev.has(cellId)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const next = new Map(prev);
|
|
103
|
+
next.delete(cellId);
|
|
104
|
+
store.set(breakpointsAtom, next);
|
|
105
|
+
sendBreakpoints(next);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Re-push the current breakpoints to a freshly started kernel, whose `MarimoPdb` starts empty. */
|
|
109
|
+
export function resyncBreakpoints(): void {
|
|
110
|
+
const current = store.get(breakpointsAtom);
|
|
111
|
+
if (current.size > 0) {
|
|
112
|
+
sendBreakpoints(current);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Toggle a breakpoint at `(cellId, line)` and sync the full set to the kernel. */
|
|
117
|
+
export function toggleBreakpoint(cellId: CellId, line: number): void {
|
|
118
|
+
const prev = store.get(breakpointsAtom);
|
|
119
|
+
const next = new Map(prev);
|
|
120
|
+
const lines = new Set(next.get(cellId) ?? []);
|
|
121
|
+
if (lines.has(line)) {
|
|
122
|
+
lines.delete(line);
|
|
123
|
+
} else {
|
|
124
|
+
lines.add(line);
|
|
125
|
+
}
|
|
126
|
+
if (lines.size > 0) {
|
|
127
|
+
next.set(cellId, lines);
|
|
128
|
+
} else {
|
|
129
|
+
next.delete(cellId);
|
|
130
|
+
}
|
|
131
|
+
store.set(breakpointsAtom, next);
|
|
132
|
+
sendBreakpoints(next);
|
|
133
|
+
}
|
|
@@ -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,16 @@ 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
|
+
createActiveLineInfoAtom,
|
|
35
|
+
createCellBreakpointsAtom,
|
|
36
|
+
createDebuggerLineAtom,
|
|
37
|
+
} from "./debugger-state";
|
|
38
|
+
import { activeLineTimer } from "./line-timing-decorations";
|
|
28
39
|
import {
|
|
29
40
|
type CodemirrorCellActions,
|
|
30
41
|
cellActionsState,
|
|
@@ -106,11 +117,8 @@ function cellKeymaps({
|
|
|
106
117
|
keybindings.push(
|
|
107
118
|
{
|
|
108
119
|
key: hotkeys.getHotkey("cell.goToDefinition").key,
|
|
109
|
-
preventDefault: true,
|
|
110
|
-
stopPropagation: true,
|
|
111
120
|
run: (ev) => {
|
|
112
|
-
goToDefinitionAtCursorPosition(ev);
|
|
113
|
-
return true;
|
|
121
|
+
return goToDefinitionAtCursorPosition(ev);
|
|
114
122
|
},
|
|
115
123
|
},
|
|
116
124
|
{
|
|
@@ -423,6 +431,8 @@ export function cellBundle({
|
|
|
423
431
|
cellActions: CodemirrorCellActions;
|
|
424
432
|
keymapConfig: KeymapConfig;
|
|
425
433
|
}): Extension[] {
|
|
434
|
+
const debuggerOn = getFeatureFlag("debugger");
|
|
435
|
+
const lineTimingOn = getFeatureFlag("line_timing");
|
|
426
436
|
return [
|
|
427
437
|
cellActionsState.of(cellActions),
|
|
428
438
|
cellIdState.of(cellId),
|
|
@@ -431,6 +441,24 @@ export function cellBundle({
|
|
|
431
441
|
errorLineHighlighter(
|
|
432
442
|
createObservable(createTracebackInfoAtom(cellId), store),
|
|
433
443
|
),
|
|
444
|
+
// Experimental live debugger and line-timing highlight. Gated so there is
|
|
445
|
+
// no gutter/overhead when disabled. Both track the same active line; when
|
|
446
|
+
// both flags are on, the green timing highlight replaces the amber one.
|
|
447
|
+
debuggerOn
|
|
448
|
+
? breakpointGutter(
|
|
449
|
+
cellId,
|
|
450
|
+
createObservable(createCellBreakpointsAtom(cellId), store),
|
|
451
|
+
)
|
|
452
|
+
: [],
|
|
453
|
+
lineTimingOn
|
|
454
|
+
? activeLineTimer(
|
|
455
|
+
createObservable(createActiveLineInfoAtom(cellId), store),
|
|
456
|
+
)
|
|
457
|
+
: debuggerOn
|
|
458
|
+
? debuggerLineHighlighter(
|
|
459
|
+
createObservable(createDebuggerLineAtom(cellId), store),
|
|
460
|
+
)
|
|
461
|
+
: [],
|
|
434
462
|
];
|
|
435
463
|
}
|
|
436
464
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EditorState,
|
|
5
|
+
type Extension,
|
|
6
|
+
RangeSetBuilder,
|
|
7
|
+
} from "@codemirror/state";
|
|
8
|
+
import {
|
|
9
|
+
Decoration,
|
|
10
|
+
type DecorationSet,
|
|
11
|
+
EditorView,
|
|
12
|
+
type PluginValue,
|
|
13
|
+
ViewPlugin,
|
|
14
|
+
type ViewUpdate,
|
|
15
|
+
WidgetType,
|
|
16
|
+
} from "@codemirror/view";
|
|
17
|
+
import type { Observable } from "@/core/state/observable";
|
|
18
|
+
import { Logger } from "@/utils/Logger";
|
|
19
|
+
import { formatElapsedTime } from "@/utils/time";
|
|
20
|
+
|
|
21
|
+
/** The line a cell is currently executing and when execution first hit it. */
|
|
22
|
+
export interface ActiveLineInfo {
|
|
23
|
+
line: number;
|
|
24
|
+
startedAtMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Only show the timer once a line has been busy this long, so short-lived
|
|
28
|
+
// lines never flash a pill.
|
|
29
|
+
const SHOW_TIMER_AFTER_MS = 500;
|
|
30
|
+
const TICK_INTERVAL_MS = 250;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* End-of-line elapsed-time pill for the currently executing line.
|
|
34
|
+
*
|
|
35
|
+
* Ticking is pure DOM mutation on the widget's own interval; no editor
|
|
36
|
+
* transactions are dispatched while the timer counts.
|
|
37
|
+
*/
|
|
38
|
+
class LineTimerWidget extends WidgetType {
|
|
39
|
+
// Track intervals per DOM node: CodeMirror may reuse the node across
|
|
40
|
+
// decoration remaps (`eq` below), and `destroy` only receives the node.
|
|
41
|
+
private static readonly intervals = new WeakMap<HTMLElement, number>();
|
|
42
|
+
|
|
43
|
+
private readonly startedAtMs: number;
|
|
44
|
+
|
|
45
|
+
constructor(startedAtMs: number) {
|
|
46
|
+
super();
|
|
47
|
+
this.startedAtMs = startedAtMs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override eq(other: LineTimerWidget): boolean {
|
|
51
|
+
return other.startedAtMs === this.startedAtMs;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override toDOM(): HTMLElement {
|
|
55
|
+
const el = document.createElement("span");
|
|
56
|
+
el.className = "cm-line-timer";
|
|
57
|
+
el.setAttribute("aria-hidden", "true");
|
|
58
|
+
const tick = () => {
|
|
59
|
+
const elapsed = Date.now() - this.startedAtMs;
|
|
60
|
+
el.textContent =
|
|
61
|
+
elapsed >= SHOW_TIMER_AFTER_MS ? formatElapsedTime(elapsed) : "";
|
|
62
|
+
};
|
|
63
|
+
tick();
|
|
64
|
+
LineTimerWidget.intervals.set(
|
|
65
|
+
el,
|
|
66
|
+
window.setInterval(tick, TICK_INTERVAL_MS),
|
|
67
|
+
);
|
|
68
|
+
return el;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override destroy(dom: HTMLElement): void {
|
|
72
|
+
const interval = LineTimerWidget.intervals.get(dom);
|
|
73
|
+
if (interval !== undefined) {
|
|
74
|
+
window.clearInterval(interval);
|
|
75
|
+
LineTimerWidget.intervals.delete(dom);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override ignoreEvent(): boolean {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createTimingDecorations(
|
|
85
|
+
state: EditorState,
|
|
86
|
+
info: ActiveLineInfo | null,
|
|
87
|
+
): DecorationSet {
|
|
88
|
+
if (info === null || info.line < 1 || info.line > state.doc.lines) {
|
|
89
|
+
return Decoration.none;
|
|
90
|
+
}
|
|
91
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
92
|
+
try {
|
|
93
|
+
const line = state.doc.line(info.line);
|
|
94
|
+
builder.add(
|
|
95
|
+
line.from,
|
|
96
|
+
line.from,
|
|
97
|
+
Decoration.line({ class: "cm-timing-current-line" }),
|
|
98
|
+
);
|
|
99
|
+
builder.add(
|
|
100
|
+
line.to,
|
|
101
|
+
line.to,
|
|
102
|
+
Decoration.widget({
|
|
103
|
+
widget: new LineTimerWidget(info.startedAtMs),
|
|
104
|
+
side: 1,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
Logger.debug("Invalid timing line", { error });
|
|
109
|
+
}
|
|
110
|
+
return builder.finish();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class ActiveLineTimer implements PluginValue {
|
|
114
|
+
private unsubscribe: () => void;
|
|
115
|
+
decorations: DecorationSet;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
view: EditorView,
|
|
119
|
+
infoObservable: Observable<ActiveLineInfo | null>,
|
|
120
|
+
) {
|
|
121
|
+
this.decorations = createTimingDecorations(
|
|
122
|
+
view.state,
|
|
123
|
+
infoObservable.get(),
|
|
124
|
+
);
|
|
125
|
+
this.unsubscribe = infoObservable.sub((info) => {
|
|
126
|
+
this.decorations = createTimingDecorations(view.state, info);
|
|
127
|
+
// Force a re-render; the decoration set changed out of band.
|
|
128
|
+
view.dispatch({ userEvent: "marimo.timing-line-update" });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
update(update: ViewUpdate) {
|
|
133
|
+
this.decorations = this.decorations.map(update.changes);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
destroy() {
|
|
137
|
+
this.unsubscribe();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Highlight the line a cell's frame watcher is currently executing in green,
|
|
143
|
+
* with a dim elapsed-time pill at the end of the line once it has been busy
|
|
144
|
+
* for a while.
|
|
145
|
+
*/
|
|
146
|
+
export function activeLineTimer(
|
|
147
|
+
infoObservable: Observable<ActiveLineInfo | null>,
|
|
148
|
+
): Extension {
|
|
149
|
+
return [
|
|
150
|
+
ViewPlugin.define((view) => new ActiveLineTimer(view, infoObservable), {
|
|
151
|
+
decorations: (plugin) => plugin.decorations,
|
|
152
|
+
}),
|
|
153
|
+
EditorView.theme({
|
|
154
|
+
".cm-timing-current-line": {
|
|
155
|
+
backgroundColor: "color-mix(in srgb, var(--grass-4) 50%, transparent)",
|
|
156
|
+
},
|
|
157
|
+
".cm-line-timer": {
|
|
158
|
+
color: "var(--grass-11)",
|
|
159
|
+
fontSize: "0.85em",
|
|
160
|
+
marginLeft: "1.5ch",
|
|
161
|
+
pointerEvents: "none",
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
@@ -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": {
|