aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock19
-rw-r--r--Cargo.toml1
-rw-r--r--helix-core/Cargo.toml2
-rw-r--r--helix-core/src/indent.rs2
-rw-r--r--helix-core/src/syntax.rs3
-rw-r--r--helix-dap/Cargo.toml23
-rw-r--r--helix-dap/examples/dap-dlv.rs117
-rw-r--r--helix-dap/examples/dap-lldb.rs116
-rw-r--r--helix-dap/src/client.rs396
-rw-r--r--helix-dap/src/lib.rs24
-rw-r--r--helix-dap/src/transport.rs246
-rw-r--r--helix-dap/src/types.rs671
-rw-r--r--helix-lsp/Cargo.toml4
-rw-r--r--helix-term/Cargo.toml3
-rw-r--r--helix-term/src/application.rs144
-rw-r--r--helix-term/src/commands.rs468
-rw-r--r--helix-term/src/keymap.rs11
-rw-r--r--helix-term/src/ui/editor.rs75
-rw-r--r--helix-view/Cargo.toml2
-rw-r--r--helix-view/src/editor.rs9
-rw-r--r--languages.toml78
21 files changed, 2402 insertions, 12 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e7dbb9c8..59ada4ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 22d29260..2f841c85 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"