Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions frontend/src/components/editor/output/MarimoTracebackOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const MarimoTracebackOutput = ({
Fix with AI
</Button>
)}
{tracebackInfo && !isWasm() && (
{tracebackInfo && tracebackInfo.kind === "cell" && !isWasm() && (
<Tooltip content={"Attach pdb to the exception point."}>
<Button
size="xs"
Expand Down Expand Up @@ -180,7 +180,7 @@ function lastLine(text: string): string {

export const replaceTracebackFilenames = (domNode: DOMNode) => {
const info = getTracebackInfo(domNode);
if (info) {
if (info?.kind === "cell") {
const tooltipContent = <InsertBreakpointContent />;
return (
<span className="nb">
Expand Down Expand Up @@ -211,6 +211,21 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
</span>
);
}
if (info?.kind === "file") {
return (
<div
className="inline-block cursor-pointer text-destructive hover:underline"
onClick={(_) => {
getRequestClient().openFile({
path: info.filePath,
lineNumber: info.lineNumber,
});
}}
>
<span className="nb">"{info.filePath}"</span>
</div>
);
}
};

export const replaceTracebackPrefix = (domNode: DOMNode) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function createErrorDecorations(state: EditorState, errors: TracebackInfos) {

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

for (const error of relevantErrors) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/core/network/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export interface EditRequests {
request: PreviewDataSourceConnectionRequest,
) => Promise<null>;
validateSQL: (request: ValidateSQLRequest) => Promise<null>;
openFile: (request: { path: string }) => Promise<null>;
openFile: (request: { path: string; lineNumber?: number }) => Promise<null>;
getUsageStats: () => Promise<UsageResponse>;
// Debugger
sendPdb: (request: PdbRequest) => Promise<null>;
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/utils/__tests__/traceback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ import { extractAllTracebackInfo } from "../traceback";
describe("traceback", () => {
test("extracts cell-link", () => {
const errors = extractAllTracebackInfo(Tracebacks.raw);
expect(errors).toMatchInlineSnapshot(`
expect(
errors[0].kind === "file" &&
errors[0].filePath.endsWith("marimo/_runtime/executor.py"),
).toBe(true);
expect(errors.slice(1)).toMatchInlineSnapshot(`
[
{
"cellId": "Hbol",
"kind": "cell",
"lineNumber": 4,
},
{
"cellId": "Hbol",
"kind": "cell",
"lineNumber": 2,
},
]
Expand All @@ -22,10 +28,15 @@ describe("traceback", () => {

test("extracts cell-link from assertion", () => {
const info = extractAllTracebackInfo(Tracebacks.assertion);
expect(info).toMatchInlineSnapshot(`
expect(
info[0].kind === "file" &&
info[0].filePath.endsWith("marimo/_runtime/executor.py"),
).toBe(true);
expect(info.slice(1)).toMatchInlineSnapshot(`
[
{
"cellId": "Hbol",
"kind": "cell",
"lineNumber": 1,
},
]
Expand Down
59 changes: 44 additions & 15 deletions frontend/src/utils/traceback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,38 @@ export const elementContainsMarimoCellFile = (domNode: Element) => {
);
};

export interface TracebackInfo {
cellId: CellId;
lineNumber: number;
}
export type TracebackInfo =
| {
kind: "file";
filePath: string;
lineNumber: number;
}
| {
kind: "cell";
cellId: CellId;
lineNumber: number;
};

/**
* Extract the cell id and line number from a traceback DOM node.
*
* Example transformation:
*
* File <span class="nb">"/tmp/marimo_<number>/__marimo__cell_<CellId>.py"</span>
* , line <span class="n">1</span>...
*
* becomes
*
* { kind: "cell", cellId: <CellID>, lineNumber: 1 }
*
* or for files:
*
* File <span class="nb">"/path/to/file.py"</span>
* , line <span class="n">42</span>...
*
* becomes
*
* { kind: "file", filePath: "/path/to/file.py", lineNumber: 42 }
*/
export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null {
// The traceback can be manipulated either in output render or in the pygments
Expand All @@ -57,7 +82,7 @@ export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null {
if (
domNode instanceof Element &&
domNode.firstChild instanceof Text &&
elementContainsMarimoCellFile(domNode)
matchesSelector(domNode, "span.nb")
) {
const nextSibling = domNode.next;
if (nextSibling && nextSibling instanceof Text) {
Expand All @@ -68,15 +93,22 @@ export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null {
lineSibling.firstChild instanceof Text &&
matchesSelector(lineSibling, "span.m")
) {
const cellId = /__marimo__cell_(\w+)_/.exec(
domNode.firstChild.nodeValue,
)?.[1];
const lineNumber = Number.parseInt(
lineSibling.firstChild.nodeValue || "0",
10,
);
if (cellId && lineNumber) {
return { cellId: cellId as CellId, lineNumber };
if (domNode.firstChild.nodeValue?.includes("__marimo__")) {
const cellId = /__marimo__cell_(\w+)_/.exec(
domNode.firstChild.nodeValue,
)?.[1] as CellId;
if (cellId && lineNumber) {
return { kind: "cell", cellId, lineNumber };
}
} else {
const filePath = /"(.+?)"/.exec(domNode.firstChild.nodeValue)?.[1];
if (filePath && lineNumber) {
return { kind: "file", filePath, lineNumber };
}
}
}
}
Expand All @@ -93,11 +125,8 @@ export function extractAllTracebackInfo(traceback: string): TracebackInfo[] {
replace: (domNode) => {
const info = getTracebackInfo(domNode);
if (info) {
infos.push({
cellId: info.cellId,
lineNumber: info.lineNumber,
});
return `${info.cellId}:${info.lineNumber}`;
infos.push(info);
return "dummy";
}
},
});
Expand Down
2 changes: 1 addition & 1 deletion marimo/_server/api/endpoints/file_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ async def open_file(
body = await parse_request(request, cls=FileOpenRequest)
try:
file_system.get_details(body.path)
success = file_system.open_in_editor(body.path)
success = file_system.open_in_editor(body.path, body.line_number)
return SuccessResponse(success=success)
except Exception as e:
LOGGER.error(f"Error opening file: {e}")
Expand Down
23 changes: 21 additions & 2 deletions marimo/_server/files/os_file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def sort_key(file_info: FileInfo) -> tuple[int, str]:
results.sort(key=sort_key)
return results[:limit]

def open_in_editor(self, path: str) -> bool:
def open_in_editor(self, path: str, line_number: int | None) -> bool:
try:
# First try to get editor from environment variable
editor = os.environ.get("EDITOR")
Expand All @@ -328,9 +328,17 @@ def open_in_editor(self, path: str) -> bool:
# otherwise it silently opens the terminal in the same window that is
# running marimo.
if editor and not _is_terminal_editor(editor):
args = (
[path]
if line_number is None
else editor_open_file_in_line_args(
editor, path, line_number
)
)

try:
# For GUI editors
subprocess.run([editor, path])
subprocess.run([editor, *args])
return True
except Exception as e:
LOGGER.error(f"Error opening with EDITOR: {e}")
Expand All @@ -350,6 +358,17 @@ def open_in_editor(self, path: str) -> bool:
return False


def editor_open_file_in_line_args(
editor: str, path: str, line_number: int
) -> list[str]:
if editor == "code":
return ["--goto", f"{path}:{line_number}"]
elif editor == "subl":
return [f"{path}:{line_number}"]
else:
return [f"+{line_number}", path]


def natural_sort_file(file: FileInfo) -> list[Union[int, str]]:
return natural_sort(file.name)

Expand Down
1 change: 1 addition & 0 deletions marimo/_server/models/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class FileDetailsRequest(msgspec.Struct, rename="camel"):
class FileOpenRequest(msgspec.Struct, rename="camel"):
# The path of the file to open
path: str
line_number: Optional[int] = None


class FileTreeRequest(msgspec.Struct, rename="camel"):
Expand Down
5 changes: 5 additions & 0 deletions packages/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,11 @@ components:
type: object
FileOpenRequest:
properties:
lineNumber:
anyOf:
- type: integer
- type: 'null'
default: null
path:
type: string
required:
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3391,6 +3391,8 @@ export interface components {
};
/** FileOpenRequest */
FileOpenRequest: {
/** @default null */
lineNumber?: number | null;
path: string;
};
/** FileSearchRequest */
Expand Down
Loading