DAP: Refactor handling of Event and Request protocol types

This change refactors the DAP `Event` type, the `helix_dap` module and
the `helix_dap::transport` module to be closer to the equivalent
implementations in `helix_lsp`. The DAP `Event` type is similar to LSP's
`Notification` so this change rewrites the enum as a trait which is
implemented by empty types (for example `enum Initialized {}`).

`Event` is then reintroduced as an enum in `helix_dap` with a helper
function to convert from the `Event` as the transport knows it. By
separating the definitions of `Event` between lib and transport, we can
handle incoming events which are not known to our `Event` enum. For
example debugpy sends `"debugpySockets"` which is unknown. With this
change, unknown events are discarded with an info-level log message.

The `Request` type was already a trait but did not have an enum with the
known values. This change also introduces a `helix_dap::Request` enum
similar to `helix_dap::Event`. This resolves a TODO comment about
avoiding `unwrap` when parsing the request's arguments.
pull/12707/head
Michael Davis 2025-01-27 15:27:35 -05:00
parent 9bc63c1c59
commit fec5101a41
No known key found for this signature in database
4 changed files with 455 additions and 277 deletions

View File

@ -3,10 +3,11 @@ mod transport;
mod types; mod types;
pub use client::{Client, ConnectionType}; pub use client::{Client, ConnectionType};
pub use events::Event;
pub use transport::{Payload, Response, Transport}; pub use transport::{Payload, Response, Transport};
pub use types::*; pub use types::*;
use serde::de::DeserializeOwned;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@ -18,9 +19,84 @@ pub enum Error {
Timeout(u64), Timeout(u64),
#[error("server closed the stream")] #[error("server closed the stream")]
StreamClosed, StreamClosed,
#[error("Unhandled")]
Unhandled,
#[error(transparent)] #[error(transparent)]
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError), ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
#[error(transparent)] #[error(transparent)]
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
} }
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub enum Request {
RunInTerminal(<requests::RunInTerminal as types::Request>::Arguments),
}
impl Request {
pub fn parse(command: &str, arguments: Option<serde_json::Value>) -> Result<Self> {
use crate::types::Request as _;
let arguments = arguments.unwrap_or_default();
let request = match command {
requests::RunInTerminal::COMMAND => Self::RunInTerminal(parse_value(arguments)?),
_ => return Err(Error::Unhandled),
};
Ok(request)
}
}
#[derive(Debug)]
pub enum Event {
Initialized(<events::Initialized as events::Event>::Body),
Stopped(<events::Stopped as events::Event>::Body),
Continued(<events::Continued as events::Event>::Body),
Exited(<events::Exited as events::Event>::Body),
Terminated(<events::Terminated as events::Event>::Body),
Thread(<events::Thread as events::Event>::Body),
Output(<events::Output as events::Event>::Body),
Breakpoint(<events::Breakpoint as events::Event>::Body),
Module(<events::Module as events::Event>::Body),
LoadedSource(<events::LoadedSource as events::Event>::Body),
Process(<events::Process as events::Event>::Body),
Capabilities(<events::Capabilities as events::Event>::Body),
// ProgressStart(),
// ProgressUpdate(),
// ProgressEnd(),
// Invalidated(),
Memory(<events::Memory as events::Event>::Body),
}
impl Event {
pub fn parse(event: &str, body: Option<serde_json::Value>) -> Result<Self> {
use crate::events::Event as _;
let body = body.unwrap_or_default();
let event = match event {
events::Initialized::EVENT => Self::Initialized(parse_value(body)?),
events::Stopped::EVENT => Self::Stopped(parse_value(body)?),
events::Continued::EVENT => Self::Continued(parse_value(body)?),
events::Exited::EVENT => Self::Exited(parse_value(body)?),
events::Terminated::EVENT => Self::Terminated(parse_value(body)?),
events::Thread::EVENT => Self::Thread(parse_value(body)?),
events::Output::EVENT => Self::Output(parse_value(body)?),
events::Breakpoint::EVENT => Self::Breakpoint(parse_value(body)?),
events::Module::EVENT => Self::Module(parse_value(body)?),
events::LoadedSource::EVENT => Self::LoadedSource(parse_value(body)?),
events::Process::EVENT => Self::Process(parse_value(body)?),
events::Capabilities::EVENT => Self::Capabilities(parse_value(body)?),
events::Memory::EVENT => Self::Memory(parse_value(body)?),
_ => return Err(Error::Unhandled),
};
Ok(event)
}
}
fn parse_value<T>(value: serde_json::Value) -> Result<T>
where
T: DeserializeOwned,
{
serde_json::from_value(value).map_err(|err| err.into())
}

