Skip to content

Commit e4e1d09

Browse files
committed
Clicking filenames in tracebacks opens them in user's editor
1 parent c8f1540 commit e4e1d09

File tree

5 files changed

+98
-20
lines changed

5 files changed

+98
-20
lines changed

frontend/src/components/editor/output/MarimoTracebackOutput.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const MarimoTracebackOutput = ({
108108
Fix with AI
109109
</Button>
110110
)}
111-
{tracebackInfo && !isWasm() && (
111+
{tracebackInfo && tracebackInfo.kind === "cell" && !isWasm() && (
112112
<Tooltip content={"Attach pdb to the exception point."}>
113113
<Button
114114
size="xs"
@@ -180,7 +180,7 @@ function lastLine(text: string): string {
180180

181181
export const replaceTracebackFilenames = (domNode: DOMNode) => {
182182
const info = getTracebackInfo(domNode);
183-
if (info) {
183+
if (info?.kind === "cell") {
184184
const tooltipContent = <InsertBreakpointContent />;
185185
return (
186186
<span className="nb">
@@ -211,6 +211,18 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
211211
</span>
212212
);
213213
}
214+
if (info?.kind === "file") {
215+
return (
216+
<div
217+
className="inline-block cursor-pointer text-destructive hover:underline"
218+
onClick={(_) => {
219+
getRequestClient().openFile({ path: info.filePath });
220+
}}
221+
>
222+
<span className="nb">"{info.filePath}"</span>
223+
</div>
224+
);
225+
}
214226
};
215227

216228
export const replaceTracebackPrefix = (domNode: DOMNode) => {

frontend/src/core/codemirror/cells/traceback-decorations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function createErrorDecorations(state: EditorState, errors: TracebackInfos) {
2929

3030
// Filter and sort errors by line number to ensure they're added in order
3131
const relevantErrors = errors
32-
.filter((error) => error.cellId === cellId)
32+
.filter((error) => error.kind === "cell" && error.cellId === cellId)
3333
.sort((a, b) => a.lineNumber - b.lineNumber);
3434

3535
for (const error of relevantErrors) {

frontend/src/utils/__tests__/traceback.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@ import { extractAllTracebackInfo } from "../traceback";
66
describe("traceback", () => {
77
test("extracts cell-link", () => {
88
const errors = extractAllTracebackInfo(Tracebacks.raw);
9-
expect(errors).toMatchInlineSnapshot(`
9+
expect(
10+
errors[0].kind === "file" &&
11+
errors[0].filePath.endsWith("marimo/_runtime/executor.py"),
12+
).toBe(true);
13+
expect(errors.slice(1)).toMatchInlineSnapshot(`
1014
[
1115
{
1216
"cellId": "Hbol",
17+
"kind": "cell",
1318
"lineNumber": 4,
1419
},
1520
{
1621
"cellId": "Hbol",
22+
"kind": "cell",
1723
"lineNumber": 2,
1824
},
1925
]
@@ -22,10 +28,15 @@ describe("traceback", () => {
2228

2329
test("extracts cell-link from assertion", () => {
2430
const info = extractAllTracebackInfo(Tracebacks.assertion);
25-
expect(info).toMatchInlineSnapshot(`
31+
expect(
32+
info[0].kind === "file" &&
33+
info[0].filePath.endsWith("marimo/_runtime/executor.py"),
34+
).toBe(true);
35+
expect(info.slice(1)).toMatchInlineSnapshot(`
2636
[
2737
{
2838
"cellId": "Hbol",
39+
"kind": "cell",
2940
"lineNumber": 1,
3041
},
3142
]

frontend/src/utils/traceback.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,38 @@ export const elementContainsMarimoCellFile = (domNode: Element) => {
2828
);
2929
};
3030

31-
export interface TracebackInfo {
32-
cellId: CellId;
33-
lineNumber: number;
34-
}
31+
export type TracebackInfo =
32+
| {
33+
kind: "file";
34+
filePath: string;
35+
lineNumber: number;
36+
}
37+
| {
38+
kind: "cell";
39+
cellId: CellId;
40+
lineNumber: number;
41+
};
3542

3643
/**
3744
* Extract the cell id and line number from a traceback DOM node.
45+
*
46+
* Example transformation:
47+
*
48+
* File <span class="nb">"/tmp/marimo_<number>/__marimo__cell_<CellId>.py"</span>
49+
* , line <span class="n">1</span>...
50+
*
51+
* becomes
52+
*
53+
* { kind: "cell", cellId: <CellID>, lineNumber: 1 }
54+
*
55+
* or for files:
56+
*
57+
* File <span class="nb">"/path/to/file.py"</span>
58+
* , line <span class="n">42</span>...
59+
*
60+
* becomes
61+
*
62+
* { kind: "file", filePath: "/path/to/file.py", lineNumber: 42 }
3863
*/
3964
export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null {
4065
// The traceback can be manipulated either in output render or in the pygments
@@ -57,7 +82,7 @@ export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null {
5782
if (
5883
domNode instanceof Element &&
5984
domNode.firstChild instanceof Text &&
60-
elementContainsMarimoCellFile(domNode)
85+
matchesSelector(domNode, "span.nb")
6186
) {
6287
const nextSibling = domNode.next;
6388
if (nextSibling && nextSibling instanceof Text) {
@@ -68,15 +93,22 @@ export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null {
6893
lineSibling.firstChild instanceof Text &&
6994
matchesSelector(lineSibling, "span.m")
7095
) {
71-
const cellId = /__marimo__cell_(\w+)_/.exec(
72-
domNode.firstChild.nodeValue,
73-
)?.[1];
7496
const lineNumber = Number.parseInt(
7597
lineSibling.firstChild.nodeValue || "0",
7698
10,
7799
);
78-
if (cellId && lineNumber) {
79-
return { cellId: cellId as CellId, lineNumber };
100+
if (domNode.firstChild.nodeValue?.includes("__marimo__")) {
101+
const cellId = /__marimo__cell_(\w+)_/.exec(
102+
domNode.firstChild.nodeValue,
103+
)?.[1] as CellId;
104+
if (cellId && lineNumber) {
105+
return { kind: "cell", cellId, lineNumber };
106+
}
107+
} else {
108+
const filePath = /"(.+?)"/.exec(domNode.firstChild.nodeValue)?.[1];
109+
if (filePath && lineNumber) {
110+
return { kind: "file", filePath, lineNumber };
111+
}
80112
}
81113
}
82114
}
@@ -93,11 +125,8 @@ export function extractAllTracebackInfo(traceback: string): TracebackInfo[] {
93125
replace: (domNode) => {
94126
const info = getTracebackInfo(domNode);
95127
if (info) {
96-
infos.push({
97-
cellId: info.cellId,
98-
lineNumber: info.lineNumber,
99-
});
100-
return `${info.cellId}:${info.lineNumber}`;
128+
infos.push(info);
129+
return "dummy";
101130
}
102131
},
103132
});

test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# /// script
2+
# requires-python = ">=3.12"
3+
# dependencies = [
4+
# "marimo",
5+
# ]
6+
# ///
7+
import marimo
8+
9+
__generated_with = "0.16.2"
10+
app = marimo.App(width="full")
11+
12+
13+
@app.cell
14+
def _():
15+
import pathlib
16+
pathlib.Path(5)
17+
return
18+
19+
20+
@app.cell
21+
def _():
22+
return
23+
24+
25+
if __name__ == "__main__":
26+
app.run()

0 commit comments

Comments
 (0)