DAP: Support the startDebugging reverse request (#13403)

pull/13827/head
Jason Williams 2025-06-23 15:48:05 +01:00 committed by GitHub
parent 58dfa158c2
commit 2338b44909
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 417 additions and 129 deletions

4
Cargo.lock generated
View File

@ -1439,13 +1439,17 @@ version = "25.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"fern", "fern",
"futures-executor",
"futures-util",
"helix-core", "helix-core",
"helix-stdx", "helix-stdx",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
"slotmap",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-stream",
] ]
[[package]] [[package]]

View File

@ -47,6 +47,9 @@ unicode-segmentation = "1.2"
ropey = { version = "1.6.1", default-features = false, features = ["simd"] } ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
foldhash = "0.1" foldhash = "0.1"
parking_lot = "0.12" parking_lot = "0.12"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
tokio-stream = "0.1.17"
[workspace.package] [workspace.package]
version = "25.1.1" version = "25.1.1"

View File

@ -22,6 +22,11 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
thiserror.workspace = true thiserror.workspace = true
slotmap.workspace = true
futures-executor.workspace = true
futures-util.workspace = true
tokio-stream.workspace = true
[dev-dependencies] [dev-dependencies]
fern = "0.7" fern = "0.7"

View File

@ -1,10 +1,11 @@
use crate::{ use crate::{
requests::DisconnectArguments, registry::DebugAdapterId,
requests::{DisconnectArguments, TerminateArguments},
transport::{Payload, Request, Response, Transport}, transport::{Payload, Request, Response, Transport},
types::*, types::*,
Error, Result, Error, Result,
}; };
use helix_core::syntax::config::DebuggerQuirks; use helix_core::syntax::config::{DebugAdapterConfig, DebuggerQuirks};
use serde_json::Value; use serde_json::Value;
@ -27,12 +28,14 @@ use tokio::{
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
id: usize, id: DebugAdapterId,
_process: Option<Child>, _process: Option<Child>,
server_tx: UnboundedSender<Payload>, server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64, request_counter: AtomicU64,
connection_type: Option<ConnectionType>, connection_type: Option<ConnectionType>,
starting_request_args: Option<Value>, starting_request_args: Option<Value>,
/// The socket address of the debugger, if using TCP transport.
pub socket: Option<SocketAddr>,
pub caps: Option<DebuggerCapabilities>, pub caps: Option<DebuggerCapabilities>,
// thread_id -> frames // thread_id -> frames
pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>, pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
@ -41,23 +44,20 @@ pub struct Client {
/// Currently active frame for the current thread. /// Currently active frame for the current thread.
pub active_frame: Option<usize>, pub active_frame: Option<usize>,
pub quirks: DebuggerQuirks, pub quirks: DebuggerQuirks,
} /// The config which was used to start this debugger.
pub config: Option<DebugAdapterConfig>,
#[derive(Clone, Copy, Debug)]
pub enum ConnectionType {
Launch,
Attach,
} }
impl Client { impl Client {
// Spawn a process and communicate with it by either TCP or stdio // Spawn a process and communicate with it by either TCP or stdio
// The returned stream includes the Client ID so consumers can differentiate between multiple clients
pub async fn process( pub async fn process(
transport: &str, transport: &str,
command: &str, command: &str,
args: Vec<&str>, args: Vec<&str>,
port_arg: Option<&str>, port_arg: Option<&str>,
id: usize, id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<Payload>)> { ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
if command.is_empty() { if command.is_empty() {
return Result::Err(Error::Other(anyhow!("Command not provided"))); return Result::Err(Error::Other(anyhow!("Command not provided")));
} }
@ -72,9 +72,9 @@ impl Client {
rx: Box<dyn AsyncBufRead + Unpin + Send>, rx: Box<dyn AsyncBufRead + Unpin + Send>,
tx: Box<dyn AsyncWrite + Unpin + Send>, tx: Box<dyn AsyncWrite + Unpin + Send>,
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>, err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
id: usize, id: DebugAdapterId,
process: Option<Child>, process: Option<Child>,
) -> Result<(Self, UnboundedReceiver<Payload>)> { ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let (server_rx, server_tx) = Transport::start(rx, tx, err, id); let (server_rx, server_tx) = Transport::start(rx, tx, err, id);
let (client_tx, client_rx) = unbounded_channel(); let (client_tx, client_rx) = unbounded_channel();
@ -86,22 +86,24 @@ impl Client {
caps: None, caps: None,
connection_type: None, connection_type: None,
starting_request_args: None, starting_request_args: None,
socket: None,
stack_frames: HashMap::new(), stack_frames: HashMap::new(),
thread_states: HashMap::new(), thread_states: HashMap::new(),
thread_id: None, thread_id: None,
active_frame: None, active_frame: None,
quirks: DebuggerQuirks::default(), quirks: DebuggerQuirks::default(),
config: None,
}; };
tokio::spawn(Self::recv(server_rx, client_tx)); tokio::spawn(Self::recv(id, server_rx, client_tx));
Ok((client, client_rx)) Ok((client, client_rx))
} }
pub async fn tcp( pub async fn tcp(
addr: std::net::SocketAddr, addr: std::net::SocketAddr,
id: usize, id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<Payload>)> { ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let stream = TcpStream::connect(addr).await?; let stream = TcpStream::connect(addr).await?;
let (rx, tx) = stream.into_split(); let (rx, tx) = stream.into_split();
Self::streams(Box::new(BufReader::new(rx)), Box::new(tx), None, id, None) Self::streams(Box::new(BufReader::new(rx)), Box::new(tx), None, id, None)
@ -110,8 +112,8 @@ impl Client {
pub fn stdio( pub fn stdio(
cmd: &str, cmd: &str,
args: Vec<&str>, args: Vec<&str>,
id: usize, id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<Payload>)> { ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
// Resolve path to the binary // Resolve path to the binary
let cmd = helix_stdx::env::which(cmd)?; let cmd = helix_stdx::env::which(cmd)?;
@ -162,8 +164,8 @@ impl Client {
cmd: &str, cmd: &str,
args: Vec<&str>, args: Vec<&str>,
port_format: &str, port_format: &str,
id: usize, id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<Payload>)> { ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let port = Self::get_port().await.unwrap(); let port = Self::get_port().await.unwrap();
let process = Command::new(cmd) let process = Command::new(cmd)
@ -178,40 +180,49 @@ impl Client {
// Wait for adapter to become ready for connection // Wait for adapter to become ready for connection
time::sleep(time::Duration::from_millis(500)).await; time::sleep(time::Duration::from_millis(500)).await;
let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
let stream = TcpStream::connect(SocketAddr::new( let stream = TcpStream::connect(socket).await?;
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
port,
))
.await?;
let (rx, tx) = stream.into_split(); let (rx, tx) = stream.into_split();
Self::streams( let mut result = Self::streams(
Box::new(BufReader::new(rx)), Box::new(BufReader::new(rx)),
Box::new(tx), Box::new(tx),
None, None,
id, id,
Some(process), Some(process),
) );
// Set the socket address for the client
if let Ok((client, _)) = &mut result {
client.socket = Some(socket);
}
result
} }
async fn recv(mut server_rx: UnboundedReceiver<Payload>, client_tx: UnboundedSender<Payload>) { async fn recv(
id: DebugAdapterId,
mut server_rx: UnboundedReceiver<Payload>,
client_tx: UnboundedSender<(DebugAdapterId, Payload)>,
) {
while let Some(msg) = server_rx.recv().await { while let Some(msg) = server_rx.recv().await {
match msg { match msg {
Payload::Event(ev) => { Payload::Event(ev) => {
client_tx.send(Payload::Event(ev)).expect("Failed to send"); client_tx
.send((id, Payload::Event(ev)))
.expect("Failed to send");
} }
Payload::Response(_) => unreachable!(), Payload::Response(_) => unreachable!(),
Payload::Request(req) => { Payload::Request(req) => {
client_tx client_tx
.send(Payload::Request(req)) .send((id, Payload::Request(req)))
.expect("Failed to send"); .expect("Failed to send");
} }
} }
} }
} }
pub fn id(&self) -> usize { pub fn id(&self) -> DebugAdapterId {
self.id self.id
} }
@ -354,6 +365,14 @@ impl Client {
self.call::<requests::Disconnect>(args) self.call::<requests::Disconnect>(args)
} }
pub fn terminate(
&mut self,
args: Option<TerminateArguments>,
) -> impl Future<Output = Result<Value>> {
self.connection_type = None;
self.call::<requests::Terminate>(args)
}
pub fn launch(&mut self, args: serde_json::Value) -> impl Future<Output = Result<Value>> { pub fn launch(&mut self, args: serde_json::Value) -> impl Future<Output = Result<Value>> {
self.connection_type = Some(ConnectionType::Launch); self.connection_type = Some(ConnectionType::Launch);
self.starting_request_args = Some(args.clone()); self.starting_request_args = Some(args.clone());

View File

@ -1,8 +1,9 @@
mod client; mod client;
pub mod registry;
mod transport; mod transport;
mod types; mod types;
pub use client::{Client, ConnectionType}; pub use client::Client;
pub use transport::{Payload, Response, Transport}; pub use transport::{Payload, Response, Transport};
pub use types::*; pub use types::*;
@ -31,6 +32,7 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)] #[derive(Debug)]
pub enum Request { pub enum Request {
RunInTerminal(<requests::RunInTerminal as types::Request>::Arguments), RunInTerminal(<requests::RunInTerminal as types::Request>::Arguments),
StartDebugging(<requests::StartDebugging as types::Request>::Arguments),
} }
impl Request { impl Request {
@ -40,6 +42,7 @@ impl Request {
let arguments = arguments.unwrap_or_default(); let arguments = arguments.unwrap_or_default();
let request = match command { let request = match command {
requests::RunInTerminal::COMMAND => Self::RunInTerminal(parse_value(arguments)?), requests::RunInTerminal::COMMAND => Self::RunInTerminal(parse_value(arguments)?),
requests::StartDebugging::COMMAND => Self::StartDebugging(parse_value(arguments)?),
_ => return Err(Error::Unhandled), _ => return Err(Error::Unhandled),
}; };

View File

@ -0,0 +1,114 @@
use crate::{Client, Payload, Result, StackFrame};
use futures_executor::block_on;
use futures_util::stream::SelectAll;
use helix_core::syntax::config::DebugAdapterConfig;
use slotmap::SlotMap;
use std::fmt;
use tokio_stream::wrappers::UnboundedReceiverStream;
/// The resgistry is a struct that manages and owns multiple debugger clients
/// This holds the responsibility of managing the lifecycle of each client
/// plus showing the heirarcihical nature betweeen them
pub struct Registry {
inner: SlotMap<DebugAdapterId, Client>,
/// The active debugger client
///
/// TODO: You can have multiple active debuggers, so the concept of a single active debugger
/// may need to be changed
current_client_id: Option<DebugAdapterId>,
/// A stream of incoming messages from all debuggers
pub incoming: SelectAll<UnboundedReceiverStream<(DebugAdapterId, Payload)>>,
}
impl Registry {
/// Creates a new DebuggerService instance
pub fn new() -> Self {
Self {
inner: SlotMap::with_key(),
current_client_id: None,
incoming: SelectAll::new(),
}
}
pub fn start_client(
&mut self,
socket: Option<std::net::SocketAddr>,
config: &DebugAdapterConfig,
) -> Result<DebugAdapterId> {
self.inner.try_insert_with_key(|id| {
let result = match socket {
Some(socket) => block_on(Client::tcp(socket, id)),
None => block_on(Client::process(
&config.transport,
&config.command,
config.args.iter().map(|arg| arg.as_str()).collect(),
config.port_arg.as_deref(),
id,
)),
};
let (mut client, receiver) = result?;
self.incoming.push(UnboundedReceiverStream::new(receiver));
client.config = Some(config.clone());
block_on(client.initialize(config.name.clone()))?;
client.quirks = config.quirks.clone();
Ok(client)
})
}
pub fn remove_client(&mut self, id: DebugAdapterId) {
self.inner.remove(id);
}
pub fn get_client(&self, id: DebugAdapterId) -> Option<&Client> {
self.inner.get(id)
}
pub fn get_client_mut(&mut self, id: DebugAdapterId) -> Option<&mut Client> {
self.inner.get_mut(id)
}
pub fn get_active_client(&self) -> Option<&Client> {
self.current_client_id.and_then(|id| self.get_client(id))
}
pub fn get_active_client_mut(&mut self) -> Option<&mut Client> {
self.current_client_id
.and_then(|id| self.get_client_mut(id))
}
pub fn set_active_client(&mut self, id: DebugAdapterId) {
if self.get_client(id).is_some() {
self.current_client_id = Some(id);
} else {
self.current_client_id = None;
}
}
pub fn unset_active_client(&mut self) {
self.current_client_id = None;
}
pub fn current_stack_frame(&self) -> Option<&StackFrame> {
self.get_active_client()
.and_then(|debugger| debugger.current_stack_frame())
}
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
slotmap::new_key_type! {
pub struct DebugAdapterId;
}
impl fmt::Display for DebugAdapterId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}

View File

@ -1,10 +1,10 @@
use crate::{Error, Result}; use crate::{registry::DebugAdapterId, 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};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::{collections::HashMap, fmt::Debug};
use tokio::{ use tokio::{
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt}, io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt},
sync::{ sync::{
@ -52,7 +52,7 @@ pub enum Payload {
#[derive(Debug)] #[derive(Debug)]
pub struct Transport { pub struct Transport {
#[allow(unused)] #[allow(unused)]
id: usize, id: DebugAdapterId,
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>, pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
} }
@ -61,7 +61,7 @@ impl Transport {
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>, server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
server_stdin: Box<dyn AsyncWrite + Unpin + Send>, server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>, server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
id: usize, id: DebugAdapterId,
) -> (UnboundedReceiver<Payload>, UnboundedSender<Payload>) { ) -> (UnboundedReceiver<Payload>, UnboundedSender<Payload>) {
let (client_tx, rx) = unbounded_channel(); let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel(); let (tx, client_rx) = unbounded_channel();
@ -73,7 +73,7 @@ impl Transport {
let transport = Arc::new(transport); let transport = Arc::new(transport);
tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); tokio::spawn(Self::recv(id, transport.clone(), server_stdout, client_tx));
tokio::spawn(Self::send(transport, server_stdin, client_rx)); tokio::spawn(Self::send(transport, server_stdin, client_rx));
if let Some(stderr) = server_stderr { if let Some(stderr) = server_stderr {
tokio::spawn(Self::err(stderr)); tokio::spawn(Self::err(stderr));
@ -83,6 +83,7 @@ impl Transport {
} }
async fn recv_server_message( async fn recv_server_message(
id: DebugAdapterId,
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>, reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
buffer: &mut String, buffer: &mut String,
content: &mut Vec<u8>, content: &mut Vec<u8>,
@ -122,7 +123,7 @@ impl Transport {
reader.read_exact(content).await?; reader.read_exact(content).await?;
let msg = std::str::from_utf8(content).context("invalid utf8 from server")?; let msg = std::str::from_utf8(content).context("invalid utf8 from server")?;
info!("<- DAP {}", msg); info!("[{}] <- DAP {}", id, msg);
// try parsing as output (server response) or call (server request) // try parsing as output (server response) or call (server request)
let output: serde_json::Result<Payload> = serde_json::from_str(msg); let output: serde_json::Result<Payload> = serde_json::from_str(msg);
@ -164,7 +165,7 @@ impl Transport {
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>, server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
request: String, request: String,
) -> Result<()> { ) -> Result<()> {
info!("-> DAP {}", request); info!("[{}] -> DAP {}", self.id, request);
// send the headers // send the headers
server_stdin server_stdin
@ -179,15 +180,18 @@ impl Transport {
Ok(()) Ok(())
} }
fn process_response(res: Response) -> Result<Response> { fn process_response(&self, res: Response) -> Result<Response> {
if res.success { if res.success {
info!("<- DAP success in response to {}", res.request_seq); info!(
"[{}] <- DAP success in response to {}",
self.id, res.request_seq
);
Ok(res) Ok(res)
} else { } else {
error!( error!(
"<- DAP error {:?} ({:?}) for command #{} {}", "[{}] <- DAP error {:?} ({:?}) for command #{} {}",
res.message, res.body, res.request_seq, res.command self.id, res.message, res.body, res.request_seq, res.command
); );
Err(Error::Other(anyhow::format_err!("{:?}", res.body))) Err(Error::Other(anyhow::format_err!("{:?}", res.body)))
@ -205,7 +209,7 @@ impl Transport {
let tx = self.pending_requests.lock().await.remove(&request_seq); let tx = self.pending_requests.lock().await.remove(&request_seq);
match tx { match tx {
Some(tx) => match tx.send(Self::process_response(res)).await { Some(tx) => match tx.send(self.process_response(res)).await {
Ok(_) => (), Ok(_) => (),
Err(_) => error!( Err(_) => error!(
"Tried sending response into a closed channel (id={:?}), original request likely timed out", "Tried sending response into a closed channel (id={:?}), original request likely timed out",
@ -225,12 +229,12 @@ impl Transport {
ref seq, ref seq,
.. ..
}) => { }) => {
info!("<- DAP request {} #{}", command, seq); info!("[{}] <- DAP request {} #{}", self.id, command, seq);
client_tx.send(msg).expect("Failed to send"); client_tx.send(msg).expect("Failed to send");
Ok(()) Ok(())
} }
Payload::Event(ref event) => { Payload::Event(ref event) => {
info!("<- DAP event {:?}", event); info!("[{}] <- DAP event {:?}", self.id, event);
client_tx.send(msg).expect("Failed to send"); client_tx.send(msg).expect("Failed to send");
Ok(()) Ok(())
} }
@ -238,6 +242,7 @@ impl Transport {
} }
async fn recv( async fn recv(
id: DebugAdapterId,
transport: Arc<Self>, transport: Arc<Self>,
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>, mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: UnboundedSender<Payload>, client_tx: UnboundedSender<Payload>,
@ -246,6 +251,7 @@ impl Transport {
let mut content_buffer = Vec::new(); let mut content_buffer = Vec::new();
loop { loop {
match Self::recv_server_message( match Self::recv_server_message(
id,
&mut server_stdout, &mut server_stdout,
&mut recv_buffer, &mut recv_buffer,
&mut content_buffer, &mut content_buffer,
@ -255,7 +261,7 @@ impl Transport {
Ok(msg) => match transport.process_server_message(&client_tx, msg).await { Ok(msg) => match transport.process_server_message(&client_tx, msg).await {
Ok(_) => (), Ok(_) => (),
Err(err) => { Err(err) => {
error!("err: <- {err:?}"); error!(" [{id}] err: <- {err:?}");
break; break;
} }
}, },

View File

@ -438,6 +438,21 @@ pub mod requests {
const COMMAND: &'static str = "disconnect"; const COMMAND: &'static str = "disconnect";
} }
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminateArguments {
pub restart: Option<bool>,
}
#[derive(Debug)]
pub enum Terminate {}
impl Request for Terminate {
type Arguments = Option<TerminateArguments>;
type Result = ();
const COMMAND: &'static str = "terminate";
}
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigurationDone {} pub enum ConfigurationDone {}
@ -752,6 +767,21 @@ pub mod requests {
type Result = RunInTerminalResponse; type Result = RunInTerminalResponse;
const COMMAND: &'static str = "runInTerminal"; const COMMAND: &'static str = "runInTerminal";
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StartDebuggingArguments {
pub request: ConnectionType,
pub configuration: Value,
}
#[derive(Debug)]
pub enum StartDebugging {}
impl Request for StartDebugging {
type Arguments = StartDebuggingArguments;
type Result = ();
const COMMAND: &'static str = "startDebugging";
}
} }
// Events // Events
@ -992,6 +1022,13 @@ pub mod events {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConnectionType {
Launch,
Attach,
}
#[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"}"#;

View File

@ -19,14 +19,14 @@ helix-loader = { path = "../helix-loader" }
helix-lsp-types = { path = "../helix-lsp-types" } helix-lsp-types = { path = "../helix-lsp-types" }
anyhow = "1.0" anyhow = "1.0"
futures-executor = "0.3" futures-executor.workspace = true
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util.workspace = true
globset = "0.4.16" globset = "0.4.16"
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1.45", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio = { version = "1.45", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.17" tokio-stream.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
arc-swap = "1" arc-swap = "1"
slotmap.workspace = true slotmap.workspace = true

View File

@ -608,8 +608,8 @@ impl Application {
// limit render calls for fast language server messages // limit render calls for fast language server messages
helix_event::request_redraw(); helix_event::request_redraw();
} }
EditorEvent::DebuggerEvent(payload) => { EditorEvent::DebuggerEvent((id, payload)) => {
let needs_render = self.editor.handle_debugger_message(payload).await; let needs_render = self.editor.handle_debugger_message(id, payload).await;
if needs_render { if needs_render {
self.render().await; self.render().await;
} }

View File

@ -6,12 +6,11 @@ use crate::{
}; };
use dap::{StackFrame, Thread, ThreadStates}; use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::config::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_core::syntax::config::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client}; use helix_dap::{self as dap, requests::TerminateArguments};
use helix_lsp::block_on; use helix_lsp::block_on;
use helix_view::editor::Breakpoint; use helix_view::editor::Breakpoint;
use serde_json::{to_value, Value}; use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::text::Spans; use tui::text::Spans;
use std::collections::HashMap; use std::collections::HashMap;
@ -59,7 +58,12 @@ fn thread_picker(
move |cx, thread, _action| callback_fn(cx.editor, thread), move |cx, thread, _action| callback_fn(cx.editor, thread),
) )
.with_preview(move |editor, thread| { .with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; let frames = editor
.debug_adapters
.get_active_client()
.as_ref()?
.stack_frames
.get(&thread.id)?;
let frame = frames.first()?; let frame = frames.first()?;
let path = frame.source.as_ref()?.path.as_ref()?.as_path(); let path = frame.source.as_ref()?.path.as_ref()?.as_path();
let pos = Some(( let pos = Some((
@ -116,34 +120,16 @@ pub fn dap_start_impl(
params: Option<Vec<std::borrow::Cow<str>>>, params: Option<Vec<std::borrow::Cow<str>>>,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let doc = doc!(cx.editor); let doc = doc!(cx.editor);
let config = doc let config = doc
.language_config() .language_config()
.and_then(|config| config.debugger.as_ref()) .and_then(|config| config.debugger.as_ref())
.ok_or_else(|| anyhow!("No debug adapter available for language"))?; .ok_or_else(|| anyhow!("No debug adapter available for language"))?;
let result = match socket { let id = cx
Some(socket) => block_on(Client::tcp(socket, 0)), .editor
None => block_on(Client::process( .debug_adapters
&config.transport, .start_client(socket, config)
&config.command, .map_err(|e| anyhow!("Failed to start debug client: {}", e))?;
config.args.iter().map(|arg| arg.as_str()).collect(),
config.port_arg.as_deref(),
0,
)),
};
let (mut debugger, events) = match result {
Ok(r) => r,
Err(e) => bail!("Failed to start debug session: {}", e),
};
let request = debugger.initialize(config.name.clone());
if let Err(e) = block_on(request) {
bail!("Failed to initialize debug adapter: {}", e);
}
debugger.quirks = config.quirks.clone();
// TODO: avoid refetching all of this... pass a config in // TODO: avoid refetching all of this... pass a config in
let template = match name { let template = match name {
@ -209,6 +195,13 @@ pub fn dap_start_impl(
// } // }
}; };
let debugger = match cx.editor.debug_adapters.get_client_mut(id) {
Some(child) => child,
None => {
bail!("Failed to get child debugger.");
}
};
match &template.request[..] { match &template.request[..] {
"launch" => { "launch" => {
let call = debugger.launch(args); let call = debugger.launch(args);
@ -222,14 +215,12 @@ pub fn dap_start_impl(
}; };
// TODO: either await "initialized" or buffer commands until event is received // TODO: either await "initialized" or buffer commands until event is received
cx.editor.debugger = Some(debugger);
let stream = UnboundedReceiverStream::new(events);
cx.editor.debugger_events.push(stream);
Ok(()) Ok(())
} }
pub fn dap_launch(cx: &mut Context) { pub fn dap_launch(cx: &mut Context) {
if cx.editor.debugger.is_some() { // TODO: Now that we support multiple Clients, we could run multiple debuggers at once but for now keep this as is
if cx.editor.debug_adapters.get_active_client().is_some() {
cx.editor.set_error("Debugger is already running"); cx.editor.set_error("Debugger is already running");
return; return;
} }
@ -283,7 +274,7 @@ pub fn dap_launch(cx: &mut Context) {
} }
pub fn dap_restart(cx: &mut Context) { pub fn dap_restart(cx: &mut Context) {
let debugger = match &cx.editor.debugger { let debugger = match cx.editor.debug_adapters.get_active_client() {
Some(debugger) => debugger, Some(debugger) => debugger,
None => { None => {
cx.editor.set_error("Debugger is not running"); cx.editor.set_error("Debugger is not running");
@ -582,12 +573,17 @@ pub fn dap_variables(cx: &mut Context) {
} }
pub fn dap_terminate(cx: &mut Context) { pub fn dap_terminate(cx: &mut Context) {
cx.editor.set_status("Terminating debug session...");
let debugger = debugger!(cx.editor); let debugger = debugger!(cx.editor);
let request = debugger.disconnect(None); let terminate_arguments = Some(TerminateArguments {
restart: Some(false),
});
let request = debugger.terminate(terminate_arguments);
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| { dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
// editor.set_error(format!("Failed to disconnect: {}", e)); // editor.set_error(format!("Failed to disconnect: {}", e));
editor.debugger = None; editor.debug_adapters.unset_active_client();
}); });
} }

View File

@ -1794,7 +1794,7 @@ fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a
return Ok(()); return Ok(());
} }
if let Some(debugger) = cx.editor.debugger.as_mut() { if let Some(debugger) = cx.editor.debug_adapters.get_active_client() {
let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) { let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
(Some(frame), Some(thread_id)) => (frame, thread_id), (Some(frame), Some(thread_id)) => (frame, thread_id),
_ => { _ => {

View File

@ -14,7 +14,6 @@ use crate::{
tree::{self, Tree}, tree::{self, Tree},
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
}; };
use dap::StackFrame;
use helix_event::dispatch; use helix_event::dispatch;
use helix_vcs::DiffProviderRegistry; use helix_vcs::DiffProviderRegistry;
@ -52,7 +51,7 @@ use helix_core::{
}, },
Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING, Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING,
}; };
use helix_dap as dap; use helix_dap::{self as dap, registry::DebugAdapterId};
use helix_lsp::lsp; use helix_lsp::lsp;
use helix_stdx::path::canonicalize; use helix_stdx::path::canonicalize;
@ -1083,8 +1082,7 @@ pub struct Editor {
pub diagnostics: Diagnostics, pub diagnostics: Diagnostics,
pub diff_providers: DiffProviderRegistry, pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>, pub debug_adapters: dap::registry::Registry,
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>, pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
pub syn_loader: Arc<ArcSwap<syntax::Loader>>, pub syn_loader: Arc<ArcSwap<syntax::Loader>>,
@ -1142,7 +1140,7 @@ pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult), DocumentSaved(DocumentSavedEventResult),
ConfigEvent(ConfigEvent), ConfigEvent(ConfigEvent),
LanguageServerMessage((LanguageServerId, Call)), LanguageServerMessage((LanguageServerId, Call)),
DebuggerEvent(dap::Payload), DebuggerEvent((DebugAdapterId, dap::Payload)),
IdleTimer, IdleTimer,
Redraw, Redraw,
} }
@ -1229,8 +1227,7 @@ impl Editor {
language_servers, language_servers,
diagnostics: Diagnostics::new(), diagnostics: Diagnostics::new(),
diff_providers: DiffProviderRegistry::default(), diff_providers: DiffProviderRegistry::default(),
debugger: None, debug_adapters: dap::registry::Registry::new(),
debugger_events: SelectAll::new(),
breakpoints: HashMap::new(), breakpoints: HashMap::new(),
syn_loader, syn_loader,
theme_loader, theme_loader,
@ -2154,7 +2151,7 @@ impl Editor {
Some(message) = self.language_servers.incoming.next() => { Some(message) = self.language_servers.incoming.next() => {
return EditorEvent::LanguageServerMessage(message) return EditorEvent::LanguageServerMessage(message)
} }
Some(event) = self.debugger_events.next() => { Some(event) = self.debug_adapters.incoming.next() => {
return EditorEvent::DebuggerEvent(event) return EditorEvent::DebuggerEvent(event)
} }
@ -2230,10 +2227,8 @@ impl Editor {
} }
} }
pub fn current_stack_frame(&self) -> Option<&StackFrame> { pub fn current_stack_frame(&self) -> Option<&dap::StackFrame> {
self.debugger self.debug_adapters.current_stack_frame()
.as_ref()
.and_then(|debugger| debugger.current_stack_frame())
} }
/// Returns the id of a view that this doc contains a selection for, /// Returns the id of a view that this doc contains a selection for,

View File

@ -2,20 +2,22 @@ use crate::editor::{Action, Breakpoint};
use crate::{align_view, Align, Editor}; use crate::{align_view, Align, Editor};
use dap::requests::DisconnectArguments; use dap::requests::DisconnectArguments;
use helix_core::Selection; use helix_core::Selection;
use helix_dap::{self as dap, Client, ConnectionType, Payload, Request, ThreadId}; use helix_dap::{
self as dap, registry::DebugAdapterId, Client, ConnectionType, Payload, Request, ThreadId,
};
use helix_lsp::block_on; use helix_lsp::block_on;
use log::warn; use log::{error, warn};
use serde_json::json; use serde_json::{json, Value};
use std::fmt::Write; use std::fmt::Write;
use std::path::PathBuf; use std::path::PathBuf;
#[macro_export] #[macro_export]
macro_rules! debugger { macro_rules! debugger {
($editor:expr) => {{ ($editor:expr) => {{
match &mut $editor.debugger { let Some(debugger) = $editor.debug_adapters.get_active_client_mut() else {
Some(debugger) => debugger, return;
None => return, };
} debugger
}}; }};
} }
@ -141,13 +143,13 @@ 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,
id: DebugAdapterId,
payload: helix_dap::Payload,
) -> bool {
use helix_dap::{events, Event}; use helix_dap::{events, Event};
let debugger = match self.debugger.as_mut() {
Some(debugger) => debugger,
None => return false,
};
match payload { match payload {
Payload::Event(event) => { Payload::Event(event) => {
let event = match Event::parse(&event.event, event.body) { let event = match Event::parse(&event.event, event.body) {
@ -170,6 +172,11 @@ impl Editor {
all_threads_stopped, all_threads_stopped,
.. ..
}) => { }) => {
let debugger = match self.debug_adapters.get_client_mut(id) {
Some(debugger) => debugger,
None => return false,
};
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 {
@ -184,6 +191,7 @@ impl Editor {
} else if let Some(thread_id) = thread_id { } else if let Some(thread_id) = thread_id {
debugger.thread_states.insert(thread_id, reason.clone()); // TODO: dap uses "type" || "reason" here debugger.thread_states.insert(thread_id, reason.clone()); // TODO: dap uses "type" || "reason" here
fetch_stack_trace(debugger, thread_id).await;
// whichever thread stops is made "current" (if no previously selected thread). // whichever thread stops is made "current" (if no previously selected thread).
select_thread_id(self, thread_id, false).await; select_thread_id(self, thread_id, false).await;
} }
@ -205,8 +213,14 @@ impl Editor {
} }
self.set_status(status); self.set_status(status);
self.debug_adapters.set_active_client(id);
} }
Event::Continued(events::ContinuedBody { thread_id, .. }) => { Event::Continued(events::ContinuedBody { thread_id, .. }) => {
let debugger = match self.debug_adapters.get_client_mut(id) {
Some(debugger) => debugger,
None => return false,
};
debugger debugger
.thread_states .thread_states
.insert(thread_id, "running".to_owned()); .insert(thread_id, "running".to_owned());
@ -214,8 +228,15 @@ impl Editor {
debugger.resume_application(); debugger.resume_application();
} }
} }
Event::Thread(_) => { Event::Thread(thread) => {
// TODO: update thread_states, make threads request self.set_status(format!("Thread {}: {}", thread.thread_id, thread.reason));
let debugger = match self.debug_adapters.get_client_mut(id) {
Some(debugger) => debugger,
None => return false,
};
debugger.thread_id = Some(thread.thread_id);
// set the stack frame for the thread
} }
Event::Breakpoint(events::BreakpointBody { reason, breakpoint }) => { Event::Breakpoint(events::BreakpointBody { reason, breakpoint }) => {
match &reason[..] { match &reason[..] {
@ -284,6 +305,12 @@ impl Editor {
self.set_status(format!("{} {}", prefix, output)); self.set_status(format!("{} {}", prefix, output));
} }
Event::Initialized(_) => { Event::Initialized(_) => {
self.set_status("Debugger initialized...");
let debugger = match self.debug_adapters.get_client_mut(id) {
Some(debugger) => debugger,
None => return false,
};
// send existing breakpoints // send existing breakpoints
for (path, breakpoints) in &mut self.breakpoints { for (path, breakpoints) in &mut self.breakpoints {
// TODO: call futures in parallel, await all // TODO: call futures in parallel, await all
@ -296,14 +323,23 @@ impl Editor {
}; // TODO: do we need to handle error? }; // TODO: do we need to handle error?
} }
Event::Terminated(terminated) => { Event::Terminated(terminated) => {
let restart_args = if let Some(terminated) = terminated { let debugger = match self.debug_adapters.get_client_mut(id) {
Some(debugger) => debugger,
None => return false,
};
let restart_arg = if let Some(terminated) = terminated {
terminated.restart terminated.restart
} else { } else {
None None
}; };
let restart_bool = restart_arg
.as_ref()
.and_then(|v| v.as_bool())
.unwrap_or(false);
let disconnect_args = Some(DisconnectArguments { let disconnect_args = Some(DisconnectArguments {
restart: Some(restart_args.is_some()), restart: Some(restart_bool),
terminate_debuggee: None, terminate_debuggee: None,
suspend_debuggee: None, suspend_debuggee: None,
}); });
@ -316,8 +352,23 @@ impl Editor {
return false; return false;
} }
match restart_args { match restart_arg {
Some(restart_args) => { Some(Value::Bool(false)) | None => {
self.debug_adapters.remove_client(id);
self.debug_adapters.unset_active_client();
self.set_status(
"Terminated debugging session and disconnected debugger.",
);
// Go through all breakpoints and set verfified to false
// this should update the UI to show the breakpoints are no longer connected
for breakpoints in self.breakpoints.values_mut() {
for breakpoint in breakpoints.iter_mut() {
breakpoint.verified = false;
}
}
}
Some(val) => {
log::info!("Attempting to restart debug session."); log::info!("Attempting to restart debug session.");
let connection_type = match debugger.connection_type() { let connection_type = match debugger.connection_type() {
Some(connection_type) => connection_type, Some(connection_type) => connection_type,
@ -329,9 +380,9 @@ 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(val).await
} else { } else {
debugger.attach(restart_args).await debugger.attach(val).await
}; };
if let Err(err) = relaunch_resp { if let Err(err) = relaunch_resp {
@ -341,12 +392,6 @@ impl Editor {
)); ));
} }
} }
None => {
self.debugger = None;
self.set_status(
"Terminated debugging session and disconnected debugger.",
);
}
} }
} }
Event::Exited(resp) => { Event::Exited(resp) => {
@ -393,10 +438,70 @@ impl Editor {
shell_process_id: None, shell_process_id: None,
})) }))
} }
Ok(Request::StartDebugging(arguments)) => {
let debugger = match self.debug_adapters.get_client_mut(id) {
Some(debugger) => debugger,
None => {
self.set_error("No active debugger found.");
return true;
}
};
// Currently we only support starting a child debugger if the parent is using the TCP transport
let socket = match debugger.socket {
Some(socket) => socket,
None => {
self.set_error("Child debugger can only be started if the parent debugger is using TCP transport.");
return true;
}
};
let config = match debugger.config.clone() {
Some(config) => config,
None => {
error!("No configuration found for the debugger.");
return true;
}
};
let result = self.debug_adapters.start_client(Some(socket), &config);
let client_id = match result {
Ok(child) => child,
Err(err) => {
self.set_error(format!(
"Failed to create child debugger: {:?}",
err
));
return true;
}
};
let client = match self.debug_adapters.get_client_mut(client_id) {
Some(child) => child,
None => {
self.set_error("Failed to get child debugger.");
return true;
}
};
let relaunch_resp = if let ConnectionType::Launch = arguments.request {
client.launch(arguments.configuration).await
} else {
client.attach(arguments.configuration).await
};
if let Err(err) = relaunch_resp {
self.set_error(format!("Failed to start debugging session: {:?}", err));
return true;
}
Ok(json!({
"success": true,
}))
}
Err(err) => Err(err), Err(err) => Err(err),
}; };
if let Some(debugger) = self.debugger.as_mut() { if let Some(debugger) = self.debug_adapters.get_client_mut(id) {
debugger debugger
.reply(request.seq, &request.command, reply) .reply(request.seq, &request.command, reply)
.await .await

View File

@ -823,8 +823,9 @@ language-servers = [ "typescript-language-server" ]
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[language.debugger] [language.debugger]
name = "node-debug2" name = "js-debug-dap"
transport = "stdio" transport = "tcp"
port-arg = "{} 127.0.0.1"
# args consisting of cmd (node) and path to adapter should be added to user's configuration # args consisting of cmd (node) and path to adapter should be added to user's configuration
quirks = { absolute-paths = true } quirks = { absolute-paths = true }
@ -832,7 +833,7 @@ quirks = { absolute-paths = true }
name = "source" name = "source"
request = "launch" request = "launch"
completion = [ { name = "main", completion = "filename", default = "index.js" } ] completion = [ { name = "main", completion = "filename", default = "index.js" } ]
args = { program = "{0}" } args = { program = "{0}", skipFiles = [ "<node_internals>/**" ] }
[[grammar]] [[grammar]]
name = "javascript" name = "javascript"