@marimo-team/islands 0.23.14-dev39 → 0.23.14-dev45
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/{chat-ui-CO8WuXGb.js → chat-ui-DXPRhRO2.js} +3132 -3132
- package/dist/{code-visibility-CqDnWUEQ.js → code-visibility-D-7iipEs.js} +717 -708
- package/dist/{html-to-image-DLSOS-bE.js → html-to-image-DOqQBSQc.js} +2236 -2208
- package/dist/main.js +998 -987
- package/dist/{process-output-B9ysqk1y.js → process-output-kGk2Jc2x.js} +1 -1
- package/dist/{reveal-component-BtwLhHQU.js → reveal-component-BrJ-d3iB.js} +2 -2
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/app-config/user-config-form.tsx +26 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/code/readonly-python-code.tsx +12 -4
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/MarimoTracebackOutput.tsx +18 -1
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +92 -0
- package/src/core/codemirror/cells/extensions.ts +22 -0
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +1 -0
- 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 +1 -0
- package/src/core/network/types.ts +2 -0
- package/src/core/wasm/bridge.ts +4 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +9 -0
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
2
2
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
3
|
-
import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-
|
|
3
|
+
import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-DOqQBSQc.js";
|
|
4
4
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
5
5
|
import { t as Strings } from "./strings-Dq_j3Rxw.js";
|
|
6
6
|
import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
|
|
@@ -6,10 +6,10 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
6
6
|
import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-BacYv-bE.js";
|
|
7
7
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
8
8
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
9
|
-
import { lt as kioskModeAtom, pt as outputIsStale } from "./html-to-image-
|
|
9
|
+
import { lt as kioskModeAtom, pt as outputIsStale } from "./html-to-image-DOqQBSQc.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
12
|
-
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-
|
|
12
|
+
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-D-7iipEs.js";
|
|
13
13
|
import { X as useDebouncedCallback } from "./input-BSdZp5Ng.js";
|
|
14
14
|
import "./toDate-D1Z7ZXWh.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
package/package.json
CHANGED
|
@@ -45,6 +45,7 @@ export const MockRequestClient = {
|
|
|
45
45
|
openFile: vi.fn().mockResolvedValue({}),
|
|
46
46
|
getUsageStats: vi.fn().mockResolvedValue({}),
|
|
47
47
|
sendPdb: vi.fn().mockResolvedValue({}),
|
|
48
|
+
sendSetBreakpoints: vi.fn().mockResolvedValue({}),
|
|
48
49
|
sendListFiles: vi.fn().mockResolvedValue({ files: [] }),
|
|
49
50
|
sendSearchFiles: vi
|
|
50
51
|
.fn()
|
|
@@ -1288,6 +1288,32 @@ export const UserConfigForm: React.FC = () => {
|
|
|
1288
1288
|
</div>
|
|
1289
1289
|
)}
|
|
1290
1290
|
/>
|
|
1291
|
+
<OverriddenFormField
|
|
1292
|
+
control={form.control}
|
|
1293
|
+
name="experimental.debugger"
|
|
1294
|
+
render={({ field, override }) => (
|
|
1295
|
+
<div className="flex flex-col gap-y-1">
|
|
1296
|
+
<FormItem className={formItemClasses}>
|
|
1297
|
+
<FormLabel className="font-normal">Debugger</FormLabel>
|
|
1298
|
+
<FormControl>
|
|
1299
|
+
<Checkbox
|
|
1300
|
+
data-testid="debugger-checkbox"
|
|
1301
|
+
checked={override.value === true}
|
|
1302
|
+
disabled={override.isOverridden}
|
|
1303
|
+
onCheckedChange={field.onChange}
|
|
1304
|
+
/>
|
|
1305
|
+
</FormControl>
|
|
1306
|
+
</FormItem>
|
|
1307
|
+
<IsOverridden override={override} />
|
|
1308
|
+
<FormDescription>
|
|
1309
|
+
Enable the live debugger: a clickable breakpoint gutter,
|
|
1310
|
+
current-line highlighting while a cell runs, and dropping
|
|
1311
|
+
into <Kbd className="inline">pdb</Kbd> at breakpoints. This
|
|
1312
|
+
change requires a page refresh to take effect.
|
|
1313
|
+
</FormDescription>
|
|
1314
|
+
</div>
|
|
1315
|
+
)}
|
|
1316
|
+
/>
|
|
1291
1317
|
</SettingGroup>
|
|
1292
1318
|
);
|
|
1293
1319
|
}
|
|
@@ -208,7 +208,7 @@ export const DebuggerControls: React.FC<{
|
|
|
208
208
|
</Button>
|
|
209
209
|
</Tooltip>
|
|
210
210
|
{onClear && (
|
|
211
|
-
<Tooltip content="
|
|
211
|
+
<Tooltip content="Quit and clear">
|
|
212
212
|
<Button
|
|
213
213
|
variant="text"
|
|
214
214
|
size="icon"
|
|
@@ -217,7 +217,12 @@ export const DebuggerControls: React.FC<{
|
|
|
217
217
|
buttonClasses,
|
|
218
218
|
"text-(--red-11) hover:text-(--red-11) hover:bg-(--red-2) hover:border-(--red-8)",
|
|
219
219
|
)}
|
|
220
|
-
onClick={
|
|
220
|
+
onClick={() => {
|
|
221
|
+
// Quit the debugger (`q`) before tearing down the console, so
|
|
222
|
+
// the stdin response targets the still-present pdb prompt.
|
|
223
|
+
onSubmit("q");
|
|
224
|
+
onClear();
|
|
225
|
+
}}
|
|
221
226
|
>
|
|
222
227
|
<TrashIcon fontSize={36} className={iconClasses} />
|
|
223
228
|
</Button>
|
|
@@ -76,7 +76,7 @@ export const ReadonlyCode = memo(
|
|
|
76
76
|
language = "python",
|
|
77
77
|
...rest
|
|
78
78
|
} = props;
|
|
79
|
-
const [hideCode, setHideCode] = useState(initiallyHideCode);
|
|
79
|
+
const [hideCode, setHideCode] = useState(!!initiallyHideCode);
|
|
80
80
|
|
|
81
81
|
const extensions = useMemo(
|
|
82
82
|
() => [
|
|
@@ -99,8 +99,8 @@ export const ReadonlyCode = memo(
|
|
|
99
99
|
{insertNewCell && <InsertNewCell code={code} />}
|
|
100
100
|
{showHideCode && (
|
|
101
101
|
<ToggleCodeButton
|
|
102
|
-
hidden={hideCode
|
|
103
|
-
onClick={() => setHideCode(!
|
|
102
|
+
hidden={hideCode}
|
|
103
|
+
onClick={() => setHideCode((prev) => !prev)}
|
|
104
104
|
/>
|
|
105
105
|
)}
|
|
106
106
|
</div>
|
|
@@ -128,7 +128,13 @@ const CopyButton = (props: { text: string }) => {
|
|
|
128
128
|
|
|
129
129
|
return (
|
|
130
130
|
<Tooltip content="Copy code" usePortal={false}>
|
|
131
|
-
<Button
|
|
131
|
+
<Button
|
|
132
|
+
onClick={copy}
|
|
133
|
+
size="xs"
|
|
134
|
+
className="py-0"
|
|
135
|
+
variant="secondary"
|
|
136
|
+
aria-label="Copy code"
|
|
137
|
+
>
|
|
132
138
|
<CopyIcon size={14} strokeWidth={1.5} />
|
|
133
139
|
</Button>
|
|
134
140
|
</Tooltip>
|
|
@@ -143,6 +149,7 @@ const ToggleCodeButton = (props: { hidden: boolean; onClick: () => void }) => {
|
|
|
143
149
|
>
|
|
144
150
|
<Button
|
|
145
151
|
onClick={props.onClick}
|
|
152
|
+
aria-label={props.hidden ? "Show code" : "Hide code"}
|
|
146
153
|
size="xs"
|
|
147
154
|
className="py-0"
|
|
148
155
|
variant="secondary"
|
|
@@ -171,6 +178,7 @@ const InsertNewCell = (props: { code: string }) => {
|
|
|
171
178
|
size="xs"
|
|
172
179
|
className="py-0"
|
|
173
180
|
variant="secondary"
|
|
181
|
+
aria-label="Add code to notebook"
|
|
174
182
|
>
|
|
175
183
|
<PlusIcon size={14} strokeWidth={1.5} />
|
|
176
184
|
</Button>
|
|
@@ -29,6 +29,7 @@ import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
|
|
|
29
29
|
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
30
30
|
import { useIsPendingCut } from "@/core/cells/pending-cut-service";
|
|
31
31
|
import { autocompletionKeymap } from "@/core/codemirror/cm";
|
|
32
|
+
import { clearCellBreakpoints } from "@/core/codemirror/cells/debugger-state";
|
|
32
33
|
import type { LanguageAdapterType } from "@/core/codemirror/language/types";
|
|
33
34
|
import { CSSClasses } from "@/core/constants";
|
|
34
35
|
import { canCollapseOutline } from "@/core/dom/outline";
|
|
@@ -746,6 +747,9 @@ const EditableCellComponent = ({
|
|
|
746
747
|
onRefactorWithAI={handleRefactorWithAI}
|
|
747
748
|
onClear={() => {
|
|
748
749
|
actions.clearCellConsoleOutput({ cellId });
|
|
750
|
+
// The debugger "Clear" (trashcan) also drops this cell's
|
|
751
|
+
// breakpoints; no-op when none are set.
|
|
752
|
+
clearCellBreakpoints(cellId);
|
|
749
753
|
}}
|
|
750
754
|
onSubmitDebugger={(text, index) => {
|
|
751
755
|
actions.setStdinResponse({
|
|
@@ -1211,6 +1215,9 @@ const SetupCellComponent = ({
|
|
|
1211
1215
|
onRefactorWithAI={handleRefactorWithAI}
|
|
1212
1216
|
onClear={() => {
|
|
1213
1217
|
actions.clearCellConsoleOutput({ cellId });
|
|
1218
|
+
// The debugger "Clear" (trashcan) also drops this cell's
|
|
1219
|
+
// breakpoints; no-op when none are set.
|
|
1220
|
+
clearCellBreakpoints(cellId);
|
|
1214
1221
|
}}
|
|
1215
1222
|
onSubmitDebugger={(text, index) => {
|
|
1216
1223
|
actions.setStdinResponse({
|
|
@@ -24,8 +24,10 @@ import { Tooltip } from "@/components/ui/tooltip";
|
|
|
24
24
|
import { getCellEditorView } from "@/core/cells/cells";
|
|
25
25
|
import type { CellId } from "@/core/cells/ids";
|
|
26
26
|
import { SCRATCH_CELL_ID } from "@/core/cells/ids";
|
|
27
|
+
import { toggleBreakpoint } from "@/core/codemirror/cells/debugger-state";
|
|
27
28
|
import { insertDebuggerAtLine } from "@/core/codemirror/editing/debugging";
|
|
28
29
|
import { aiFeaturesEnabledAtom } from "@/core/config/config";
|
|
30
|
+
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
29
31
|
import { getRequestClient } from "@/core/network/requests";
|
|
30
32
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
31
33
|
import { isWasm } from "@/core/wasm/utils";
|
|
@@ -212,7 +214,14 @@ function lastLine(text: string): string {
|
|
|
212
214
|
export const replaceTracebackFilenames = (domNode: DOMNode) => {
|
|
213
215
|
const info = getTracebackInfo(domNode);
|
|
214
216
|
if (info?.kind === "cell") {
|
|
215
|
-
|
|
217
|
+
// With the live debugger enabled, toggle a real gutter breakpoint
|
|
218
|
+
// instead of mutating the user's code with a `breakpoint()` statement.
|
|
219
|
+
const debuggerEnabled = getFeatureFlag("debugger");
|
|
220
|
+
const tooltipContent = debuggerEnabled ? (
|
|
221
|
+
<ToggleBreakpointContent />
|
|
222
|
+
) : (
|
|
223
|
+
<InsertBreakpointContent />
|
|
224
|
+
);
|
|
216
225
|
return (
|
|
217
226
|
<span className="nb">
|
|
218
227
|
<span className="inline-flex items-center">
|
|
@@ -228,6 +237,10 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
|
|
|
228
237
|
>
|
|
229
238
|
<BugPlayIcon
|
|
230
239
|
onClick={() => {
|
|
240
|
+
if (debuggerEnabled && !isStaticNotebook()) {
|
|
241
|
+
toggleBreakpoint(info.cellId, info.lineNumber);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
231
244
|
const view = getCellEditorView(info.cellId);
|
|
232
245
|
if (view) {
|
|
233
246
|
insertDebuggerAtLine(view, info.lineNumber);
|
|
@@ -294,3 +307,7 @@ const InsertBreakpointContent = () => {
|
|
|
294
307
|
</>
|
|
295
308
|
);
|
|
296
309
|
};
|
|
310
|
+
|
|
311
|
+
const ToggleBreakpointContent = () => {
|
|
312
|
+
return <>Toggle a breakpoint at this line</>;
|
|
313
|
+
};
|
|
@@ -218,3 +218,42 @@ describe("transitionCell serialization", () => {
|
|
|
218
218
|
);
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
describe("transitionCell stdin resolution", () => {
|
|
223
|
+
function stdinOutput(): OutputMessage {
|
|
224
|
+
return {
|
|
225
|
+
channel: "stdin",
|
|
226
|
+
mimetype: "text/plain",
|
|
227
|
+
data: "(Pdb) ",
|
|
228
|
+
timestamp: Date.now() as Seconds,
|
|
229
|
+
} as OutputMessage;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
it("resolves a dangling pdb prompt when the cell goes idle", () => {
|
|
233
|
+
// A paused-pdb cell shows an unanswered stdin prompt (the debugger box);
|
|
234
|
+
// once it finishes (e.g. after quitting with `q`), the box is removed.
|
|
235
|
+
const cell = createCellRuntimeState({
|
|
236
|
+
status: "running",
|
|
237
|
+
debuggerActive: true,
|
|
238
|
+
consoleOutputs: [stdinOutput()],
|
|
239
|
+
});
|
|
240
|
+
const next = transitionCell(cell, {
|
|
241
|
+
cell_id: "cell-1",
|
|
242
|
+
status: "idle",
|
|
243
|
+
} as CellMessage);
|
|
244
|
+
expect(next.debuggerActive).toBe(false);
|
|
245
|
+
expect(next.consoleOutputs[0].response).toBe("");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("leaves an unanswered prompt interactive while still running", () => {
|
|
249
|
+
const cell = createCellRuntimeState({
|
|
250
|
+
status: "running",
|
|
251
|
+
consoleOutputs: [stdinOutput()],
|
|
252
|
+
});
|
|
253
|
+
const next = transitionCell(cell, {
|
|
254
|
+
cell_id: "cell-1",
|
|
255
|
+
status: null,
|
|
256
|
+
} as CellMessage);
|
|
257
|
+
expect(next.consoleOutputs[0].response).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
});
|
package/src/core/cells/cell.ts
CHANGED
|
@@ -118,9 +118,11 @@ export function transitionCell(
|
|
|
118
118
|
// Coalesce console outputs, which are streamed during execution.
|
|
119
119
|
let consoleOutputs = cell.consoleOutputs;
|
|
120
120
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
121
|
+
// When the cell is interrupted or finishes (idle), there is no live
|
|
122
|
+
// stdin/pdb session, so resolve any dangling stdin prompt. This removes the
|
|
123
|
+
// debugger box — e.g. after quitting pdb with `q`, which stops the cell
|
|
124
|
+
// (idle) rather than interrupting it.
|
|
125
|
+
if (didInterruptFromThisMessage || message.status === "idle") {
|
|
124
126
|
nextCell.debuggerActive = false;
|
|
125
127
|
consoleOutputs = consoleOutputs.map((output) => {
|
|
126
128
|
if (output.channel === "stdin") {
|
|
@@ -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,92 @@
|
|
|
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
|
+
/** Toggle a breakpoint at `(cellId, line)` and sync the full set to the kernel. */
|
|
76
|
+
export function toggleBreakpoint(cellId: CellId, line: number): void {
|
|
77
|
+
const prev = store.get(breakpointsAtom);
|
|
78
|
+
const next = new Map(prev);
|
|
79
|
+
const lines = new Set(next.get(cellId) ?? []);
|
|
80
|
+
if (lines.has(line)) {
|
|
81
|
+
lines.delete(line);
|
|
82
|
+
} else {
|
|
83
|
+
lines.add(line);
|
|
84
|
+
}
|
|
85
|
+
if (lines.size > 0) {
|
|
86
|
+
next.set(cellId, lines);
|
|
87
|
+
} else {
|
|
88
|
+
next.delete(cellId);
|
|
89
|
+
}
|
|
90
|
+
store.set(breakpointsAtom, next);
|
|
91
|
+
sendBreakpoints(next);
|
|
92
|
+
}
|
|
@@ -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,
|
|
@@ -428,6 +437,19 @@ export function cellBundle({
|
|
|
428
437
|
errorLineHighlighter(
|
|
429
438
|
createObservable(createTracebackInfoAtom(cellId), store),
|
|
430
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
|
+
: [],
|
|
431
453
|
];
|
|
432
454
|
}
|
|
433
455
|
|
|
@@ -11,6 +11,7 @@ export interface ExperimentalFeatures {
|
|
|
11
11
|
rtc_v2: boolean;
|
|
12
12
|
cache_panel: boolean;
|
|
13
13
|
external_agents: boolean;
|
|
14
|
+
debugger: boolean; // Live frame-watching debugger (gutter breakpoints + pdb)
|
|
14
15
|
// Add new feature flags here
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -20,6 +21,7 @@ const defaultValues: ExperimentalFeatures = {
|
|
|
20
21
|
rtc_v2: false,
|
|
21
22
|
cache_panel: false,
|
|
22
23
|
external_agents: import.meta.env.DEV,
|
|
24
|
+
debugger: false,
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export function getFeatureFlag<T extends keyof ExperimentalFeatures>(
|
|
@@ -244,6 +244,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
244
244
|
sendListFiles = throwNotImplemented;
|
|
245
245
|
sendSearchFiles = throwNotImplemented;
|
|
246
246
|
sendPdb = throwNotImplemented;
|
|
247
|
+
sendSetBreakpoints = throwNotImplemented;
|
|
247
248
|
sendCreateFileOrFolder = throwNotImplemented;
|
|
248
249
|
sendDeleteFileOrFolder = throwNotImplemented;
|
|
249
250
|
sendCopyFileOrFolder = throwNotImplemented;
|
|
@@ -108,6 +108,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
108
108
|
sendStdin: "waitForConnectionOpen",
|
|
109
109
|
sendInterrupt: "waitForConnectionOpen",
|
|
110
110
|
sendPdb: "waitForConnectionOpen",
|
|
111
|
+
sendSetBreakpoints: "waitForConnectionOpen",
|
|
111
112
|
sendInstallMissingPackages: "waitForConnectionOpen",
|
|
112
113
|
readSnippets: "waitForConnectionOpen",
|
|
113
114
|
previewDatasetColumn: "waitForConnectionOpen",
|
|
@@ -293,6 +293,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
293
293
|
})
|
|
294
294
|
.then(handleResponseReturnNull);
|
|
295
295
|
},
|
|
296
|
+
sendSetBreakpoints: (request) => {
|
|
297
|
+
return getClient()
|
|
298
|
+
.POST("/api/kernel/pdb/breakpoints", {
|
|
299
|
+
body: request,
|
|
300
|
+
params: getParams(),
|
|
301
|
+
})
|
|
302
|
+
.then(handleResponseReturnNull);
|
|
303
|
+
},
|
|
296
304
|
sendListFiles: async (request) => {
|
|
297
305
|
await waitForConnectionOpen();
|
|
298
306
|
return getClient()
|
|
@@ -68,6 +68,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
68
68
|
sendListFiles: throwNotInEditMode,
|
|
69
69
|
sendSearchFiles: throwNotInEditMode,
|
|
70
70
|
sendPdb: throwNotInEditMode,
|
|
71
|
+
sendSetBreakpoints: throwNotInEditMode,
|
|
71
72
|
sendCreateFileOrFolder: throwNotInEditMode,
|
|
72
73
|
sendDeleteFileOrFolder: throwNotInEditMode,
|
|
73
74
|
sendCopyFileOrFolder: throwNotInEditMode,
|