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 = [
"anyhow",
"fern",
"futures-executor",
"futures-util",
"helix-core",
"helix-stdx",
"log",
"serde",
"serde_json",
"slotmap",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
]
[[package]]

View File

@ -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"

View File

@ -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"

View File

@ -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());

View File

@ -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),
};

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 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;
}
},

View File

@ -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"}"#;

View File

@ -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

View File

@ -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;
}

View File

@ -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();
});
}

View File

@ -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),
_ => {

View File

@ -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,

View File

@ -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

View File

@ -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"