Skip to content

Commit cb0d170

Browse files
committed
Add Debug Adapter Protocol (DAP) support to marimo LSP
Enables debugging marimo notebooks in VS Code by forwarding DAP messages between the VS Code debugger and the LSP server. Just a stub now to wire up everything.
1 parent 3362ee9 commit cb0d170

File tree

7 files changed

+243
-0
lines changed

7 files changed

+243
-0
lines changed

extension/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,35 @@
2020
"title": "Marimo",
2121
"properties": {}
2222
},
23+
"debuggers": [
24+
{
25+
"type": "marimo",
26+
"label": "Marimo Debugger",
27+
"languages": ["python"],
28+
"configurationAttributes": {
29+
"launch": {
30+
"properties": {
31+
"notebookUri": {
32+
"type": "string",
33+
"description": "URI of the notebook to debug"
34+
}
35+
}
36+
}
37+
},
38+
"initialConfigurations": [
39+
{
40+
"type": "marimo",
41+
"request": "launch",
42+
"name": "Debug Marimo Notebook"
43+
}
44+
]
45+
}
46+
],
47+
"breakpoints": [
48+
{
49+
"language": "python"
50+
}
51+
],
2352
"notebooks": [
2453
{
2554
"type": "marimo-notebook",

extension/src/debugAdapter.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as vscode from "vscode";
2+
import * as lsp from "vscode-languageclient";
3+
import type { BaseLanguageClient } from "vscode-languageclient";
4+
5+
import { notebookType } from "./types.ts";
6+
import { Logger } from "./logging.ts";
7+
import { executeCommand } from "./commands.ts";
8+
9+
export function debugAdapter(
10+
client: BaseLanguageClient,
11+
options: { signal: AbortSignal },
12+
) {
13+
Logger.info("Debug.Init", "Registering debug adapter");
14+
15+
const disposeFactory = vscode.debug.registerDebugAdapterDescriptorFactory(
16+
"marimo",
17+
{
18+
createDebugAdapterDescriptor: createDebugAdapterDescriptor.bind(
19+
null,
20+
client,
21+
),
22+
},
23+
);
24+
25+
const disposeProvider = vscode.debug.registerDebugConfigurationProvider(
26+
"marimo",
27+
{
28+
resolveDebugConfiguration(_folder, config) {
29+
Logger.info("Debug.Config", "Resolving debug configuration", {
30+
config,
31+
});
32+
33+
const notebook = vscode.window.activeNotebookEditor?.notebook;
34+
if (!notebook || notebook.notebookType !== notebookType) {
35+
Logger.warn("Debug.Config", "No active marimo notebook found");
36+
return undefined;
37+
}
38+
config.type = "marimo";
39+
config.name = config.name ?? "Debug Marimo";
40+
config.request = config.request ?? "launch";
41+
config.notebookUri = notebook.uri.toString();
42+
43+
Logger.info("Debug.Config", "Configuration resolved", {
44+
notebookUri: config.notebookUri,
45+
type: config.type,
46+
request: config.request,
47+
});
48+
return config;
49+
},
50+
},
51+
);
52+
53+
options.signal.addEventListener("abort", () => {
54+
Logger.info("Debug.Cleanup", "Disposing debug adapter");
55+
disposeFactory.dispose();
56+
disposeProvider.dispose();
57+
});
58+
}
59+
60+
function createDebugAdapterDescriptor(
61+
client: lsp.BaseLanguageClient,
62+
session: vscode.DebugSession,
63+
): vscode.DebugAdapterDescriptor {
64+
Logger.info("Debug.Factory", "Creating debug adapter", {
65+
sessionId: session.id,
66+
name: session.name,
67+
type: session.type,
68+
configuration: session.configuration,
69+
});
70+
71+
const sendMessage = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
72+
const disposer = client.onNotification(
73+
"marimo/dap",
74+
({ sessionId, message }) => {
75+
Logger.debug("Debug.Receive", "Received DAP response from LSP", {
76+
sessionId,
77+
message,
78+
});
79+
if (sessionId === session.id) {
80+
sendMessage.fire(message);
81+
}
82+
},
83+
);
84+
85+
return new vscode.DebugAdapterInlineImplementation({
86+
onDidSendMessage: sendMessage.event,
87+
handleMessage(message) {
88+
Logger.debug("Debug.Send", "Sending DAP message to LSP", {
89+
sessionId: session.id,
90+
message,
91+
});
92+
executeCommand(client, {
93+
command: "marimo.dap",
94+
params: {
95+
sessionId: session.id,
96+
notebookUri: session.configuration.notebookUri,
97+
message,
98+
},
99+
});
100+
},
101+
dispose() {
102+
disposer.dispose();
103+
},
104+
});
105+
}

extension/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode";
22
import * as lsp from "vscode-languageclient/node";
33

4+
import { debugAdapter } from "./debugAdapter.ts";
45
import { kernelManager } from "./kernelManager.ts";
56
import { languageClient } from "./languageClient.ts";
67
import { notebookSerializer } from "./notebookSerializer.ts";
@@ -17,6 +18,7 @@ export async function activate(context: vscode.ExtensionContext) {
1718

1819
const controller = new AbortController();
1920
const client = languageClient({ signal: controller.signal });
21+
debugAdapter(client, { signal: controller.signal });
2022
kernelManager(client, { signal: controller.signal });
2123
notebookSerializer(client, { signal: controller.signal });
2224

extension/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as vscode from "vscode";
12
import { components as Api } from "@marimo-team/openapi/src/api";
23
import type { NotebookSerialization } from "./schemas.ts";
34

@@ -19,6 +20,11 @@ export type RequestMap = {
1920
>;
2021
"marimo.serialize": { notebook: NotebookSerialization };
2122
"marimo.deserialize": { source: string };
23+
"marimo.dap": {
24+
sessionId: string;
25+
notebookUri: string;
26+
message: vscode.DebugProtocolMessage;
27+
};
2228
};
2329

2430
export const notebookType = "marimo-notebook";

src/marimo_lsp/debug_adapter.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Handler for DAP messages."""
2+
3+
from __future__ import annotations
4+
5+
import typing
6+
7+
import attrs
8+
import cattrs
9+
10+
from marimo_lsp.loggers import get_logger
11+
12+
if typing.TYPE_CHECKING:
13+
from pygls.lsp.server import LanguageServer
14+
15+
from marimo_lsp.session_manager import LspSessionManager
16+
17+
logger = get_logger()
18+
converter = cattrs.Converter()
19+
20+
21+
@attrs.define
22+
class DapRequestMessage:
23+
"""
24+
A generic DAP (Debug Adapter Protocol) request message.
25+
26+
DAP requests follow a standard structure where the command field
27+
determines the action, and arguments contain command-specific parameters
28+
that require further parsing based on the command type.
29+
"""
30+
31+
seq: int
32+
"""Sequence number of the message."""
33+
34+
type: typing.Literal["request"]
35+
"""Message type - always 'request' for DAP requests."""
36+
37+
command: str
38+
"""The command to execute (e.g., 'initialize', 'launch', 'setBreakpoints')."""
39+
40+
arguments: dict | None
41+
"""Command-specific arguments. Should be parsed further in ./debug_adapter.py"""
42+
43+
44+
def handle_debug_adapter_request(
45+
ls: LanguageServer,
46+
manager: LspSessionManager,
47+
*,
48+
notebook_uri: str,
49+
session_id: str,
50+
message: dict,
51+
) -> None:
52+
"""Handle DAP requests."""
53+
request = converter.structure(message, DapRequestMessage)
54+
logger.debug(f"Debug.Send {session_id=}, {request=}")
55+
56+
session = manager.get_session(notebook_uri)
57+
assert session, f"No session in workspace for {notebook_uri}"
58+
59+
ls.protocol.notify(
60+
"marimo/dap",
61+
{
62+
"sessionId": session_id,
63+
"message": {
64+
"type": "response",
65+
"request_seq": request.seq,
66+
"success": True,
67+
"command": request.command,
68+
"request": {},
69+
},
70+
},
71+
)

src/marimo_lsp/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ class ConvertRequest(BaseRequest):
103103
"""The identifier for the text document to convert"""
104104

105105

106+
@attrs.define
107+
class DebugAdapterRequest(BaseRequest):
108+
"""A forwarded DAP request."""
109+
110+
session_id: str
111+
"""A UUID for the debug session."""
112+
113+
notebook_uri: str
114+
"""The URI of the notebook."""
115+
116+
message: dict
117+
"""They DAP message."""
118+
119+
106120
def _camel_to_snake(name: str) -> str:
107121
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
108122
return re.sub(r"([A-Z]+)([A-Z][a-z]*)", r"\1_\2", s1).lower()

src/marimo_lsp/server.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from marimo_lsp.loggers import get_logger
2323
from marimo_lsp.models import (
2424
ConvertRequest,
25+
DebugAdapterRequest,
2526
DeserializeRequest,
2627
RunRequest,
2728
SerializeRequest,
@@ -145,6 +146,21 @@ async def deserialize(args: DeserializeRequest):
145146
converter = MarimoConvert.from_py(args.source)
146147
return dataclasses.asdict(converter.to_ir())
147148

149+
@server.command("marimo.dap")
150+
async def dap(ls: LanguageServer, params: DebugAdapterRequest):
151+
"""Handle DAP messages forwarded from VS Code extension."""
152+
from marimo_lsp.debug_adapter import ( # noqa: PLC0415
153+
handle_debug_adapter_request,
154+
)
155+
156+
return handle_debug_adapter_request(
157+
ls=ls,
158+
manager=manager,
159+
session_id=params.session_id,
160+
notebook_uri=params.notebook_uri,
161+
message=params.message,
162+
)
163+
148164
@server.feature(
149165
lsp.TEXT_DOCUMENT_CODE_ACTION,
150166
lsp.CodeActionOptions(

0 commit comments

Comments
 (0)