diff --git a/Cargo.lock b/Cargo.lock index 5158cf883..5c78f7a08 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 770062286..ecb5c7e26 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 d67932afb..8033c7577 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 1529b6f93..e5824a7fd 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, server_tx: UnboundedSender, request_counter: AtomicU64, connection_type: Option, starting_request_args: Option, + /// The socket address of the debugger, if using TCP transport. + pub socket: Option, pub caps: Option, // thread_id -> frames pub stack_frames: HashMap>, @@ -41,23 +44,20 @@ pub struct Client { /// Currently active frame for the current thread. pub active_frame: Option, pub quirks: DebuggerQuirks, -} - -#[derive(Clone, Copy, Debug)] -pub enum ConnectionType { - Launch, - Attach, + /// The config which was used to start this debugger. + pub config: Option, } 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)> { + 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, tx: Box, err: Option>, - id: usize, + id: DebugAdapterId, process: Option, - ) -> Result<(Self, UnboundedReceiver)> { + ) -> 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)> { + 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)> { + 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)> { + 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, client_tx: UnboundedSender) { + async fn recv( + id: DebugAdapterId, + mut server_rx: UnboundedReceiver, + 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::(args) } + pub fn terminate( + &mut self, + args: Option, + ) -> impl Future> { + self.connection_type = None; + self.call::(args) + } + pub fn launch(&mut self, args: serde_json::Value) -> impl Future> { 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 b0605c4f0..16c84f662 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 = core::result::Result; #[derive(Debug)] pub enum Request { RunInTerminal(::Arguments), + StartDebugging(::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 000000000..a69370204 --- /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, + /// 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, + /// A stream of incoming messages from all debuggers + pub incoming: SelectAll>, +} + +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, + config: &DebugAdapterConfig, + ) -> Result { + 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 6911e4e72..8ca408df1 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>>>, } @@ -61,7 +61,7 @@ impl Transport { server_stdout: Box, server_stdin: Box, server_stderr: Option>, - id: usize, + id: DebugAdapterId, ) -> (UnboundedReceiver, UnboundedSender) { 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, buffer: &mut String, content: &mut Vec, @@ -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 = serde_json::from_str(msg); @@ -164,7 +165,7 @@ impl Transport { server_stdin: &mut Box, 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 { + fn process_response(&self, res: Response) -> Result { 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, mut server_stdout: Box, client_tx: UnboundedSender, @@ -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 67f4937f1..fdfc211ad 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, + } + + #[derive(Debug)] + pub enum Terminate {} + + impl Request for Terminate { + type Arguments = Option; + 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 83e37a982..39e750ad7 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 2b2ff8551..dd19a2d72 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 4f20af4af..f6f11d126 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>>, ) -> 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 0f0165a6f..e1bb8ee32 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 89f053741..575a0b5f1 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, - pub debugger_events: SelectAll>, + pub debug_adapters: dap::registry::Registry, pub breakpoints: HashMap>, pub syn_loader: Arc>, @@ -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 56eb8efa9..22ba3427c 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 2be8845cc..536962e1e 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 = [ "/**" ] } [[grammar]] name = "javascript"