Unnamed repository; edit this file 'description' to name the repository.
DAP: Support the startDebugging reverse request (#13403)
Jason Williams 8 months ago
parent 58dfa15 · commit 2338b44
-rw-r--r--Cargo.lock4
-rw-r--r--Cargo.toml3
-rw-r--r--helix-dap/Cargo.toml5
-rw-r--r--helix-dap/src/client.rs83
-rw-r--r--helix-dap/src/lib.rs5
-rw-r--r--helix-dap/src/registry.rs114
-rw-r--r--helix-dap/src/transport.rs36
-rw-r--r--helix-dap/src/types.rs37
-rw-r--r--helix-lsp/Cargo.toml6
-rw-r--r--helix-term/src/application.rs4
-rw-r--r--helix-term/src/commands/dap.rs62
-rw-r--r--helix-term/src/commands/typed.rs2
-rw-r--r--helix-view/src/editor.rs19
-rw-r--r--helix-view/src/handlers/dap.rs159
-rw-r--r--languages.toml7
15 files changed, 417 insertions, 129 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5158cf88..5c78f7a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1439,13 +1439,17 @@ version = "25.1.1"
dependencies = [
"anyhow",
"fern",
+ "futures-executor",
+ "futures-util",
"helix-core",
"helix-stdx",
"log",
"serde",
"serde_json",
+ "slotmap",
"thiserror 2.0.12",
"tokio",
+ "tokio-stream",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 77006228..ecb5c7e2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,6 +47,9 @@ unicode-segmentation = "1.2"
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
foldhash = "0.1"
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]
version = "25.1.1"
diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml
index d67932af..8033c757 100644
--- a/helix-dap/Cargo.toml
+++ b/helix-dap/Cargo.toml
@@ -22,6 +22,11 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
thiserror.workspace = true
+slotmap.workspace = true
+futures-executor.workspace = true
+futures-util.workspace = true
+tokio-stream.workspace = true
+
[dev-dependencies]
fern = "0.7"
diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs
index 1529b6f9..e5824a7f 100644
--- a/helix-dap/src/client.rs
+++ b/helix-dap/src/client.rs
@@ -1,10 +1,11 @@
use crate::{
- requests::DisconnectArguments,
+ registry::DebugAdapterId,
+ requests::{DisconnectArguments, TerminateArguments},
transport::{Payload, Request, Response, Transport},
types::*,
Error, Result,
};
-use helix_core::syntax::config::DebuggerQuirks;
+use helix_core::syntax::config::{DebugAdapterConfig, DebuggerQuirks};
use serde_json::Value;
@@ -27,12 +28,14 @@ use tokio::{
#[derive(Debug)]
pub struct Client {
- id: usize,
+ id: DebugAdapterId,
_process: Option<Child>,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
connection_type: Option<ConnectionType>,
starting_request_args: Option<Value>,
+ /// The socket address of the debugger, if using TCP transport.
+ pub socket: Option<SocketAddr>,
pub caps: Option<DebuggerCapabilities>,
// thread_id -> frames
pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
@@ -41,23 +44,20 @@ pub struct Client {
/// Currently active frame for the current thread.
pub active_frame: Option<usize>,
pub quirks: DebuggerQuirks,
-}
-
-#[derive(Clone, Copy, Debug)]
-pub enum ConnectionType {
- Launch,
- Attach,
+ /// The config which was used to start this debugger.
+ pub config: Option<DebugAdapterConfig>,
}
impl Client {
// 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(
transport: &str,
command: &str,
args: Vec<&str>,
port_arg: Option<&str>,
- id: usize,
- ) -> Result<(Self, UnboundedReceiver<Payload>)> {
+ id: DebugAdapterId,
+ ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
if command.is_empty() {
return Result::Err(Error::Other(anyhow!("Command not provided")));
}
@@ -72,9 +72,9 @@ impl Client {
rx: Box<dyn AsyncBufRead + Unpin + Send>,
tx: Box<dyn AsyncWrite + Unpin + Send>,
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
- id: usize,
+ id: DebugAdapterId,
process: Option<Child>,
- ) -> Result<(Self, UnboundedReceiver<Payload>)> {
+ ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let (server_rx, server_tx) = Transport::start(rx, tx, err, id);
let (client_tx, client_rx) = unbounded_channel();
@@ -86,22 +86,24 @@ impl Client {
caps: None,
connection_type: None,
starting_request_args: None,
+ socket: None,
stack_frames: HashMap::new(),
thread_states: HashMap::new(),
thread_id: None,
active_frame: None,
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))
}
pub async fn tcp(
addr: std::net::SocketAddr,
- id: usize,
- ) -> Result<(Self, UnboundedReceiver<Payload>)> {
+ id: DebugAdapterId,
+ ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let stream = TcpStream::connect(addr).await?;
let (rx, tx) = stream.into_split();
Self::streams(Box::new(BufReader::new(rx)), Box::new(tx), None, id, None)
@@ -110,8 +112,8 @@ impl Client {
pub fn stdio(
cmd: &str,
args: Vec<&str>,
- id: usize,
- ) -> Result<(Self, UnboundedReceiver<Payload>)> {
+ id: DebugAdapterId,
+ ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
// Resolve path to the binary
let cmd = helix_stdx::env::which(cmd)?;
@@ -162,8 +164,8 @@ impl Client {
cmd: &str,
args: Vec<&str>,
port_format: &str,
- id: usize,
- ) -> Result<(Self, UnboundedReceiver<Payload>)> {
+ id: DebugAdapterId,
+ ) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let port = Self::get_port().await.unwrap();
let process = Command::new(cmd)
@@ -178,40 +180,49 @@ impl Client {
// Wait for adapter to become ready for connection
time::sleep(time::Duration::from_millis(500)).await;
-
- let stream = TcpStream::connect(SocketAddr::new(
- IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
- port,
- ))
- .await?;
+ let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
+ let stream = TcpStream::connect(socket).await?;
let (rx, tx) = stream.into_split();
- Self::streams(
+ let mut result = Self::streams(
Box::new(BufReader::new(rx)),
Box::new(tx),
None,
id,
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 {
match msg {
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::Request(req) => {
client_tx
- .send(Payload::Request(req))
+ .send((id, Payload::Request(req)))
.expect("Failed to send");
}
}
}
}
- pub fn id(&self) -> usize {
+ pub fn id(&self) -> DebugAdapterId {
self.id
}
@@ -354,6 +365,14 @@ impl Client {
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>> {
self.connection_type = Some(ConnectionType::Launch);
self.starting_request_args = Some(args.clone());
diff --git a/helix-dap/src/lib.rs b/helix-dap/src/lib.rs
index b0605c4f..16c84f66 100644
--- a/helix-dap/src/lib.rs
+++ b/helix-dap/src/lib.rs
@@ -1,8 +1,9 @@
mod client;
+pub mod registry;
mod transport;
mod types;
-pub use client::{Client, ConnectionType};
+pub use client::Client;
pub use transport::{Payload, Response, Transport};
pub use types::*;
@@ -31,6 +32,7 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub enum Request {
RunInTerminal(<requests::RunInTerminal as types::Request>::Arguments),
+ StartDebugging(<requests::StartDebugging as types::Request>::Arguments),
}
impl Request {
@@ -40,6 +42,7 @@ impl Request {
let arguments = arguments.unwrap_or_default();
let request = match command {
requests::RunInTerminal::COMMAND => Self::RunInTerminal(parse_value(arguments)?),
+ requests::StartDebugging::COMMAND => Self::StartDebugging(parse_value(arguments)?),
_ => return Err(Error::Unhandled),
};
diff --git a/helix-dap/src/registry.rs b/helix-dap/src/registry.rs
new file mode 100644
index 00000000..a6937020
--- /dev/null
+++ b/helix-dap/src/registry.rs
@@ -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)
+ }
+}
diff --git a/helix-dap/src/transport.rs b/helix-dap/src/transport.rs
index 6911e4e7..8ca408df 100644
--- a/helix-dap/src/transport.rs
+++ b/helix-dap/src/transport.rs
@@ -1,10 +1,10 @@
-use crate::{Error, Result};
+use crate::{registry::DebugAdapterId, Error, Result};
use anyhow::Context;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value;
-use std::collections::HashMap;
use std::sync::Arc;
+use std::{collections::HashMap, fmt::Debug};
use tokio::{
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt},
sync::{
@@ -52,7 +52,7 @@ pub enum Payload {
#[derive(Debug)]
pub struct Transport {
#[allow(unused)]
- id: usize,
+ id: DebugAdapterId,
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
}
@@ -61,7 +61,7 @@ impl Transport {
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
- id: usize,
+ id: DebugAdapterId,
) -> (UnboundedReceiver<Payload>, UnboundedSender<Payload>) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
@@ -73,7 +73,7 @@ impl 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));
if let Some(stderr) = server_stderr {
tokio::spawn(Self::err(stderr));
@@ -83,6 +83,7 @@ impl Transport {
}
async fn recv_server_message(
+ id: DebugAdapterId,
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
buffer: &mut String,
content: &mut Vec<u8>,
@@ -122,7 +123,7 @@ impl Transport {
reader.read_exact(content).await?;
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)
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>,
request: String,
) -> Result<()> {
- info!("-> DAP {}", request);
+ info!("[{}] -> DAP {}", self.id, request);
// send the headers
server_stdin
@@ -179,15 +180,18 @@ impl Transport {
Ok(())
}
- fn process_response(res: Response) -> Result<Response> {
+ fn process_response(&self, res: Response) -> Result<Response> {
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)
} else {
error!(
- "<- DAP error {:?} ({:?}) for command #{} {}",
- res.message, res.body, res.request_seq, res.command
+ "[{}] <- DAP error {:?} ({:?}) for command #{} {}",
+ self.id, res.message, res.body, res.request_seq, res.command
);
Err(Error::Other(anyhow::format_err!("{:?}", res.body)))
@@ -205,7 +209,7 @@ impl Transport {
let tx = self.pending_requests.lock().await.remove(&request_seq);
match tx {
- Some(tx) => match tx.send(Self::process_response(res)).await {
+ Some(tx) => match tx.send(self.process_response(res)).await {
Ok(_) => (),
Err(_) => error!(
"Tried sending response into a closed channel (id={:?}), original request likely timed out",
@@ -225,12 +229,12 @@ impl Transport {
ref seq,
..
}) => {
- info!("<- DAP request {} #{}", command, seq);
+ info!("[{}] <- DAP request {} #{}", self.id, command, seq);
client_tx.send(msg).expect("Failed to send");
Ok(())
}
Payload::Event(ref event) => {
- info!("<- DAP event {:?}", event);
+ info!("[{}] <- DAP event {:?}", self.id, event);
client_tx.send(msg).expect("Failed to send");
Ok(())
}
@@ -238,6 +242,7 @@ impl Transport {
}
async fn recv(
+ id: DebugAdapterId,
transport: Arc<Self>,
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: UnboundedSender<Payload>,
@@ -246,6 +251,7 @@ impl Transport {
let mut content_buffer = Vec::new();
loop {
match Self::recv_server_message(
+ id,
&mut server_stdout,
&mut recv_buffer,
&mut content_buffer,
@@ -255,7 +261,7 @@ impl Transport {
Ok(msg) => match transport.process_server_message(&client_tx, msg).await {
Ok(_) => (),
Err(err) => {
- error!("err: <- {err:?}");
+ error!(" [{id}] err: <- {err:?}");
break;
}
},
diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs
index 67f4937f..fdfc211a 100644
--- a/helix-dap/src/types.rs
+++ b/helix-dap/src/types.rs
@@ -438,6 +438,21 @@ pub mod requests {
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)]
pub enum ConfigurationDone {}
@@ -752,6 +767,21 @@ pub mod requests {
type Result = RunInTerminalResponse;
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
@@ -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]
fn test_deserialize_module_id_from_number() {
let raw = r#"{"id": 0, "name": "Name"}"#;
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 83e37a98..39e750ad 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -19,14 +19,14 @@ helix-loader = { path = "../helix-loader" }
helix-lsp-types = { path = "../helix-lsp-types" }
anyhow = "1.0"
-futures-executor = "0.3"
-futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
+futures-executor.workspace = true
+futures-util.workspace = true
globset = "0.4.16"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
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-stream = "0.1.17"
+tokio-stream.workspace = true
parking_lot.workspace = true
arc-swap = "1"
slotmap.workspace = true
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 2b2ff855..dd19a2d7 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -608,8 +608,8 @@ impl Application {
// limit render calls for fast language server messages
helix_event::request_redraw();
}
- EditorEvent::DebuggerEvent(payload) => {
- let needs_render = self.editor.handle_debugger_message(payload).await;
+ EditorEvent::DebuggerEvent((id, payload)) => {
+ let needs_render = self.editor.handle_debugger_message(id, payload).await;
if needs_render {
self.render().await;
}
diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs
index 4f20af4a..f6f11d12 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -6,12 +6,11 @@ use crate::{
};
use dap::{StackFrame, Thread, ThreadStates};
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_view::editor::Breakpoint;
use serde_json::{to_value, Value};
-use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::text::Spans;
use std::collections::HashMap;
@@ -59,7 +58,12 @@ fn thread_picker(
move |cx, thread, _action| callback_fn(cx.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 path = frame.source.as_ref()?.path.as_ref()?.as_path();
let pos = Some((
@@ -116,34 +120,16 @@ pub fn dap_start_impl(
params: Option<Vec<std::borrow::Cow<str>>>,
) -> Result<(), anyhow::Error> {
let doc = doc!(cx.editor);
-
let config = doc
.language_config()
.and_then(|config| config.debugger.as_ref())
.ok_or_else(|| anyhow!("No debug adapter available for language"))?;
- let result = match socket {
- Some(socket) => block_on(Client::tcp(socket, 0)),
- None => block_on(Client::process(
- &config.transport,
- &config.command,
- 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();
+ let id = cx
+ .editor
+ .debug_adapters
+ .start_client(socket, config)
+ .map_err(|e| anyhow!("Failed to start debug client: {}", e))?;
// TODO: avoid refetching all of this... pass a config in
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[..] {
"launch" => {
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
- cx.editor.debugger = Some(debugger);
- let stream = UnboundedReceiverStream::new(events);
- cx.editor.debugger_events.push(stream);
Ok(())
}
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");
return;
}
@@ -283,7 +274,7 @@ pub fn dap_launch(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,
None => {
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) {
+ cx.editor.set_status("Terminating debug session...");
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: ()| {
// editor.set_error(format!("Failed to disconnect: {}", e));
- editor.debugger = None;
+ editor.debug_adapters.unset_active_client();
});
}
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 0f0165a6..e1bb8ee3 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1794,7 +1794,7 @@ fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a
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) {
(Some(frame), Some(thread_id)) => (frame, thread_id),
_ => {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 89f05374..575a0b5f 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -14,7 +14,6 @@ use crate::{
tree::{self, Tree},
Document, DocumentId, View, ViewId,
};
-use dap::StackFrame;
use helix_event::dispatch;
use helix_vcs::DiffProviderRegistry;
@@ -52,7 +51,7 @@ use helix_core::{
},
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_stdx::path::canonicalize;
@@ -1083,8 +1082,7 @@ pub struct Editor {
pub diagnostics: Diagnostics,
pub diff_providers: DiffProviderRegistry,
- pub debugger: Option<dap::Client>,
- pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
+ pub debug_adapters: dap::registry::Registry,
pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
pub syn_loader: Arc<ArcSwap<syntax::Loader>>,
@@ -1142,7 +1140,7 @@ pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult),
ConfigEvent(ConfigEvent),
LanguageServerMessage((LanguageServerId, Call)),
- DebuggerEvent(dap::Payload),
+ DebuggerEvent((DebugAdapterId, dap::Payload)),
IdleTimer,
Redraw,
}
@@ -1229,8 +1227,7 @@ impl Editor {
language_servers,
diagnostics: Diagnostics::new(),
diff_providers: DiffProviderRegistry::default(),
- debugger: None,
- debugger_events: SelectAll::new(),
+ debug_adapters: dap::registry::Registry::new(),
breakpoints: HashMap::new(),
syn_loader,
theme_loader,
@@ -2154,7 +2151,7 @@ impl Editor {
Some(message) = self.language_servers.incoming.next() => {
return EditorEvent::LanguageServerMessage(message)
}
- Some(event) = self.debugger_events.next() => {
+ Some(event) = self.debug_adapters.incoming.next() => {
return EditorEvent::DebuggerEvent(event)
}
@@ -2230,10 +2227,8 @@ impl Editor {
}
}
- pub fn current_stack_frame(&self) -> Option<&StackFrame> {
- self.debugger
- .as_ref()
- .and_then(|debugger| debugger.current_stack_frame())
+ pub fn current_stack_frame(&self) -> Option<&dap::StackFrame> {
+ self.debug_adapters.current_stack_frame()
}
/// Returns the id of a view that this doc contains a selection for,
diff --git a/helix-view/src/handlers/dap.rs b/helix-view/src/handlers/dap.rs
index 56eb8efa..22ba3427 100644
--- a/helix-view/src/handlers/dap.rs
+++ b/helix-view/src/handlers/dap.rs
@@ -2,20 +2,22 @@ use crate::editor::{Action, Breakpoint};
use crate::{align_view, Align, Editor};
use dap::requests::DisconnectArguments;
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 log::warn;
-use serde_json::json;
+use log::{error, warn};
+use serde_json::{json, Value};
use std::fmt::Write;
use std::path::PathBuf;
#[macro_export]
macro_rules! debugger {
($editor:expr) => {{
- match &mut $editor.debugger {
- Some(debugger) => debugger,
- None => return,
- }
+ let Some(debugger) = $editor.debug_adapters.get_active_client_mut() else {
+ return;
+ };
+ debugger
}};
}
@@ -141,13 +143,13 @@ pub fn breakpoints_changed(
}
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};
- let debugger = match self.debugger.as_mut() {
- Some(debugger) => debugger,
- None => return false,
- };
match payload {
Payload::Event(event) => {
let event = match Event::parse(&event.event, event.body) {
@@ -170,6 +172,11 @@ impl Editor {
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();
if all_threads_stopped {
@@ -184,6 +191,7 @@ impl Editor {
} else if let Some(thread_id) = thread_id {
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).
select_thread_id(self, thread_id, false).await;
}
@@ -205,8 +213,14 @@ impl Editor {
}
self.set_status(status);
+ self.debug_adapters.set_active_client(id);
}
Event::Continued(events::ContinuedBody { thread_id, .. }) => {
+ let debugger = match self.debug_adapters.get_client_mut(id) {
+ Some(debugger) => debugger,
+ None => return false,
+ };
+
debugger
.thread_states
.insert(thread_id, "running".to_owned());
@@ -214,8 +228,15 @@ impl Editor {
debugger.resume_application();
}
}
- Event::Thread(_) => {
- // TODO: update thread_states, make threads request
+ Event::Thread(thread) => {
+ 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 }) => {
match &reason[..] {
@@ -284,6 +305,12 @@ impl Editor {
self.set_status(format!("{} {}", prefix, output));
}
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
for (path, breakpoints) in &mut self.breakpoints {
// TODO: call futures in parallel, await all
@@ -296,14 +323,23 @@ impl Editor {
}; // TODO: do we need to handle error?
}
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
} else {
None
};
+ let restart_bool = restart_arg
+ .as_ref()
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
let disconnect_args = Some(DisconnectArguments {
- restart: Some(restart_args.is_some()),
+ restart: Some(restart_bool),
terminate_debuggee: None,
suspend_debuggee: None,
});
@@ -316,8 +352,23 @@ impl Editor {
return false;
}
- match restart_args {
- Some(restart_args) => {
+ match restart_arg {
+ 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.");
let connection_type = match debugger.connection_type() {
Some(connection_type) => connection_type,
@@ -329,9 +380,9 @@ impl Editor {
let relaunch_resp = if let ConnectionType::Launch = connection_type
{
- debugger.launch(restart_args).await
+ debugger.launch(val).await
} else {
- debugger.attach(restart_args).await
+ debugger.attach(val).await
};
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) => {
@@ -393,10 +438,70 @@ impl Editor {
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),
};
- if let Some(debugger) = self.debugger.as_mut() {
+ if let Some(debugger) = self.debug_adapters.get_client_mut(id) {
debugger
.reply(request.seq, &request.command, reply)
.await
diff --git a/languages.toml b/languages.toml
index 2be8845c..536962e1 100644
--- a/languages.toml
+++ b/languages.toml
@@ -823,8 +823,9 @@ language-servers = [ "typescript-language-server" ]
indent = { tab-width = 2, unit = " " }
[language.debugger]
-name = "node-debug2"
-transport = "stdio"
+name = "js-debug-dap"
+transport = "tcp"
+port-arg = "{} 127.0.0.1"
# args consisting of cmd (node) and path to adapter should be added to user's configuration
quirks = { absolute-paths = true }
@@ -832,7 +833,7 @@ quirks = { absolute-paths = true }
name = "source"
request = "launch"
completion = [ { name = "main", completion = "filename", default = "index.js" } ]
-args = { program = "{0}" }
+args = { program = "{0}", skipFiles = [ "<node_internals>/**" ] }
[[grammar]]
name = "javascript"