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
31 changes: 31 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@
"title": "Marimo",
"properties": {}
},
"debuggers": [
{
"type": "marimo",
"label": "Marimo Debugger",
"languages": [
"python"
],
"configurationAttributes": {
"launch": {
"properties": {
"notebookUri": {
"type": "string",
"description": "URI of the notebook to debug"
}
}
}
},
"initialConfigurations": [
{
"type": "marimo",
"request": "launch",
"name": "Debug Marimo Notebook"
}
]
}
],
"breakpoints": [
{
"language": "python"
}
],
"notebooks": [
{
"type": "marimo-notebook",
Expand Down
104 changes: 104 additions & 0 deletions extension/src/debugAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as vscode from "vscode";
import type * as lsp from "vscode-languageclient";
import type { BaseLanguageClient } from "vscode-languageclient";
import { executeCommand } from "./commands.ts";
import { Logger } from "./logging.ts";
import { notebookType } from "./types.ts";

export function debugAdapter(
client: BaseLanguageClient,
options: { signal: AbortSignal },
) {
Logger.info("Debug.Init", "Registering debug adapter");

const disposeFactory = vscode.debug.registerDebugAdapterDescriptorFactory(
"marimo",
{
createDebugAdapterDescriptor: createDebugAdapterDescriptor.bind(
null,
client,
),
},
);

const disposeProvider = vscode.debug.registerDebugConfigurationProvider(
"marimo",
{
resolveDebugConfiguration(_folder, config) {
Logger.info("Debug.Config", "Resolving debug configuration", {
config,
});

const notebook = vscode.window.activeNotebookEditor?.notebook;
if (!notebook || notebook.notebookType !== notebookType) {
Logger.warn("Debug.Config", "No active marimo notebook found");
return undefined;
}
config.type = "marimo";
config.name = config.name ?? "Debug Marimo";
config.request = config.request ?? "launch";
config.notebookUri = notebook.uri.toString();

Logger.info("Debug.Config", "Configuration resolved", {
notebookUri: config.notebookUri,
type: config.type,
request: config.request,
});
return config;
},
},
);

options.signal.addEventListener("abort", () => {
Logger.info("Debug.Cleanup", "Disposing debug adapter");
disposeFactory.dispose();
disposeProvider.dispose();
});
}

function createDebugAdapterDescriptor(
client: lsp.BaseLanguageClient,
session: vscode.DebugSession,
): vscode.DebugAdapterDescriptor {
Logger.info("Debug.Factory", "Creating debug adapter", {
sessionId: session.id,
name: session.name,
type: session.type,
configuration: session.configuration,
});

const sendMessage = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
const disposer = client.onNotification(
"marimo/dap",
({ sessionId, message }) => {
Logger.debug("Debug.Receive", "Received DAP response from LSP", {
sessionId,
message,
});
if (sessionId === session.id) {
sendMessage.fire(message);
}
},
);
Comment on lines +71 to +82
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responses from the request handler.


return new vscode.DebugAdapterInlineImplementation({
onDidSendMessage: sendMessage.event,
handleMessage(message) {
Logger.debug("Debug.Send", "Sending DAP message to LSP", {
sessionId: session.id,
message,
});
executeCommand(client, {
command: "marimo.dap",
params: {
sessionId: session.id,
notebookUri: session.configuration.notebookUri,
message,
},
Comment on lines +91 to +97
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom LSP "command" that wraps a DAP request with additional information (for a request). I assume that the handler will manage its own debug sessions.

});
},
dispose() {
disposer.dispose();
},
});
}
3 changes: 3 additions & 0 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as vscode from "vscode";
import * as lsp from "vscode-languageclient/node";

import * as cmds from "./commands.ts";
import { debugAdapter } from "./debugAdapter.ts";
import { kernelManager } from "./kernelManager.ts";
import { languageClient } from "./languageClient.ts";

Expand All @@ -16,6 +18,7 @@ export async function activate(context: vscode.ExtensionContext) {

const controller = new AbortController();
const client = languageClient({ signal: controller.signal });
debugAdapter(client, { signal: controller.signal });
kernelManager(client, { signal: controller.signal });
notebookSerializer(client, { signal: controller.signal });

Expand Down
7 changes: 7 additions & 0 deletions extension/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { components as Api } from "@marimo-team/openapi/src/api";
import type * as vscode from "vscode";

import type { NotebookSerialization } from "./schemas.ts";

type Schemas = Api["schemas"];
Expand All @@ -19,6 +21,11 @@ export type RequestMap = {
>;
"marimo.serialize": { notebook: NotebookSerialization };
"marimo.deserialize": { source: string };
"marimo.dap": {
sessionId: string;
notebookUri: string;
message: vscode.DebugProtocolMessage;
};
};

export const notebookType = "marimo-notebook";
71 changes: 71 additions & 0 deletions src/marimo_lsp/debug_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Handler for DAP messages."""

from __future__ import annotations

import typing

import attrs
import cattrs

from marimo_lsp.loggers import get_logger

if typing.TYPE_CHECKING:
from pygls.lsp.server import LanguageServer

from marimo_lsp.session_manager import LspSessionManager

logger = get_logger()
converter = cattrs.Converter()


@attrs.define
class DapRequestMessage:
"""
A generic DAP (Debug Adapter Protocol) request message.

DAP requests follow a standard structure where the command field
determines the action, and arguments contain command-specific parameters
that require further parsing based on the command type.
"""

seq: int
"""Sequence number of the message."""

type: typing.Literal["request"]
"""Message type - always 'request' for DAP requests."""

command: str
"""The command to execute (e.g., 'initialize', 'launch', 'setBreakpoints')."""

arguments: dict | None
"""Command-specific arguments. Should be parsed further in ./debug_adapter.py"""


def handle_debug_adapter_request(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

barebones request/response. I assume this is where your large conditional thing would go @dmadisetti

ls: LanguageServer,
manager: LspSessionManager,
*,
notebook_uri: str,
session_id: str,
message: dict,
) -> None:
"""Handle DAP requests."""
request = converter.structure(message, DapRequestMessage)
logger.debug(f"Debug.Send {session_id=}, {request=}")

session = manager.get_session(notebook_uri)
assert session, f"No session in workspace for {notebook_uri}"

ls.protocol.notify(
"marimo/dap",
{
"sessionId": session_id,
"message": {
"type": "response",
"request_seq": request.seq,
"success": True,
"command": request.command,
"request": {},
},
},
)
14 changes: 14 additions & 0 deletions src/marimo_lsp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ class ConvertRequest(BaseRequest):
"""The identifier for the text document to convert"""


@attrs.define
class DebugAdapterRequest(BaseRequest):
"""A forwarded DAP request."""

session_id: str
"""A UUID for the debug session."""

notebook_uri: str
"""The URI of the notebook."""

message: dict
"""They DAP message."""


def _camel_to_snake(name: str) -> str:
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub(r"([A-Z]+)([A-Z][a-z]*)", r"\1_\2", s1).lower()
Expand Down
16 changes: 16 additions & 0 deletions src/marimo_lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from marimo_lsp.loggers import get_logger
from marimo_lsp.models import (
ConvertRequest,
DebugAdapterRequest,
DeserializeRequest,
RunRequest,
SerializeRequest,
Expand Down Expand Up @@ -145,6 +146,21 @@ async def deserialize(args: DeserializeRequest):
converter = MarimoConvert.from_py(args.source)
return dataclasses.asdict(converter.to_ir())

@server.command("marimo.dap")
async def dap(ls: LanguageServer, params: DebugAdapterRequest):
"""Handle DAP messages forwarded from VS Code extension."""
from marimo_lsp.debug_adapter import ( # noqa: PLC0415
handle_debug_adapter_request,
)

return handle_debug_adapter_request(
ls=ls,
manager=manager,
session_id=params.session_id,
notebook_uri=params.notebook_uri,
message=params.message,
)

@server.feature(
lsp.TEXT_DOCUMENT_CODE_ACTION,
lsp.CodeActionOptions(
Expand Down