mirror of https://github.com/helix-editor/helix
DAP: Support the startDebugging reverse request (#13403)
parent
58dfa158c2
commit
2338b44909
|
@ -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]]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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"}"#;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
_ => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue