diff options
-rw-r--r-- | Cargo.lock | 19 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | helix-core/Cargo.toml | 2 | ||||
-rw-r--r-- | helix-core/src/indent.rs | 2 | ||||
-rw-r--r-- | helix-core/src/syntax.rs | 3 | ||||
-rw-r--r-- | helix-dap/Cargo.toml | 23 | ||||
-rw-r--r-- | helix-dap/examples/dap-dlv.rs | 117 | ||||
-rw-r--r-- | helix-dap/examples/dap-lldb.rs | 116 | ||||
-rw-r--r-- | helix-dap/src/client.rs | 396 | ||||
-rw-r--r-- | helix-dap/src/lib.rs | 24 | ||||
-rw-r--r-- | helix-dap/src/transport.rs | 246 | ||||
-rw-r--r-- | helix-dap/src/types.rs | 671 | ||||
-rw-r--r-- | helix-lsp/Cargo.toml | 4 | ||||
-rw-r--r-- | helix-term/Cargo.toml | 3 | ||||
-rw-r--r-- | helix-term/src/application.rs | 144 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 468 | ||||
-rw-r--r-- | helix-term/src/keymap.rs | 11 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 75 | ||||
-rw-r--r-- | helix-view/Cargo.toml | 2 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 9 | ||||
-rw-r--r-- | languages.toml | 78 |
21 files changed, 2402 insertions, 12 deletions
@@ -306,12 +306,14 @@ version = "0.4.1" dependencies = [ "arc-swap", "etcetera", + "helix-dap", "helix-syntax", "once_cell", "quickcheck", "regex", "ropey", "serde", + "serde_json", "similar", "smallvec", "tendril", @@ -323,6 +325,19 @@ dependencies = [ ] [[package]] +name = "helix-dap" +version = "0.4.1" +dependencies = [ + "anyhow", + "fern", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", +] + +[[package]] name = "helix-lsp" version = "0.4.1" dependencies = [ @@ -362,6 +377,7 @@ dependencies = [ "futures-util", "fuzzy-matcher", "helix-core", + "helix-dap", "helix-lsp", "helix-tui", "helix-view", @@ -375,6 +391,7 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "tokio", + "tokio-stream", "toml", ] @@ -403,6 +420,7 @@ dependencies = [ "encoding_rs", "futures-util", "helix-core", + "helix-dap", "helix-lsp", "helix-tui", "log", @@ -410,6 +428,7 @@ dependencies = [ "serde", "slotmap", "tokio", + "tokio-stream", "toml", "url", "which", @@ -6,6 +6,7 @@ members = [ "helix-tui", "helix-syntax", "helix-lsp", + "helix-dap", ] # Build helix-syntax in release mode to make the code path faster in development. diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 8c83816c..8fdfa3ce 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -14,6 +14,7 @@ include = ["src/**/*", "README.md"] [dependencies] helix-syntax = { version = "0.4", path = "../helix-syntax" } +helix-dap = { version = "0.4", path = "../helix-dap" } ropey = "1.3" smallvec = "1.4" @@ -28,6 +29,7 @@ arc-swap = "1" regex = "1" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" toml = "0.5" similar = "1.3" diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index f5f36aca..8dd161d8 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -458,6 +458,8 @@ where unit: String::from(" "), }), indent_query: OnceCell::new(), + debug_adapter: None, + debug_configs: None, }], }); diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 4bceb73b..ae99a159 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -5,6 +5,7 @@ use crate::{ Rope, RopeSlice, Tendril, }; +use helix_dap::DebugAdapterConfig; pub use helix_syntax::get_language; use arc_swap::ArcSwap; @@ -55,6 +56,8 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) indent_query: OnceCell<Option<IndentQuery>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub debugger: Option<DebugAdapterConfig>, } #[derive(Debug, Serialize, Deserialize)] diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml new file mode 100644 index 00000000..60115447 --- /dev/null +++ b/helix-dap/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "helix-dap" +version = "0.4.1" +authors = ["Blaž Hrastnik <blaz@mxxn.io>"] +edition = "2018" +license = "MPL-2.0" +description = "DAP client implementation for Helix project" +categories = ["editor"] +repository = "https://github.com/helix-editor/helix" +homepage = "https://helix-editor.com" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +log = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } + +[dev-dependencies] +fern = "0.6" diff --git a/helix-dap/examples/dap-dlv.rs b/helix-dap/examples/dap-dlv.rs new file mode 100644 index 00000000..eecc4318 --- /dev/null +++ b/helix-dap/examples/dap-dlv.rs @@ -0,0 +1,117 @@ +use helix_dap::{events, Client, Event, Payload, Result, SourceBreakpoint}; +use serde::{Deserialize, Serialize}; +use serde_json::to_value; +use tokio::sync::mpsc::UnboundedReceiver; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct LaunchArguments { + mode: String, + program: String, +} + +async fn dispatch(mut rx: UnboundedReceiver<Payload>) { + loop { + match rx.recv().await.unwrap() { + Payload::Event(Event::Output(events::Output { + category, output, .. + })) => { + println!( + "> [{}] {}", + category.unwrap_or("unknown".to_owned()), + output + ); + } + Payload::Event(Event::Stopped(_)) => { + println!("stopped"); + } + _ => {} + }; + } +} + +#[tokio::main] +pub async fn main() -> Result<()> { + let base_config = fern::Dispatch::new().level(log::LevelFilter::Info); + + let stderr_config = fern::Dispatch::new() + .format(|out, message, record| out.finish(format_args!("[{}] {}", record.level(), message))) + .chain(std::io::stderr()); + + base_config + .chain(stderr_config) + .apply() + .expect("Failed to set up logging"); + + let (mut client, events) = + Client::tcp_process("dlv", vec!["dap"], "-l 127.0.0.1:{}", 0).await?; + println!("create: {:?}", client); + + tokio::spawn(dispatch(events)); + + println!("init: {:?}", client.initialize("go".to_owned()).await); + println!("caps: {:?}", client.capabilities()); + + let args = LaunchArguments { + mode: "exec".to_owned(), + program: "/tmp/godebug/main".to_owned(), + }; + + println!("launch: {:?}", client.launch(to_value(args)?).await); + + println!( + "breakpoints: {:#?}", + client + .set_breakpoints( + "/tmp/godebug/main.go".into(), + vec![SourceBreakpoint { + line: 8, + column: Some(2), + condition: None, + hit_condition: None, + log_message: None, + }] + ) + .await + ); + + let mut _in = String::new(); + std::io::stdin() + .read_line(&mut _in) + .expect("Failed to read line"); + + println!("configurationDone: {:?}", client.configuration_done().await); + + let threads = client.threads().await?; + println!("threads: {:#?}", threads); + let bt = client + .stack_trace(threads[0].id) + .await + .expect("expected stack trace"); + println!("stack trace: {:#?}", bt); + let scopes = client + .scopes(bt.0[0].id) + .await + .expect("expected scopes for thread"); + println!("scopes: {:#?}", scopes); + println!( + "vars: {:#?}", + client.variables(scopes[1].variables_reference).await + ); + + let mut _in = String::new(); + std::io::stdin() + .read_line(&mut _in) + .expect("Failed to read line"); + + println!("continued: {:?}", client.continue_thread(0).await); + + let mut _in = String::new(); + std::io::stdin() + .read_line(&mut _in) + .expect("Failed to read line"); + + println!("disconnect: {:?}", client.disconnect().await); + + Ok(()) +} diff --git a/helix-dap/examples/dap-lldb.rs b/helix-dap/examples/dap-lldb.rs new file mode 100644 index 00000000..2adef8b2 --- /dev/null +++ b/helix-dap/examples/dap-lldb.rs @@ -0,0 +1,116 @@ +use helix_dap::{events, Client, Event, Payload, Result, SourceBreakpoint}; +use serde::{Deserialize, Serialize}; +use serde_json::to_value; +use tokio::sync::mpsc::UnboundedReceiver; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct LaunchArguments { + program: String, + console: String, +} + +async fn dispatch(mut rx: UnboundedReceiver<Payload>) { + loop { + match rx.recv().await.unwrap() { + Payload::Event(Event::Output(events::Output { + category, output, .. + })) => { + println!( + "> [{}] {}", + category.unwrap_or("unknown".to_owned()), + output + ); + } + Payload::Event(Event::Stopped(_)) => { + println!("stopped"); + } + _ => {} + }; + } +} + +#[tokio::main] +pub async fn main() -> Result<()> { + let base_config = fern::Dispatch::new().level(log::LevelFilter::Info); + + let stderr_config = fern::Dispatch::new() + .format(|out, message, record| out.finish(format_args!("[{}] {}", record.level(), message))) + .chain(std::io::stderr()); + + base_config + .chain(stderr_config) + .apply() + .expect("Failed to set up logging"); + + let (mut client, events) = Client::tcp_process("lldb-vscode", vec![], "-p {}", 0).await?; + println!("create: {:?}", client); + + tokio::spawn(dispatch(events)); + + println!("init: {:?}", client.initialize("lldb".to_owned()).await); + println!("caps: {:?}", client.capabilities()); + + let args = LaunchArguments { + program: "/tmp/cdebug/main".to_owned(), + console: "internalConsole".to_owned(), + }; + + println!("launch: {:?}", client.launch(to_value(args)?).await); + + println!( + "breakpoints: {:#?}", + client + .set_breakpoints( + "/tmp/cdebug/main.c".into(), + vec![SourceBreakpoint { + line: 6, + column: Some(2), + condition: None, + hit_condition: None, + log_message: None, + }] + ) + .await + ); + + let mut _in = String::new(); + std::io::stdin() + .read_line(&mut _in) + .expect("Failed to read line"); + + println!("configurationDone: {:?}", client.configuration_done().await); + + let threads = client.threads().await?; + println!("threads: {:#?}", threads); + let bt = client + .stack_trace(threads[0].id) + .await + .expect("expected stack trace"); + println!("stack trace: {:#?}", bt); + let scopes = client + .scopes(bt.0[0].id) + .await + .expect("expected scopes for thread"); + println!("scopes: {:#?}", scopes); + println!( + "vars: {:#?}", + client.variables(scopes[0].variables_reference).await + ); + + let mut _in = String::new(); + std::io::stdin() + .read_line(&mut _in) + .expect("Failed to read line"); + + println!("continued: {:?}", client.continue_thread(0).await); + + let mut _in = String::new(); + std::io::stdin() + .read_line(&mut _in) + .expect("Failed to read line"); + + println!("disconnect: {:?}", client.disconnect().await); + + Ok(()) +} diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs new file mode 100644 index 00000000..ed4f8ed5 --- /dev/null +++ b/helix-dap/src/client.rs @@ -0,0 +1,396 @@ +use crate::{ + transport::{Payload, Request, Transport}, + types::*, + Error, Result, +}; +use anyhow::anyhow; +pub use log::{error, info}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::PathBuf, + process::Stdio, + sync::atomic::{AtomicU64, Ordering}, +}; +use tokio::{ + io::{AsyncBufRead, AsyncWrite, BufReader, BufWriter}, + net::TcpStream, + process::{Child, Command}, + sync::mpsc::{channel, unbounded_channel, UnboundedReceiver, UnboundedSender}, + time, +}; + +#[derive(Debug)] +pub struct Client { + id: usize, + _process: Option<Child>, + server_tx: UnboundedSender<Request>, + request_counter: AtomicU64, + pub caps: Option<DebuggerCapabilities>, + // + pub breakpoints: HashMap<PathBuf, Vec<SourceBreakpoint>>, + // TODO: multiple threads support + pub stack_pointer: Option<StackFrame>, + pub stopped_thread: Option<usize>, + pub is_running: bool, +} + +impl Client { + // Spawn a process and communicate with it by either TCP or stdio + pub async fn process( + cfg: DebugAdapterConfig, + id: usize, + ) -> Result<(Self, UnboundedReceiver<Payload>)> { + if cfg.transport == "tcp" && cfg.port_arg.is_some() { + Self::tcp_process( + &cfg.command, + cfg.args.iter().map(|s| s.as_str()).collect(), + &cfg.port_arg.unwrap(), + id, + ) + .await + } else if cfg.transport == "stdio" { + Self::stdio( + &cfg.command, + cfg.args.iter().map(|s| s.as_str()).collect(), + id, + ) + } else { + Result::Err(Error::Other(anyhow!( + "Incorrect transport {}", + cfg.transport + ))) + } + } + + pub fn streams( + rx: Box<dyn AsyncBufRead + Unpin + Send>, + tx: Box<dyn AsyncWrite + Unpin + Send>, + id: usize, + process: Option<Child>, + ) -> Result<(Self, UnboundedReceiver<Payload>)> { + let (server_rx, server_tx) = Transport::start(rx, tx, id); + let (client_rx, client_tx) = unbounded_channel(); + + let client = Self { + id, + _process: process, + server_tx, + request_counter: AtomicU64::new(0), + caps: None, + // + breakpoints: HashMap::new(), + stack_pointer: None, + stopped_thread: None, + is_running: false, + }; + + tokio::spawn(Self::recv(server_rx, client_rx)); + + Ok((client, client_tx)) + } + + pub async fn tcp( + addr: std::net::SocketAddr, + id: usize, + ) -> Result<(Self, UnboundedReceiver<Payload>)> { + let stream = TcpStream::connect(addr).await?; + let (rx, tx) = stream.into_split(); + Self::streams(Box::new(BufReader::new(rx)), Box::new(tx), id, None) + } + + pub fn stdio( + cmd: &str, + args: Vec<&str>, + id: usize, + ) -> Result<(Self, UnboundedReceiver<Payload>)> { + let process = Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + // make sure the process is reaped on drop + .kill_on_drop(true) + .spawn(); + + let mut process = process?; + + // TODO: do we need bufreader/writer here? or do we use async wrappers on unblock? + let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin")); + let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout")); + + Self::streams( + Box::new(BufReader::new(reader)), + Box::new(writer), + id, + Some(process), + ) + } + + async fn get_port() -> Option<u16> { + Some( + tokio::net::TcpListener::bind(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 0, + )) + .await + .ok()? + .local_addr() + .ok()? + .port(), + ) + } + + pub async fn tcp_process( + cmd: &str, + args: Vec<&str>, + port_format: &str, + id: usize, + ) -> Result<(Self, UnboundedReceiver<Payload>)> { + let port = Self::get_port().await.unwrap(); + + let process = Command::new(cmd) + .args(args) + .args(port_format.replace("{}", &port.to_string()).split(' ')) + // silence messages + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + // Do not kill debug adapter when leaving, it should exit automatically + .spawn()?; + + // 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 (rx, tx) = stream.into_split(); + Self::streams( + Box::new(BufReader::new(rx)), + Box::new(tx), + id, + Some(process), + ) + } + + async fn recv(mut server_rx: UnboundedReceiver<Payload>, client_tx: UnboundedSender<Payload>) { + while let Some(msg) = server_rx.recv().await { + match msg { + Payload::Event(ev) => { + client_tx.send(Payload::Event(ev)).expect("Failed to send"); + } + Payload::Response(_) => unreachable!(), + Payload::Request(req) => { + client_tx + .send(Payload::Request(req)) + .expect("Failed to send"); + } + } + } + } + + pub fn id(&self) -> usize { + self.id + } + + fn next_request_id(&self) -> u64 { + self.request_counter.fetch_add(1, Ordering::Relaxed) + } + + async fn request<R: crate::types::Request>( + &self, + arguments: R::Arguments, + ) -> Result<R::Result> { + let (callback_tx, mut callback_rx) = channel(1); + + let arguments = Some(serde_json::to_value(arguments)?); + + let req = Request { + back_ch: Some(callback_tx), + seq: self.next_request_id(), + command: R::COMMAND.to_string(), + arguments, + }; + + self.server_tx + .send(req) + .expect("Failed to send request to debugger"); + + let response = callback_rx.recv().await.unwrap()?; + let response = serde_json::from_value(response.body.unwrap_or_default())?; + Ok(response) + } + + pub fn capabilities(&self) -> &DebuggerCapabilities { + self.caps.as_ref().expect("debugger not yet initialized!") + } + + pub async fn initialize(&mut self, adapter_id: String) -> Result<()> { + let args = requests::InitializeArguments { + client_id: Some("hx".to_owned()), + client_name: Some("helix".to_owned()), + adapter_id, + locale: Some("en-us".to_owned()), + lines_start_at_one: Some(true), + columns_start_at_one: Some(true), + path_format: Some("path".to_owned()), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(false), + supports_memory_references: Some(false), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + }; + + let response = self.request::<requests::Initialize>(args).await?; + self.caps = Some(response); + + Ok(()) + } + + pub async fn disconnect(&mut self) -> Result<()> { + self.request::<requests::Disconnect>(()).await + } + + pub async fn launch(&mut self, args: serde_json::Value) -> Result<()> { + let response = self.request::<requests::Launch>(args).await?; + log::error!("launch response {}", response); + + Ok(()) + } + + pub async fn attach(&mut self, args: serde_json::Value) -> Result<()> { + let response = self.request::<requests::Attach>(args).await?; + log::error!("attach response {}", response); + + Ok(()) + } + + pub async fn set_breakpoints( + &mut self, + file: PathBuf, + breakpoints: Vec<SourceBreakpoint>, + ) -> Result<Option<Vec<Breakpoint>>> { + let args = requests::SetBreakpointsArguments { + source: Source { + path: Some(file), + name: None, + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }, + breakpoints: Some(breakpoints), + source_modified: Some(false), + }; + + let response = self.request::<requests::SetBreakpoints>(args).await?; + + Ok(response.breakpoints) + } + + pub async fn configuration_done(&mut self) -> Result<()> { + self.request::<requests::ConfigurationDone>(()).await + } + + pub async fn continue_thread(&mut self, thread_id: usize) -> Result<Option<bool>> { + let args = requests::ContinueArguments { thread_id }; + + let response = self.request::<requests::Continue>(args).await?; + Ok(response.all_threads_continued) + } + + pub async fn stack_trace( + &mut self, + thread_id: usize, + ) -> Result<(Vec<StackFrame>, Option<usize>)> { + let args = requests::StackTraceArguments { + thread_id, + start_frame: None, + levels: None, + format: None, + }; + + let response = self.request::<requests::StackTrace>(args).await?; + Ok((response.stack_frames, response.total_frames)) + } + + pub async fn threads(&mut self) -> Result<Vec<Thread>> { + let response = self.request::<requests::Threads>(()).await?; + Ok(response.threads) + } + + pub async fn scopes(&mut self, frame_id: usize) -> Result<Vec<Scope>> { + let args = requests::ScopesArguments { frame_id }; + + let response = self.request::<requests::Scopes>(args).await?; + Ok(response.scopes) + } + + pub async fn variables(&mut self, variables_reference: usize) -> Result<Vec<Variable>> { + let args = requests::VariablesArguments { + variables_reference, + filter: None, + start: None, + count: None, + format: None, + }; + + let response = self.request::<requests::Variables>(args).await?; + Ok(response.variables) + } + + pub async fn step_in(&mut self, thread_id: usize) -> Result<()> { + let args = requests::StepInArguments { + thread_id, + target_id: None, + granularity: None, + }; + + self.request::<requests::StepIn>(args).await + } + + pub async fn step_out(&mut self, thread_id: usize) -> Result<()> { + let args = requests::StepOutArguments { + thread_id, + granularity: None, + }; + + self.request::<requests::StepOut>(args).await + } + + pub async fn next(&mut self, thread_id: usize) -> Result<()> { + let args = requests::NextArguments { + thread_id, + granularity: None, + }; + + self.request::<requests::Next>(args).await + } + + pub async fn pause(&mut self, thread_id: usize) -> Result<()> { + let args = requests::PauseArguments { thread_id }; + + self.request::<requests::Pause>(args).await + } + + pub async fn eval( + &mut self, + expression: String, + frame_id: Option<usize>, + ) -> Result<requests::EvaluateResponse> { + let args = requests::EvaluateArguments { + expression, + frame_id, + context: None, + format: None, + }; + + self.request::<requests::Evaluate>(args).await + } +} diff --git a/helix-dap/src/lib.rs b/helix-dap/src/lib.rs new file mode 100644 index 00000000..f60b102c --- /dev/null +++ b/helix-dap/src/lib.rs @@ -0,0 +1,24 @@ +mod client; +mod transport; +mod types; + +pub use client::Client; +pub use events::Event; +pub use transport::{Payload, Response, Transport}; +pub use types::*; + +use thiserror::Error; +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to parse: {0}")] + Parse(#[from] serde_json::Error), + #[error("IO Error: {0}")] + IO(#[from] std::io::Error), + #[error("request timed out")] + Timeout, + #[error("server closed the stream")] + StreamClosed, + #[error(transparent)] + Other(#[from] anyhow::Error), +} +pub type Result<T> = core::result::Result<T, Error>; diff --git a/helix-dap/src/transport.rs b/helix-dap/src/transport.rs new file mode 100644 index 00000000..afb7694d --- /dev/null +++ b/helix-dap/src/transport.rs @@ -0,0 +1,246 @@ +use crate::{Error, Event, 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 tokio::{ + io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + sync::{ + mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, + Mutex, + }, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Request { + #[serde(skip)] + pub back_ch: Option<Sender<Result<Response>>>, + pub seq: u64, + pub command: String, + pub arguments: Option<Value>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct Response { + // seq is omitted as unused and is not sent by some implementations + pub request_seq: u64, + pub success: bool, + pub command: String, + pub message: Option<String>, + pub body: Option<Value>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Payload { + // type = "event" + Event(Event), + // type = "response" + Response(Response), + // type = "request" + Request(Request), +} + +#[derive(Debug)] +pub struct Transport { + id: usize, + pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>, +} + +impl Transport { + pub fn start( + server_stdout: Box<dyn AsyncBufRead + Unpin + Send>, + server_stdin: Box<dyn AsyncWrite + Unpin + Send>, + id: usize, + ) -> (UnboundedReceiver<Payload>, UnboundedSender<Request>) { + let (client_tx, rx) = unbounded_channel(); + let (tx, client_rx) = unbounded_channel(); + + let transport = Self { + id, + pending_requests: Mutex::new(HashMap::default()), + }; + + let transport = Arc::new(transport); + + tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); + tokio::spawn(Self::send(transport, server_stdin, client_rx)); + + (rx, tx) + } + + async fn recv_server_message( + reader: &mut Box<dyn AsyncBufRead + Unpin + Send>, + buffer: &mut String, + ) -> Result<Payload> { + let mut content_length = None; + loop { + buffer.truncate(0); + reader.read_line(buffer).await?; + let header = buffer.trim(); + + if header.is_empty() { + break; + } + + let mut parts = header.split(": "); + + match (parts.next(), parts.next(), parts.next()) { + (Some("Content-Length"), Some(value), None) => { + content_length = Some(value.parse().context("invalid content length")?); + } + (Some(_), Some(_), None) => {} + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse header", + ) + .into()); + } + } + } + + let content_length = content_length.context("missing content length")?; + + //TODO: reuse vector + let mut content = vec![0; content_length]; + reader.read_exact(&mut content).await?; + let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; + + info!("<- DAP {}", msg); + + // try parsing as output (server response) or call (server request) + let output: serde_json::Result<Payload> = serde_json::from_str(msg); + + Ok(output?) + } + + async fn send_payload_to_server( + &self, + server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>, + mut req: Request, + ) -> Result<()> { + let back_ch = req.back_ch.take(); + let seq = req.seq; + let json = serde_json::to_string(&Payload::Request(req))?; + if let Some(back) = back_ch { + self.pending_requests.lock().await.insert(seq, back); + } + self.send_string_to_server(server_stdin, json).await + } + + async fn send_string_to_server( + &self, + server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>, + request: String, + ) -> Result<()> { + info!("-> DAP {}", request); + + // send the headers + server_stdin + .write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes()) + .await?; + + // send the body + server_stdin.write_all(request.as_bytes()).await?; + + server_stdin.flush().await?; + + Ok(()) + } + + fn process_response(res: Response) -> Result<Response> { + if res.success { + info!("<- DAP success in response to {}", res.request_seq); + + Ok(res) + } else { + error!( + "<- DAP error {:?} ({:?}) for command #{} {}", + res.message, res.body, res.request_seq, res.command + ); + + Err(Error::Other(anyhow::format_err!("{:?}", res.body))) + } + } + + async fn process_server_message( + &self, + client_tx: &UnboundedSender<Payload>, + msg: Payload, + ) -> Result<()> { + match msg { + Payload::Response(res) => { + let request_seq = res.request_seq; + let tx = self.pending_requests.lock().await.remove(&request_seq); + + match tx { + 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", + request_seq + ), + } + None => { + warn!("Response to nonexistent request #{}", res.request_seq); + client_tx.send(Payload::Response(res)).expect("Failed to send"); + } + } + + Ok(()) + } + Payload::Request(Request { + ref command, + ref seq, + .. + }) => { + info!("<- DAP request {} #{}", command, seq); + client_tx.send(msg).expect("Failed to send"); + Ok(()) + } + Payload::Event(ref event) => { + info!("<- DAP event {:?}", event); + client_tx.send(msg).expect("Failed to send"); + Ok(()) + } + } + } + + async fn recv( + transport: Arc<Self>, + mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>, + client_tx: UnboundedSender<Payload>, + ) { + let mut recv_buffer = String::new(); + loop { + match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await { + Ok(msg) => { + transport + .process_server_message(&client_tx, msg) + .await + .unwrap(); + } + Err(err) => { + error!("err: <- {:?}", err); + break; + } + } + } + } + + async fn send( + transport: Arc<Self>, + mut server_stdin: Box<dyn AsyncWrite + Unpin + Send>, + mut client_rx: UnboundedReceiver<Request>, + ) { + while let Some(req) = client_rx.recv().await { + transport + .send_payload_to_server(&mut server_stdin, req) + .await + .unwrap() + } + } +} diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs new file mode 100644 index 00000000..03f22e4d --- /dev/null +++ b/helix-dap/src/types.rs @@ -0,0 +1,671 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{collections::HashMap, path::PathBuf}; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DebugTemplate { + pub name: String, + pub request: String, + pub args: HashMap<String, String>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DebugAdapterConfig { + pub name: String, + pub transport: String, + pub command: String, + pub args: Vec<String>, + pub port_arg: Option<String>, + pub templates: Vec<DebugTemplate>, +} + +pub trait Request { + type Arguments: serde::de::DeserializeOwned + serde::Serialize; + type Result: serde::de::DeserializeOwned + serde::Serialize; + const COMMAND: &'static str; +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ColumnDescriptor { + pub attribute_name: String, + pub label: String, + pub format: Option<String>, + #[serde(rename = "type")] + pub col_type: Option<String>, + pub width: Option<usize>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExceptionBreakpointsFilter { + pub filter: String, + pub label: String, + pub description: Option<String>, + pub default: Option<bool>, + pub supports_condition: Option<bool>, + pub condition_description: Option<String>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DebuggerCapabilities { + pub supports_configuration_done_request: Option<bool>, + pub supports_function_breakpoints: Option<bool>, + pub supports_conditional_breakpoints: Option<bool>, + pub supports_hit_conditional_breakpoints: Option<bool>, + pub supports_evaluate_for_hovers: Option<bool>, + pub supports_step_back: Option<bool>, + pub supports_set_variable: Option<bool>, + pub supports_restart_frame: Option<bool>, + pub supports_goto_targets_request: Option<bool>, + pub supports_step_in_targets_request: Option<bool>, + pub supports_completions_request: Option<bool>, + pub supports_modules_request: Option<bool>, + pub supports_restart_request: Option<bool>, + pub supports_exception_options: Option<bool>, + pub supports_value_formatting_options: Option<bool>, + pub supports_exception_info_request: Option<bool>, + pub support_terminate_debuggee: Option<bool>, + pub support_suspend_debuggee: Option<bool>, + pub supports_delayed_stack_trace_loading: Option<bool>, + pub supports_loaded_sources_request: Option<bool>, + pub supports_log_points: Option<bool>, + pub supports_terminate_threads_request: Option<bool>, + pub supports_set_expression: Option<bool>, + pub supports_terminate_request: Option<bool>, + pub supports_data_breakpoints: Option<bool>, + pub supports_read_memory_request: Option<bool>, + pub supports_write_memory_request: Option<bool>, + pub supports_disassemble_request: Option<bool>, + pub supports_cancel_request: Option<bool>, + pub supports_breakpoint_locations_request: Option<bool>, + pub supports_clipboard_context: Option<bool>, + pub supports_stepping_granularity: Option<bool>, + pub supports_instruction_breakpoints: Option<bool>, + pub supports_exception_filter_options: Option<bool>, + pub exception_breakpoint_filters: Option<Vec<ExceptionBreakpointsFilter>>, + pub completion_trigger_characters: Option<Vec<String>>, + pub additional_module_columns: Option<Vec<ColumnDescriptor>>, + pub supported_checksum_algorithms: Option<Vec<String>>, +} + +impl std::ops::Deref for DebuggerCapabilities { + type Target = Option<bool>; + + fn deref(&self) -> &Self::Target { + &self.supports_exception_options + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Checksum { + pub algorithm: String, + pub checksum: String, +} + +#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Source { + pub name: Option<String>, + pub path: Option<PathBuf>, + pub source_reference: Option<usize>, + pub presentation_hint: Option<String>, + pub origin: Option<String>, + pub sources: Option<Vec<Source>>, + pub adapter_data: Option<Value>, + pub checksums: Option<Vec<Checksum>>, +} + +#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceBreakpoint { + pub line: usize, + pub column: Option<usize>, + pub condition: Option<String>, + pub hit_condition: Option<String>, + pub log_message: Option<String>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Breakpoint { + pub id: Option<usize>, + pub verified: bool, + pub message: Option<String>, + pub source: Option<Source>, + pub line: Option<usize>, + pub column: Option<usize>, + pub end_line: Option<usize>, + pub end_column: Option<usize>, + pub instruction_reference: Option<String>, + pub offset: Option<usize>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StackFrameFormat { + pub parameters: Option<bool>, + pub parameter_types: Option<bool>, + pub parameter_names: Option<bool>, + pub parameter_values: Option<bool>, + pub line: Option<bool>, + pub module: Option<bool>, + pub include_all: Option<bool>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StackFrame { + pub id: usize, + pub name: String, + pub source: Option<Source>, + pub line: usize, + pub column: usize, + pub end_line: Option<usize>, + pub end_column: Option<usize>, + pub can_restart: Option<bool>, + pub instruction_pointer_reference: Option<String>, + pub module_id: Option<Value>, + pub presentation_hint: Option<String>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Thread { + pub id: usize, + pub name: String, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Scope { + pub name: String, + pub presentation_hint: Option<String>, + pub variables_reference: usize, + pub named_variables: Option<usize>, + pub indexed_variables: Option<usize>, + pub expensive: bool, + pub source: Option<Source>, + pub line: Option<usize>, + pub column: Option<usize>, + pub end_line: Option<usize>, + pub end_column: Option<usize>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValueFormat { + pub hex: Option<bool>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VariablePresentationHint { + pub kind: Option<String>, + pub attributes: Option<Vec<String>>, + pub visibility: Option<String>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Variable { + pub name: String, + pub value: String, + #[serde(rename = "type")] + pub data_type: Option<String>, + pub presentation_hint: Option<VariablePresentationHint>, + pub evaluate_name: Option<String>, + pub variables_reference: usize, + pub named_variables: Option<usize>, + pub indexed_variables: Option<usize>, + pub memory_reference: Option<String>, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Module { + pub id: String, // TODO: || number + pub name: String, + pub path: Option<PathBuf>, + pub is_optimized: Option<bool>, + pub is_user_code: Option<bool>, + pub version: Option<String>, + pub symbol_status: Option<String>, + pub symbol_file_path: Option<String>, + pub date_time_stamp: Option<String>, + pub address_range: Option<String>, +} + +pub mod requests { + use super::*; + #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct InitializeArguments { + #[serde(rename = "clientID")] + pub client_id: Option<String>, + pub client_name: Option<String>, + #[serde(rename = "adapterID")] + pub adapter_id: String, + pub locale: Option<String>, + #[serde(rename = "linesStartAt1")] + pub lines_start_at_one: Option<bool>, + #[serde(rename = "columnsStartAt1")] + pub columns_start_at_one: Option<bool>, + pub path_format: Option<String>, + pub supports_variable_type: Option<bool>, + pub supports_variable_paging: Option<bool>, + pub supports_run_in_terminal_request: Option<bool>, + pub supports_memory_references: Option<bool>, + pub supports_progress_reporting: Option<bool>, + pub supports_invalidated_event: Option<bool>, + } + + #[derive(Debug)] + pub enum Initialize {} + + impl Request for Initialize { + type Arguments = InitializeArguments; + type Result = DebuggerCapabilities; + const COMMAND: &'static str = "initialize"; + } + + #[derive(Debug)] + pub enum Launch {} + + impl Request for Launch { + type Arguments = Value; + type Result = Value; + const COMMAND: &'static str = "launch"; + } + + #[derive(Debug)] + pub enum Attach {} + + impl Request for Attach { + type Arguments = Value; + type Result = Value; + const COMMAND: &'static str = "attach"; + } + + #[derive(Debug)] + pub enum Disconnect {} + + impl Request for Disconnect { + type Arguments = (); + type Result = (); + const COMMAND: &'static str = "disconnect"; + } + + #[derive(Debug)] + pub enum ConfigurationDone {} + + impl Request for ConfigurationDone { + type Arguments = (); + type Result = (); + const COMMAND: &'static str = "configurationDone"; + } + + #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct SetBreakpointsArguments { + pub source: Source, + pub breakpoints: Option<Vec<SourceBreakpoint>>, + // lines is deprecated + pub source_modified: Option<bool>, + } + + #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct SetBreakpointsResponse { + pub breakpoints: Option<Vec<Breakpoint>>, + } + + #[derive(Debug)] + pub enum SetBreakpoints {} + + impl Request for SetBreakpoints { + type Arguments = SetBreakpointsArguments; + type Result = SetBreakpointsResponse; + const COMMAND: &'static str = "setBreakpoints"; + } + + #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ContinueArguments { + pub thread_id: usize, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ContinueResponse { + pub all_threads_continued: Option<bool>, + } + + #[derive(Debug)] + pub enum Continue {} + + impl Request for Continue { + type Arguments = ContinueArguments; + type Result = ContinueResponse; + const COMMAND: &'static str = "continue"; + } + + #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct StackTraceArguments { + pub thread_id: usize, + pub start_frame: Option<usize>, + pub levels: Option<usize>, + pub format: Option<StackFrameFormat>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct StackTraceResponse { + pub total_frames: Option<usize>, + pub stack_frames: Vec<StackFrame>, + } + + #[derive(Debug)] + pub enum StackTrace {} + + impl Request for StackTrace { + type Arguments = StackTraceArguments; + type Result = StackTraceResponse; + const COMMAND: &'static str = "stackTrace"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ThreadsResponse { + pub threads: Vec<Thread>, + } + + #[derive(Debug)] + pub enum Threads {} + + impl Request for Threads { + type Arguments = (); + type Result = ThreadsResponse; + const COMMAND: &'static str = "threads"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ScopesArguments { + pub frame_id: usize, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ScopesResponse { + pub scopes: Vec<Scope>, + } + + #[derive(Debug)] + pub enum Scopes {} + + impl Request for Scopes { + type Arguments = ScopesArguments; + type Result = ScopesResponse; + const COMMAND: &'static str = "scopes"; + } + + #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct VariablesArguments { + pub variables_reference: usize, + pub filter: Option<String>, + pub start: Option<usize>, + pub count: Option<usize>, + pub format: Option<ValueFormat>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct VariablesResponse { + pub variables: Vec<Variable>, + } + + #[derive(Debug)] + pub enum Variables {} + + impl Request for Variables { + type Arguments = VariablesArguments; + type Result = VariablesResponse; + const COMMAND: &'static str = "variables"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct StepInArguments { + pub thread_id: usize, + pub target_id: Option<usize>, + pub granularity: Option<String>, + } + + #[derive(Debug)] + pub enum StepIn {} + + impl Request for StepIn { + type Arguments = StepInArguments; + type Result = (); + const COMMAND: &'static str = "stepIn"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct StepOutArguments { + pub thread_id: usize, + pub granularity: Option<String>, + } + + #[derive(Debug)] + pub enum StepOut {} + + impl Request for StepOut { + type Arguments = StepOutArguments; + type Result = (); + const COMMAND: &'static str = "stepOut"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct NextArguments { + pub thread_id: usize, + pub granularity: Option<String>, + } + + #[derive(Debug)] + pub enum Next {} + + impl Request for Next { + type Arguments = NextArguments; + type Result = (); + const COMMAND: &'static str = "next"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct PauseArguments { + pub thread_id: usize, + } + + #[derive(Debug)] + pub enum Pause {} + + impl Request for Pause { + type Arguments = PauseArguments; + type Result = (); + const COMMAND: &'static str = "pause"; + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct EvaluateArguments { + pub expression: String, + pub frame_id: Option<usize>, + pub context: Option<String>, + pub format: Option<ValueFormat>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct EvaluateResponse { + pub result: String, + #[serde(rename = "type")] + pub data_type: Option<String>, + pub presentation_hint: Option<VariablePresentationHint>, + pub variables_reference: usize, + pub named_variables: Option<usize>, + pub indexed_variables: Option<usize>, + pub memory_reference: Option<String>, + } + + #[derive(Debug)] + pub enum Evaluate {} + + impl Request for Evaluate { + type Arguments = EvaluateArguments; + type Result = EvaluateResponse; + const COMMAND: &'static str = "evaluate"; + } +} + +// Events + +pub mod events { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + #[serde(tag = "event", content = "body")] + // seq is omitted as unused and is not sent by some implementations + pub enum Event { + Initialized, + Stopped(Stopped), + Continued(Continued), + Exited(Exited), + Terminated(Option<Terminated>), + Thread(Thread), + Output(Output), + Breakpoint(Breakpoint), + Module(Module), + LoadedSource(LoadedSource), + Process(Process), + Capabilities(Capabilities), + // ProgressStart(), + // ProgressUpdate(), + // ProgressEnd(), + // Invalidated(), + Memory(Memory), + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Stopped { + pub reason: String, + pub description: Option<String>, + pub thread_id: Option<usize>, + pub preserve_focus_hint: Option<bool>, + pub text: Option<String>, + pub all_threads_stopped: Option<bool>, + pub hit_breakpoint_ids: Option<Vec<usize>>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Continued { + pub thread_id: usize, + pub all_threads_continued: Option<bool>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Exited { + pub exit_code: usize, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Terminated { + pub restart: Option<Value>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Thread { + pub reason: String, + pub thread_id: usize, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Output { + pub output: String, + pub category: Option<String>, + pub group: Option<String>, + pub line: Option<usize>, + pub column: Option<usize>, + pub variables_reference: Option<usize>, + pub source: Option<Source>, + pub data: Option<Value>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Breakpoint { + pub reason: String, + pub breakpoint: super::Breakpoint, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Module { + pub reason: String, + pub module: super::Module, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct LoadedSource { + pub reason: String, + pub source: super::Source, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Process { + pub name: String, + pub system_process_id: Option<usize>, + pub is_local_process: Option<bool>, + pub start_method: Option<String>, // TODO: use enum + pub pointer_size: Option<usize>, + } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Capabilities { + pub capabilities: super::DebuggerCapabilities, + } + + // #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + // #[serde(rename_all = "camelCase")] + // pub struct Invalidated { + // pub areas: Vec<InvalidatedArea>, + // pub thread_id: Option<usize>, + // pub stack_frame_id: Option<usize>, + // } + + #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Memory { + pub memory_reference: String, + pub offset: usize, + pub count: usize, + } +} diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 2d4a16c6..63f27cf8 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.9", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } -tokio-stream = "0.1.7" +tokio = { version = "1.9", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio-stream = "0.1" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 57d592cc..6e9c0daf 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -24,6 +24,7 @@ path = "src/main.rs" helix-core = { version = "0.4", path = "../helix-core" } helix-view = { version = "0.4", path = "../helix-view" } helix-lsp = { version = "0.4", path = "../helix-lsp" } +helix-dap = { version = "0.4", path = "../helix-dap" } anyhow = "1" once_cell = "1.8" @@ -33,7 +34,7 @@ num_cpus = "1" tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } crossterm = { version = "0.21", features = ["event-stream"] } signal-hook = "0.3" - +tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } # Logging diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3d59c33a..17c762da 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,11 +1,18 @@ -use helix_core::syntax; +use helix_core::{syntax, Range, Selection}; +use helix_dap::Payload; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{theme, Editor}; -use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; +use crate::{ + args::Args, + commands::{align_view, Align}, + compositor::Compositor, + config::Config, + job::Jobs, + ui, +}; use log::error; - use std::{ io::{stdout, Write}, sync::Arc, @@ -184,6 +191,9 @@ impl Application { last_render = Instant::now(); } } + Some(payload) = self.editor.debugger_events.next() => { + self.handle_debugger_message(payload).await; + } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); @@ -245,6 +255,134 @@ impl Application { } } + pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) { + use helix_dap::{events, Event}; + let mut debugger = match self.editor.debugger.as_mut() { + Some(debugger) => debugger, + None => return, + }; + + match payload { + Payload::Event(ev) => match ev { + Event::Stopped(events::Stopped { + thread_id, + description, + text, + reason, + all_threads_stopped, + .. + }) => { + debugger.is_running = false; + let main = debugger.threads().await.ok().and_then(|threads| { + // Workaround for debugging Go tests. Main thread has * in beginning of its name + let mut main = threads.iter().find(|t| t.name.starts_with('*')).cloned(); + if main.is_none() { + main = threads.get(0).cloned(); + } + main + }); + + if let Some(main) = main { + let (bt, _) = debugger.stack_trace(main.id).await.unwrap(); + debugger.stack_pointer = bt.get(0).cloned(); + debugger.stopped_thread = Some(main.id); + } + + let scope = match thread_id { + Some(id) => format!("Thread {}", id), + None => "Target".to_owned(), + }; + + let mut status = format!("{} stopped because of {}", scope, reason); + if let Some(desc) = description { + status.push_str(&format!(" {}", desc)); + } + if let Some(text) = text { + status.push_str(&format!(" {}", text)); + } + if all_threads_stopped.unwrap_or_default() { + status.push_str(" (all threads stopped)"); + } + + if let Some(helix_dap::StackFrame { + source: + Some(helix_dap::Source { + path: Some(ref src), + .. + }), + line, + column, + end_line, + end_column, + .. + }) = debugger.stack_pointer + { + let path = src.clone(); + self.editor + .open(path, helix_view::editor::Action::Replace) + .unwrap(); + + let (view, doc) = current!(self.editor); + + let text_end = doc.text().len_chars().saturating_sub(1); + let start = doc.text().try_line_to_char(line - 1).unwrap_or(0) + column; + if let Some(end_line) = end_line { + let end = doc.text().try_line_to_char(end_line - 1).unwrap_or(0) + + end_column.unwrap_or(0); + doc.set_selection( + view.id, + Selection::new( + helix_core::SmallVec::from_vec(vec![Range::new( + start.min(text_end), + end.min(text_end), + )]), + 0, + ), + ); + } else { + doc.set_selection(view.id, Selection::point(start.min(text_end))); + } + align_view(doc, view, Align::Center); + } + self.editor.set_status(status); + } + Event::Output(events::Output { + category, output, .. + }) => { + let prefix = match category { + Some(category) => { + if &category == "telemetry" { + return; + } + format!("Debug ({}):", category) + } + None => "Debug:".to_owned(), + }; + + self.editor.set_status(format!("{} {}", prefix, output)); + } + Event::Initialized => { + self.editor + .set_status("Debugged application started".to_owned()); + } + Event::Continued(_) => { + if let Some(debugger) = self.editor.debugger.as_mut() { + debugger.stopped_thread = None; + debugger.stack_pointer = None; + debugger.is_running = true; + } + } + ev => { + log::warn!("Unhandled event {:?}", ev); + return; // return early to skip render + } + }, + Payload::Response(_) => unreachable!(), + Payload::Request(_) => todo!(), + } + self.render(); + } + pub async fn handle_language_server_message( &mut self, call: helix_lsp::Call, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9a7b6510..6ba244ee 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -24,16 +24,18 @@ use helix_lsp::{ }; use insert::*; use movement::Movement; +use serde_json::Value; use crate::{ compositor::{self, Component, Compositor}, ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; +use tokio_stream::wrappers::UnboundedReceiverStream; use crate::job::{self, Job, Jobs}; use futures_util::FutureExt; use std::num::NonZeroUsize; -use std::{fmt, future::Future}; +use std::{collections::HashMap, fmt, future::Future}; use std::{ borrow::Cow, @@ -97,13 +99,13 @@ impl<'a> Context<'a> { } } -enum Align { +pub enum Align { Top, Center, Bottom, } -fn align_view(doc: &Document, view: &mut View, align: Align) { +pub fn align_view(doc: &Document, view: &mut View, align: Align) { let pos = doc .selection(view.id) .primary() @@ -302,6 +304,15 @@ impl Command { surround_delete, "Surround delete", select_textobject_around, "Select around object", select_textobject_inner, "Select inside object", + dap_toggle_breakpoint, "Toggle breakpoint", + dap_run, "Begin program execution", + dap_continue, "Continue program execution", + dap_pause, "Pause program execution", + dap_in, "Step in", + dap_out, "Step out", + dap_next, "Step to next", + dap_variables, "List variables", + dap_terminate, "End debug session", suspend, "Suspend" ); } @@ -1336,7 +1347,6 @@ fn append_mode(cx: &mut Context) { mod cmd { use super::*; - use std::collections::HashMap; use helix_view::editor::Action; use ui::completers::{self, Completer}; @@ -1900,6 +1910,102 @@ mod cmd { Ok(()) } + fn debug_eval( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + use helix_lsp::block_on; + if let Some(debugger) = cx.editor.debugger.as_mut() { + let id = debugger.stack_pointer.clone().map(|x| x.id); + let response = block_on(debugger.eval(args.join(" "), id))?; + cx.editor.set_status(response.result); + } + Ok(()) + } + + fn edit_breakpoint_impl( + cx: &mut compositor::Context, + condition: Option<String>, + log_message: Option<String>, + ) { + use helix_lsp::block_on; + + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let pos = doc.selection(view.id).primary().cursor(text); + let breakpoint = helix_dap::SourceBreakpoint { + line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init) + condition, + log_message, + ..Default::default() + }; + let path = match doc.path() { + Some(path) => path.to_path_buf(), + None => { + cx.editor + .set_error("Can't edit breakpoint: document has no path".to_string()); + return; + } + }; + if let Some(debugger) = &mut cx.editor.debugger { + if breakpoint.condition.is_some() + && !debugger + .caps + .clone() + .unwrap() + .supports_conditional_breakpoints + .unwrap_or_default() + { + cx.editor.set_error( + "Can't edit breakpoint: debugger does not support conditional breakpoints" + .to_string(), + ); + return; + } + if breakpoint.log_message.is_some() + && !debugger + .caps + .clone() + .unwrap() + .supports_log_points + .unwrap_or_default() + { + cx.editor.set_error( + "Can't edit breakpoint: debugger does not support logpoints".to_string(), + ); + return; + } + + let breakpoints = debugger.breakpoints.entry(path.clone()).or_default(); + if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) { + breakpoints.remove(pos); + breakpoints.push(breakpoint); + + let breakpoints = breakpoints.clone(); + + let request = debugger.set_breakpoints(path, breakpoints); + let _ = block_on(request).unwrap(); + } + } + } + + fn debug_breakpoint_condition( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let condition = args.join(" "); + let condition = if condition.is_empty() { + None + } else { + Some(condition) + }; + + edit_breakpoint_impl(cx, condition, None); + Ok(()) + } + fn vsplit( cx: &mut compositor::Context, args: &[&str], @@ -1922,6 +2028,55 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { + let log_message = args.join(" "); + let log_message = if log_message.is_empty() { + None + } else { + Some(log_message) + }; + + edit_breakpoint_impl(cx, None, log_message); + Ok(()) + } + + fn debug_start( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let mut args = args.to_owned(); + let name = match args.len() { + 0 => None, + _ => Some(args.remove(0)), + }; + dap_start_impl(&mut cx.editor, name, None, Some(args)); + Ok(()) + } + + fn debug_remote( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let mut args = args.to_owned(); + let address = match args.len() { + 0 => None, + _ => Some(args.remove(0).parse().unwrap()), + }; + let name = match args.len() { + 0 => None, + _ => Some(args.remove(0)), + }; + dap_start_impl(&mut cx.editor, name, address, Some(args)); + + Ok(()) + } + + fn debug_set_logpoint( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); let id = doc.id(); @@ -2174,6 +2329,41 @@ mod cmd { completer: None, }, TypableCommand { + name: "debug-start", + alias: Some("dbg"), + doc: "Start a debug session from a given template with given parameters.", + fun: debug_start, + completer: Some(completers::filename), + }, + TypableCommand { + name: "debug-remote", + alias: Some("dbg-tcp"), + doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.", + fun: debug_remote, + completer: Some(completers::filename), + }, + TypableCommand { + name: "debug-eval", + alias: None, + doc: "Evaluate expression in current debug context.", + fun: debug_eval, + completer: None, + }, + TypableCommand { + name: "debug-breakpoint-condition", + alias: None, + doc: "Set current breakpoint condition.", + fun: debug_breakpoint_condition, + completer: None, + }, + TypableCommand { + name: "debug-set-logpoint", + alias: None, + doc: "Make current breakpoint a log point.", + fun: debug_set_logpoint, + completer: None, + }, + TypableCommand { name: "vsplit", alias: Some("vsp"), doc: "Open the file in a vertical split.", @@ -4296,3 +4486,273 @@ fn suspend(_cx: &mut Context) { #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); } + +// DAP +fn dap_start_impl( + editor: &mut Editor, + name: Option<&str>, + socket: Option<std::net::SocketAddr>, + params: Option<Vec<&str>>, +) { + use helix_dap::Client; + use helix_lsp::block_on; + use serde_json::to_value; + + let (_, doc) = current!(editor); + + let path = match doc.path() { + Some(path) => path.to_path_buf(), + None => { + editor.set_error("Can't start debug: document has no path".to_string()); + return; + } + }; + + let config = editor + .syn_loader + .language_config_for_file_name(&path) + .and_then(|x| x.debugger.clone()); + let config = match config { + Some(c) => c, + None => { + editor.set_error( + "Can't start debug: no debug adapter available for language".to_string(), + ); + return; + } + }; + + let (mut debugger, events) = match socket { + Some(socket) => block_on(Client::tcp(socket, 0)).unwrap(), + None => block_on(Client::process(config.clone(), 0)).unwrap(), + }; + + let request = debugger.initialize(config.name.clone()); + let _ = block_on(request).unwrap(); + + let start_config = match name { + Some(name) => config.templates.iter().find(|t| t.name == name), + None => config.templates.get(0), + }; + let start_config = match start_config { + Some(c) => c, + None => { + editor.set_error("Can't start debug: no debug config with given name".to_string()); + return; + } + }; + + let template = start_config.args.clone(); + let mut args: HashMap<String, Value> = HashMap::new(); + + if let Some(params) = params { + for (k, t) in template { + let mut value = t; + for (i, x) in params.iter().enumerate() { + // For param #0 replace {0} in args + value = value.replace(format!("{{{}}}", i).as_str(), x); + } + + if let Ok(integer) = value.parse::<usize>() { + args.insert(k, Value::Number(serde_json::Number::from(integer))); + } else { + args.insert(k, Value::String(value)); + } + } + } + + let args = to_value(args).unwrap(); + + // TODO gracefully handle errors from debugger + match &start_config.request[..] { + "launch" => block_on(debugger.launch(args)).unwrap(), + "attach" => block_on(debugger.attach(args)).unwrap(), + _ => { + editor.set_error("Unsupported request".to_string()); + return; + } + }; + + // TODO: either await "initialized" or buffer commands until event is received + editor.debugger = Some(debugger); + let stream = UnboundedReceiverStream::new(events); + editor.debugger_events.push(stream); +} + +fn dap_toggle_breakpoint(cx: &mut Context) { + use helix_lsp::block_on; + + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let pos = doc.selection(view.id).primary().cursor(text); + + let breakpoint = helix_dap::SourceBreakpoint { + line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init) + ..Default::default() + }; + + let path = match doc.path() { + Some(path) => path.to_path_buf(), + None => { + cx.editor + .set_error("Can't set breakpoint: document has no path".to_string()); + return; + } + }; + + // TODO: need to map breakpoints over edits and update them? + // we shouldn't really allow editing while debug is running though + + if let Some(debugger) = &mut cx.editor.debugger { + let breakpoints = debugger.breakpoints.entry(path.clone()).or_default(); + if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) { + breakpoints.remove(pos); + } else { + breakpoints.push(breakpoint); + } + + let breakpoints = breakpoints.clone(); + + let request = debugger.set_breakpoints(path, breakpoints); + let _ = block_on(request).unwrap(); + } +} + +fn dap_run(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + let request = debugger.configuration_done(); + let _ = block_on(request).unwrap(); + debugger.is_running = true; + } +} + +fn dap_continue(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.continue_thread(debugger.stopped_thread.unwrap()); + let _ = block_on(request).unwrap(); + debugger.is_running = true; + debugger.stack_pointer = None; + } +} + +fn dap_pause(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if !debugger.is_running { + cx.editor.set_status("Debuggee is not running".to_owned()); + return; + } + + // FIXME: correct number here + let request = debugger.pause(0); + let _ = block_on(request).unwrap(); + } +} + +fn dap_in(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.step_in(debugger.stopped_thread.unwrap()); + let _ = block_on(request).unwrap(); + } +} + +fn dap_out(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.step_out(debugger.stopped_thread.unwrap()); + let _ = block_on(request).unwrap(); + } +} + +fn dap_next(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.next(debugger.stopped_thread.unwrap()); + let _ = block_on(request).unwrap(); + } +} + +fn dap_variables(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Cannot access variables while target is running".to_owned()); + return; + } + if debugger.stack_pointer.is_none() { + cx.editor + .set_status("Cannot find current stack pointer to access variables".to_owned()); + return; + } + + let frame_id = debugger.stack_pointer.clone().unwrap().id; + let scopes = block_on(debugger.scopes(frame_id)).unwrap(); + let mut s = String::new(); + + for scope in scopes.iter() { + let response = block_on(debugger.variables(scope.variables_reference)); + + if let Ok(vars) = response { + for var in vars { + let prefix = match var.data_type { + Some(data_type) => format!("{} ", data_type), + None => "".to_owned(), + }; + // s.push_str(&format!("{}{} = {}; ", prefix, var.name, var.value)); + s.push_str(&format!("{}{}; ", prefix, var.name,)); + } + } + } + cx.editor.set_status(s); + } +} + +fn dap_terminate(cx: &mut Context) { + use helix_lsp::block_on; + + if let Some(debugger) = &mut cx.editor.debugger { + let request = debugger.disconnect(); + let _ = block_on(request).unwrap(); + cx.editor.debugger = None; + } +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 57bcb321..02b5f25c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -485,6 +485,17 @@ impl Default for Keymaps { "s" => symbol_picker, "a" => code_action, "'" => last_picker, + "d" => { "Debug" + "b" => dap_toggle_breakpoint, + "r" => dap_run, + "c" => dap_continue, + "h" => dap_pause, + "j" => dap_in, + "k" => dap_out, + "l" => dap_next, + "v" => dap_variables, + "t" => dap_terminate, + }, "w" => { "Window" "C-w" | "w" => rotate_view, "C-h" | "h" => hsplit, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4da8bfd5..92a631ed 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -15,6 +15,7 @@ use helix_core::{ unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, }; +use helix_dap::{SourceBreakpoint, StackFrame}; use helix_view::{ document::Mode, editor::LineNumber, @@ -71,6 +72,7 @@ impl EditorView { is_focused: bool, loader: &syntax::Loader, config: &helix_view::editor::Config, + debugger: &Option<helix_dap::Client>, ) { let inner = view.inner_area(); let area = view.area; @@ -87,7 +89,9 @@ impl EditorView { }; Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights); - Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config); + Self::render_gutter( + doc, view, view.area, surface, theme, is_focused, config, debugger, + ); if is_focused { Self::render_focused_view_elements(view, doc, inner, theme, surface); @@ -106,7 +110,7 @@ impl EditorView { } } - self.render_diagnostics(doc, view, inner, surface, theme); + self.render_diagnostics(doc, view, inner, surface, theme, debugger); let statusline_area = view .area @@ -409,6 +413,7 @@ impl EditorView { theme: &Theme, is_focused: bool, config: &helix_view::editor::Config, + debugger: &Option<helix_dap::Client>, ) { let text = doc.text().slice(..); let last_line = view.last_line(doc); @@ -438,6 +443,15 @@ impl EditorView { .map(|range| range.cursor_line(text)) .collect(); + let mut breakpoints: Option<Vec<SourceBreakpoint>> = None; + let mut stack_pointer: Option<StackFrame> = None; + if let Some(debugger) = debugger { + if let Some(path) = doc.path() { + breakpoints = debugger.breakpoints.get(path).cloned(); + stack_pointer = debugger.stack_pointer.clone() + } + } + for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { use helix_core::diagnostic::Severity; if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) { @@ -457,6 +471,36 @@ impl EditorView { let selected = cursors.contains(&line); + if let Some(bps) = breakpoints.as_ref() { + if let Some(breakpoint) = bps.iter().find(|breakpoint| breakpoint.line - 1 == line) + { + if breakpoint.condition.is_some() { + surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, error); + } else if breakpoint.log_message.is_some() { + surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, info); + } else { + surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, warning); + } + } + } + + if let Some(sp) = stack_pointer.as_ref() { + if let Some(src) = sp.source.as_ref() { + if doc + .path() + .map(|path| src.path == Some(path.clone())) + .unwrap_or(false) + && sp.line - 1 == line + { + surface.set_style( + Rect::new(viewport.x, viewport.y + i as u16, 6, 1), + helix_view::graphics::Style::default() + .bg(helix_view::graphics::Color::LightYellow), + ); + } + } + } + let text = if line == last_line && !draw_last { " ~".into() } else { @@ -487,6 +531,7 @@ impl EditorView { viewport: Rect, surface: &mut Surface, theme: &Theme, + debugger: &Option<helix_dap::Client>, ) { use helix_core::diagnostic::Severity; use tui::{ @@ -524,6 +569,31 @@ impl EditorView { lines.extend(text.lines); } + if let Some(debugger) = debugger { + if let Some(path) = doc.path() { + if let Some(breakpoints) = debugger.breakpoints.get(path) { + let line = doc.text().char_to_line(cursor); + if let Some(breakpoint) = breakpoints + .iter() + .find(|breakpoint| breakpoint.line - 1 == line) + { + if let Some(condition) = &breakpoint.condition { + lines.extend( + Text::styled(condition, info.add_modifier(Modifier::UNDERLINED)) + .lines, + ); + } + if let Some(log_message) = &breakpoint.log_message { + lines.extend( + Text::styled(log_message, info.add_modifier(Modifier::UNDERLINED)) + .lines, + ); + } + } + } + } + } + let paragraph = Paragraph::new(lines).alignment(Alignment::Right); let width = 80.min(viewport.width); let height = 15.min(viewport.height); @@ -1010,6 +1080,7 @@ impl Component for EditorView { is_focused, loader, &cx.editor.config, + &cx.editor.debugger, ); } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index c0a39700..1f55a36b 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -18,6 +18,7 @@ bitflags = "1.3" anyhow = "1" helix-core = { version = "0.4", path = "../helix-core" } helix-lsp = { version = "0.4", path = "../helix-lsp"} +helix-dap = { version = "0.4", path = "../helix-dap"} crossterm = { version = "0.21", optional = true } # Conversion traits @@ -25,6 +26,7 @@ once_cell = "1.8" url = "2" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } +tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } slotmap = "1" diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 18cb9106..295dfc0e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -7,6 +7,9 @@ use crate::{ }; use futures_util::future; +use futures_util::stream::select_all::SelectAll; +use tokio_stream::wrappers::UnboundedReceiverStream; + use std::{ path::{Path, PathBuf}, sync::Arc, @@ -70,6 +73,10 @@ pub struct Editor { pub registers: Registers, pub theme: Theme, pub language_servers: helix_lsp::Registry, + + pub debugger: Option<helix_dap::Client>, + pub debugger_events: SelectAll<UnboundedReceiverStream<helix_dap::Payload>>, + pub clipboard_provider: Box<dyn ClipboardProvider>, pub syn_loader: Arc<syntax::Loader>, @@ -107,6 +114,8 @@ impl Editor { selected_register: RegisterSelection::default(), theme: themes.default(), language_servers, + debugger: None, + debugger_events: SelectAll::new(), syn_loader: config_loader, theme_loader: themes, registers: Registers::default(), diff --git a/languages.toml b/languages.toml index 47155523..9b9fb4b0 100644 --- a/languages.toml +++ b/languages.toml @@ -20,6 +20,23 @@ config = """ language-server = { command = "rust-analyzer" } indent = { tab-width = 4, unit = " " } +[language.debugger] +name = "lldb" +transport = "tcp" +command = "lldb-vscode" +args = [] +port-arg = "-p {}" + +[[language.debugger.templates]] +name = "binary" +request = "launch" +args = { console = "internalConsole", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +args = { console = "internalConsole", pid = "{0}" } + [[language]] name = "toml" scope = "source.toml" @@ -70,6 +87,23 @@ comment-token = "//" language-server = { command = "clangd" } indent = { tab-width = 2, unit = " " } +[language.debugger] +name = "lldb" +transport = "tcp" +command = "lldb-vscode" +args = [] +port-arg = "-p {}" + +[[language.debugger.templates]] +name = "binary" +request = "launch" +args = { console = "internalConsole", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +args = { console = "internalConsole", pid = "{0}" } + [[language]] name = "cpp" scope = "source.cpp" @@ -81,6 +115,23 @@ comment-token = "//" language-server = { command = "clangd" } indent = { tab-width = 2, unit = " " } +[language.debugger] +name = "lldb" +transport = "tcp" +command = "lldb-vscode" +args = [] +port-arg = "-p {}" + +[[language.debugger.templates]] +name = "binary" +request = "launch" +args = { console = "internalConsole", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +args = { console = "internalConsole", pid = "{0}" } + [[language]] name = "go" scope = "source.go" @@ -94,6 +145,33 @@ language-server = { command = "gopls" } # TODO: gopls needs utf-8 offsets? indent = { tab-width = 4, unit = "\t" } +[language.debugger] +name = "go" +transport = "tcp" +command = "dlv" +args = ["dap"] +port-arg = "-l 127.0.0.1:{}" + +[[language.debugger.templates]] +name = "source" +request = "launch" +args = { mode = "debug", program = "{0}" } + +[[language.debugger.templates]] +name = "binary" +request = "launch" +args = { mode = "exec", program = "{0}" } + +[[language.debugger.templates]] +name = "test" +request = "launch" +args = { mode = "test", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +args = { mode = "local", processId = "{0}" } + [[language]] name = "javascript" scope = "source.js" |