View File

@ -1,4 +1,4 @@
use crate::{Error, Event, Result}; use crate::{Error, Result};
use anyhow::Context; use anyhow::Context;
use log::{error, info, warn}; use log::{error, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -32,11 +32,17 @@ pub struct Response {
pub body: Option<Value>, pub body: Option<Value>,
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Event {
pub event: String,
pub body: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum Payload { pub enum Payload {
// type = "event" // type = "event"
Event(Box<Event>), Event(Event),
// type = "response" // type = "response"
Response(Response), Response(Response),
// type = "request" // type = "request"

View File

@ -759,33 +759,30 @@ pub mod requests {
pub mod events { pub mod events {
use super::*; use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)] pub trait Event {
#[serde(rename_all = "camelCase")] type Body: serde::de::DeserializeOwned + serde::Serialize;
#[serde(tag = "event", content = "body")] const EVENT: &'static str;
// seq is omitted as unused and is not sent by some implementations }
pub enum Event {
Initialized(Option<DebuggerCapabilities>), #[derive(Debug)]
Stopped(Stopped), pub enum Initialized {}
Continued(Continued),
Exited(Exited), impl Event for Initialized {
Terminated(Option<Terminated>), type Body = Option<DebuggerCapabilities>;
Thread(Thread), const EVENT: &'static str = "initialized";
Output(Output), }
Breakpoint(Breakpoint),
Module(Module), #[derive(Debug)]
LoadedSource(LoadedSource), pub enum Stopped {}
Process(Process),
Capabilities(Capabilities), impl Event for Stopped {
// ProgressStart(), type Body = StoppedBody;
// ProgressUpdate(), const EVENT: &'static str = "stopped";
// ProgressEnd(),
// Invalidated(),
Memory(Memory),
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Stopped { pub struct StoppedBody {
pub reason: String, pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
@ -801,37 +798,77 @@ pub mod events {
pub hit_breakpoint_ids: Option<Vec<usize>>, pub hit_breakpoint_ids: Option<Vec<usize>>,
} }
#[derive(Debug)]
pub enum Continued {}
impl Event for Continued {
type Body = ContinuedBody;
const EVENT: &'static str = "continued";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Continued { pub struct ContinuedBody {
pub thread_id: ThreadId, pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub all_threads_continued: Option<bool>, pub all_threads_continued: Option<bool>,
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug)]
#[serde(rename_all = "camelCase")] pub enum Exited {}
pub struct Exited {
pub exit_code: usize, impl Event for Exited {
type Body = ExitedBody;
const EVENT: &'static str = "exited";
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Terminated { pub struct ExitedBody {
pub exit_code: usize,
}
#[derive(Debug)]
pub enum Terminated {}
impl Event for Terminated {
type Body = Option<TerminatedBody>;
const EVENT: &'static str = "terminated";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminatedBody {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub restart: Option<Value>, pub restart: Option<Value>,
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug)]
#[serde(rename_all = "camelCase")] pub enum Thread {}
pub struct Thread {
pub reason: String, impl Event for Thread {
pub thread_id: ThreadId, type Body = ThreadBody;
const EVENT: &'static str = "thread";
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Output { pub struct ThreadBody {
pub reason: String,
pub thread_id: ThreadId,
}
#[derive(Debug)]
pub enum Output {}
impl Event for Output {
type Body = OutputBody;
const EVENT: &'static str = "output";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputBody {
pub output: String, pub output: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>, pub category: Option<String>,
@ -849,30 +886,62 @@ pub mod events {
pub data: Option<Value>, pub data: Option<Value>,
} }
#[derive(Debug)]
pub enum Breakpoint {}
impl Event for Breakpoint {
type Body = BreakpointBody;
const EVENT: &'static str = "breakpoint";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Breakpoint { pub struct BreakpointBody {
pub reason: String, pub reason: String,
pub breakpoint: super::Breakpoint, pub breakpoint: super::Breakpoint,
} }
#[derive(Debug)]
pub enum Module {}
impl Event for Module {
type Body = ModuleBody;
const EVENT: &'static str = "module";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Module { pub struct ModuleBody {
pub reason: String, pub reason: String,
pub module: super::Module, pub module: super::Module,
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug)]
#[serde(rename_all = "camelCase")] pub enum LoadedSource {}
pub struct LoadedSource {
pub reason: String, impl Event for LoadedSource {
pub source: super::Source, type Body = LoadedSourceBody;
const EVENT: &'static str = "loadedSource";
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Process { pub struct LoadedSourceBody {
pub reason: String,
pub source: super::Source,
}
#[derive(Debug)]
pub enum Process {}
impl Event for Process {
type Body = ProcessBody;
const EVENT: &'static str = "process";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessBody {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub system_process_id: Option<usize>, pub system_process_id: Option<usize>,
@ -884,38 +953,55 @@ pub mod events {
pub pointer_size: Option<usize>, pub pointer_size: Option<usize>,
} }
#[derive(Debug)]
pub enum Capabilities {}
impl Event for Capabilities {
type Body = CapabilitiesBody;
const EVENT: &'static str = "capabilities";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Capabilities { pub struct CapabilitiesBody {
pub capabilities: super::DebuggerCapabilities, pub capabilities: super::DebuggerCapabilities,
} }
// #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] // #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
// #[serde(rename_all = "camelCase")] // #[serde(rename_all = "camelCase")]
// pub struct Invalidated { // pub struct InvalidatedBody {
// pub areas: Vec<InvalidatedArea>, // pub areas: Vec<InvalidatedArea>,
// pub thread_id: Option<ThreadId>, // pub thread_id: Option<ThreadId>,
// pub stack_frame_id: Option<usize>, // pub stack_frame_id: Option<usize>,
// } // }
#[derive(Debug)]
pub enum Memory {}
impl Event for Memory {
type Body = MemoryBody;
const EVENT: &'static str = "memory";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Memory { pub struct MemoryBody {
pub memory_reference: String, pub memory_reference: String,
pub offset: usize, pub offset: usize,
pub count: usize, pub count: usize,
} }
}
#[test] #[test]
fn test_deserialize_module_id_from_number() { fn test_deserialize_module_id_from_number() {
let raw = r#"{"id": 0, "name": "Name"}"#; let raw = r#"{"id": 0, "name": "Name"}"#;
let module: super::Module = serde_json::from_str(raw).expect("Error!"); let module: Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0"); assert_eq!(module.id, "0");
} }
#[test] #[test]
fn test_deserialize_module_id_from_string() { fn test_deserialize_module_id_from_string() {
let raw = r#"{"id": "0", "name": "Name"}"#; let raw = r#"{"id": "0", "name": "Name"}"#;
let module: super::Module = serde_json::from_str(raw).expect("Error!"); let module: Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0"); assert_eq!(module.id, "0");
} }

View File

@ -5,6 +5,7 @@ use helix_core::Selection;
use helix_dap::{self as dap, Client, ConnectionType, Payload, Request, ThreadId}; use helix_dap::{self as dap, Client, ConnectionType, Payload, Request, ThreadId};
use helix_lsp::block_on; use helix_lsp::block_on;
use log::warn; use log::warn;
use serde_json::json;
use std::fmt::Write; use std::fmt::Write;
use std::path::PathBuf; use std::path::PathBuf;
@ -141,7 +142,6 @@ pub fn breakpoints_changed(
impl Editor { impl Editor {
pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) -> bool { pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) -> bool {
use dap::requests::RunInTerminal;
use helix_dap::{events, Event}; use helix_dap::{events, Event};
let debugger = match self.debugger.as_mut() { let debugger = match self.debugger.as_mut() {
@ -149,8 +149,20 @@ impl Editor {
None => return false, None => return false,
}; };
match payload { match payload {
Payload::Event(ev) => match *ev { Payload::Event(event) => {
Event::Stopped(events::Stopped { let event = match Event::parse(&event.event, event.body) {
Ok(event) => event,
Err(dap::Error::Unhandled) => {
log::info!("Discarding unknown DAP event '{}'", event.event);
return false;
}
Err(err) => {
log::warn!("Discarding invalid DAP event '{}': {err}", event.event);
return false;
}
};
match event {
Event::Stopped(events::StoppedBody {
thread_id, thread_id,
description, description,
text, text,
@ -161,7 +173,9 @@ impl Editor {
let all_threads_stopped = all_threads_stopped.unwrap_or_default(); let all_threads_stopped = all_threads_stopped.unwrap_or_default();
if all_threads_stopped { if all_threads_stopped {
if let Ok(response) = debugger.request::<dap::requests::Threads>(()).await { if let Ok(response) =
debugger.request::<dap::requests::Threads>(()).await
{
for thread in response.threads { for thread in response.threads {
fetch_stack_trace(debugger, thread.id).await; fetch_stack_trace(debugger, thread.id).await;
} }
@ -192,7 +206,7 @@ impl Editor {
self.set_status(status); self.set_status(status);
} }
Event::Continued(events::Continued { thread_id, .. }) => { Event::Continued(events::ContinuedBody { thread_id, .. }) => {
debugger debugger
.thread_states .thread_states
.insert(thread_id, "running".to_owned()); .insert(thread_id, "running".to_owned());
@ -203,7 +217,7 @@ impl Editor {
Event::Thread(_) => { Event::Thread(_) => {
// TODO: update thread_states, make threads request // TODO: update thread_states, make threads request
} }
Event::Breakpoint(events::Breakpoint { reason, breakpoint }) => { Event::Breakpoint(events::BreakpointBody { reason, breakpoint }) => {
match &reason[..] { match &reason[..] {
"new" => { "new" => {
if let Some(source) = breakpoint.source { if let Some(source) = breakpoint.source {
@ -230,9 +244,10 @@ impl Editor {
.message .message
.clone() .clone()
.or_else(|| breakpoints[i].message.take()); .or_else(|| breakpoints[i].message.take());
breakpoints[i].line = breakpoint breakpoints[i].line =
.line breakpoint.line.map_or(breakpoints[i].line, |line| {
.map_or(breakpoints[i].line, |line| line.saturating_sub(1)); line.saturating_sub(1)
});
breakpoints[i].column = breakpoints[i].column =
breakpoint.column.or(breakpoints[i].column); breakpoint.column.or(breakpoints[i].column);
} }
@ -252,7 +267,7 @@ impl Editor {
} }
} }
} }
Event::Output(events::Output { Event::Output(events::OutputBody {
category, output, .. category, output, ..
}) => { }) => {
let prefix = match category { let prefix = match category {
@ -312,7 +327,8 @@ impl Editor {
} }
}; };
let relaunch_resp = if let ConnectionType::Launch = connection_type { let relaunch_resp = if let ConnectionType::Launch = connection_type
{
debugger.launch(restart_args).await debugger.launch(restart_args).await
} else { } else {
debugger.attach(restart_args).await debugger.attach(restart_args).await
@ -345,54 +361,48 @@ impl Editor {
log::warn!("Unhandled event {:?}", ev); log::warn!("Unhandled event {:?}", ev);
return false; // return early to skip render return false; // return early to skip render
} }
}, }
}
Payload::Response(_) => unreachable!(), Payload::Response(_) => unreachable!(),
Payload::Request(request) => match request.command.as_str() { Payload::Request(request) => {
RunInTerminal::COMMAND => { let reply = match Request::parse(&request.command, request.arguments) {
let arguments: dap::requests::RunInTerminalArguments = Ok(Request::RunInTerminal(arguments)) => {
serde_json::from_value(request.arguments.unwrap_or_default()).unwrap(); let config = self.config();
// TODO: no unwrap let Some(config) = config.terminal.as_ref() else {
let config = match self.config().terminal.clone() {
Some(config) => config,
None => {
self.set_error("No external terminal defined"); self.set_error("No external terminal defined");
return true; return true;
}
}; };
// Re-borrowing debugger to avoid issues when loading config let process = match std::process::Command::new(&config.command)
let debugger = match self.debugger.as_mut() { .args(&config.args)
Some(debugger) => debugger,
None => return false,
};
let process = match std::process::Command::new(config.command)
.args(config.args)
.arg(arguments.args.join(" ")) .arg(arguments.args.join(" "))
.spawn() .spawn()
{ {
Ok(process) => process, Ok(process) => process,
Err(err) => { Err(err) => {
self.set_error(format!("Error starting external terminal: {}", err)); self.set_error(format!(
"Error starting external terminal: {}",
err
));
return true; return true;
} }
}; };
let _ = debugger Ok(json!(dap::requests::RunInTerminalResponse {
.reply(
request.seq,
dap::requests::RunInTerminal::COMMAND,
serde_json::to_value(dap::requests::RunInTerminalResponse {
process_id: Some(process.id()), process_id: Some(process.id()),
shell_process_id: None, shell_process_id: None,
}) }))
.map_err(|e| e.into()), }
) Err(err) => Err(err),
.await; };
if let Some(debugger) = self.debugger.as_mut() {
debugger
.reply(request.seq, &request.command, reply)
.await
.ok();
}
} }
_ => log::error!("DAP reverse request not implemented: {:?}", request),
},
} }
true true
} }