mirror of https://github.com/helix-editor/helix
Merge branch 'master' into help-command
commit
c95317a8ec
@ -0,0 +1,33 @@
|
|||||||
|
use crate::merge_toml_values;
|
||||||
|
|
||||||
|
/// Default bultin-in languages.toml.
|
||||||
|
pub fn default_lang_config() -> toml::Value {
|
||||||
|
toml::from_slice(include_bytes!("../../languages.toml"))
|
||||||
|
.expect("Could not parse bultin-in languages.toml to valid toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User configured languages.toml file, merged with the default config.
|
||||||
|
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
|
||||||
|
let def_lang_conf = default_lang_config();
|
||||||
|
let data = std::fs::read(crate::config_dir().join("languages.toml"));
|
||||||
|
let user_lang_conf = match data {
|
||||||
|
Ok(raw) => {
|
||||||
|
let value = toml::from_slice(&raw)?;
|
||||||
|
merge_toml_values(def_lang_conf, value)
|
||||||
|
}
|
||||||
|
Err(_) => def_lang_conf,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(user_lang_conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Syntax configuration loader based on built-in languages.toml.
|
||||||
|
pub fn default_syntax_loader() -> crate::syntax::Configuration {
|
||||||
|
default_lang_config()
|
||||||
|
.try_into()
|
||||||
|
.expect("Could not serialize built-in language.toml")
|
||||||
|
}
|
||||||
|
/// Syntax configuration loader based on user configured languages.toml.
|
||||||
|
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
|
||||||
|
user_lang_config()?.try_into()
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "helix-dap"
|
||||||
|
version = "0.6.0"
|
||||||
|
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]
|
||||||
|
helix-core = { version = "0.6", path = "../helix-core" }
|
||||||
|
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"
|
@ -0,0 +1,477 @@
|
|||||||
|
use crate::{
|
||||||
|
transport::{Payload, Request, Response, Transport},
|
||||||
|
types::*,
|
||||||
|
Error, Result, ThreadId,
|
||||||
|
};
|
||||||
|
use helix_core::syntax::DebuggerQuirks;
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
pub use log::{error, info};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
future::Future,
|
||||||
|
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<Payload>,
|
||||||
|
request_counter: AtomicU64,
|
||||||
|
pub caps: Option<DebuggerCapabilities>,
|
||||||
|
// thread_id -> frames
|
||||||
|
pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
|
||||||
|
pub thread_states: HashMap<ThreadId, String>,
|
||||||
|
pub thread_id: Option<ThreadId>,
|
||||||
|
/// Currently active frame for the current thread.
|
||||||
|
pub active_frame: Option<usize>,
|
||||||
|
pub quirks: DebuggerQuirks,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
// Spawn a process and communicate with it by either TCP or stdio
|
||||||
|
pub async fn process(
|
||||||
|
transport: &str,
|
||||||
|
command: &str,
|
||||||
|
args: Vec<&str>,
|
||||||
|
port_arg: Option<&str>,
|
||||||
|
id: usize,
|
||||||
|
) -> Result<(Self, UnboundedReceiver<Payload>)> {
|
||||||
|
if command.is_empty() {
|
||||||
|
return Result::Err(Error::Other(anyhow!("Command not provided")));
|
||||||
|
}
|
||||||
|
if transport == "tcp" && port_arg.is_some() {
|
||||||
|
Self::tcp_process(command, args, port_arg.unwrap(), id).await
|
||||||
|
} else if transport == "stdio" {
|
||||||
|
Self::stdio(command, args, id)
|
||||||
|
} else {
|
||||||
|
Result::Err(Error::Other(anyhow!("Incorrect transport {}", transport)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn streams(
|
||||||
|
rx: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||||
|
tx: Box<dyn AsyncWrite + Unpin + Send>,
|
||||||
|
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||||
|
id: usize,
|
||||||
|
process: Option<Child>,
|
||||||
|
) -> Result<(Self, UnboundedReceiver<Payload>)> {
|
||||||
|
let (server_rx, server_tx) = Transport::start(rx, tx, err, id);
|
||||||
|
let (client_rx, client_tx) = unbounded_channel();
|
||||||
|
|
||||||
|
let client = Self {
|
||||||
|
id,
|
||||||
|
_process: process,
|
||||||
|
server_tx,
|
||||||
|
request_counter: AtomicU64::new(0),
|
||||||
|
caps: None,
|
||||||
|
//
|
||||||
|
stack_frames: HashMap::new(),
|
||||||
|
thread_states: HashMap::new(),
|
||||||
|
thread_id: None,
|
||||||
|
active_frame: None,
|
||||||
|
quirks: DebuggerQuirks::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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), None, 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"));
|
||||||
|
let errors = process.stderr.take().map(BufReader::new);
|
||||||
|
|
||||||
|
Self::streams(
|
||||||
|
Box::new(BufReader::new(reader)),
|
||||||
|
Box::new(writer),
|
||||||
|
// errors.map(|errors| Box::new(BufReader::new(errors))),
|
||||||
|
match errors {
|
||||||
|
Some(errors) => Some(Box::new(BufReader::new(errors))),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
None,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal, called by specific DAP commands when resuming
|
||||||
|
pub fn resume_application(&mut self) {
|
||||||
|
if let Some(thread_id) = self.thread_id {
|
||||||
|
self.thread_states.insert(thread_id, "running".to_string());
|
||||||
|
self.stack_frames.remove(&thread_id);
|
||||||
|
}
|
||||||
|
self.active_frame = None;
|
||||||
|
self.thread_id = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a RPC request on the debugger.
|
||||||
|
pub fn call<R: crate::types::Request>(
|
||||||
|
&self,
|
||||||
|
arguments: R::Arguments,
|
||||||
|
) -> impl Future<Output = Result<Value>>
|
||||||
|
where
|
||||||
|
R::Arguments: serde::Serialize,
|
||||||
|
{
|
||||||
|
let server_tx = self.server_tx.clone();
|
||||||
|
let id = self.next_request_id();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
let arguments = Some(serde_json::to_value(arguments)?);
|
||||||
|
|
||||||
|
let (callback_tx, mut callback_rx) = channel(1);
|
||||||
|
|
||||||
|
let req = Request {
|
||||||
|
back_ch: Some(callback_tx),
|
||||||
|
seq: id,
|
||||||
|
command: R::COMMAND.to_string(),
|
||||||
|
arguments,
|
||||||
|
};
|
||||||
|
|
||||||
|
server_tx
|
||||||
|
.send(Payload::Request(req))
|
||||||
|
.map_err(|e| Error::Other(e.into()))?;
|
||||||
|
|
||||||
|
// TODO: specifiable timeout, delay other calls until initialize success
|
||||||
|
timeout(Duration::from_secs(20), callback_rx.recv())
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Timeout)? // return Timeout
|
||||||
|
.ok_or(Error::StreamClosed)?
|
||||||
|
.map(|response| response.body.unwrap_or_default())
|
||||||
|
// TODO: check response.success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request<R: crate::types::Request>(&self, params: R::Arguments) -> Result<R::Result>
|
||||||
|
where
|
||||||
|
R::Arguments: serde::Serialize,
|
||||||
|
R::Result: core::fmt::Debug, // TODO: temporary
|
||||||
|
{
|
||||||
|
// a future that resolves into the response
|
||||||
|
let json = self.call::<R>(params).await?;
|
||||||
|
let response = serde_json::from_value(json)?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reply(
|
||||||
|
&self,
|
||||||
|
request_seq: u64,
|
||||||
|
command: &str,
|
||||||
|
result: core::result::Result<Value, Error>,
|
||||||
|
) -> impl Future<Output = Result<()>> {
|
||||||
|
let server_tx = self.server_tx.clone();
|
||||||
|
let command = command.to_string();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let response = match result {
|
||||||
|
Ok(result) => Response {
|
||||||
|
request_seq,
|
||||||
|
command,
|
||||||
|
success: true,
|
||||||
|
message: None,
|
||||||
|
body: Some(result),
|
||||||
|
},
|
||||||
|
Err(error) => Response {
|
||||||
|
request_seq,
|
||||||
|
command,
|
||||||
|
success: false,
|
||||||
|
message: Some(error.to_string()),
|
||||||
|
body: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
server_tx
|
||||||
|
.send(Payload::Response(response))
|
||||||
|
.map_err(|e| Error::Other(e.into()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(true),
|
||||||
|
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 fn disconnect(&self) -> impl Future<Output = Result<Value>> {
|
||||||
|
self.call::<requests::Disconnect>(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch(&self, args: serde_json::Value) -> impl Future<Output = Result<Value>> {
|
||||||
|
self.call::<requests::Launch>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attach(&self, args: serde_json::Value) -> impl Future<Output = Result<Value>> {
|
||||||
|
self.call::<requests::Attach>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_breakpoints(
|
||||||
|
&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(&self) -> Result<()> {
|
||||||
|
self.request::<requests::ConfigurationDone>(()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn continue_thread(&self, thread_id: ThreadId) -> impl Future<Output = Result<Value>> {
|
||||||
|
let args = requests::ContinueArguments { thread_id };
|
||||||
|
|
||||||
|
self.call::<requests::Continue>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stack_trace(
|
||||||
|
&self,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
) -> 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 fn threads(&self) -> impl Future<Output = Result<Value>> {
|
||||||
|
self.call::<requests::Threads>(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn scopes(&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(&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 fn step_in(&self, thread_id: ThreadId) -> impl Future<Output = Result<Value>> {
|
||||||
|
let args = requests::StepInArguments {
|
||||||
|
thread_id,
|
||||||
|
target_id: None,
|
||||||
|
granularity: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call::<requests::StepIn>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_out(&self, thread_id: ThreadId) -> impl Future<Output = Result<Value>> {
|
||||||
|
let args = requests::StepOutArguments {
|
||||||
|
thread_id,
|
||||||
|
granularity: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call::<requests::StepOut>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self, thread_id: ThreadId) -> impl Future<Output = Result<Value>> {
|
||||||
|
let args = requests::NextArguments {
|
||||||
|
thread_id,
|
||||||
|
granularity: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call::<requests::Next>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause(&self, thread_id: ThreadId) -> impl Future<Output = Result<Value>> {
|
||||||
|
let args = requests::PauseArguments { thread_id };
|
||||||
|
|
||||||
|
self.call::<requests::Pause>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn eval(
|
||||||
|
&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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_exception_breakpoints(
|
||||||
|
&self,
|
||||||
|
filters: Vec<String>,
|
||||||
|
) -> impl Future<Output = Result<Value>> {
|
||||||
|
let args = requests::SetExceptionBreakpointsArguments { filters };
|
||||||
|
|
||||||
|
self.call::<requests::SetExceptionBreakpoints>(args)
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
@ -0,0 +1,280 @@
|
|||||||
|
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(Box<Event>),
|
||||||
|
// type = "response"
|
||||||
|
Response(Response),
|
||||||
|
// type = "request"
|
||||||
|
Request(Request),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Transport {
|
||||||
|
#[allow(unused)]
|
||||||
|
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>,
|
||||||
|
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||||
|
id: usize,
|
||||||
|
) -> (UnboundedReceiver<Payload>, UnboundedSender<Payload>) {
|
||||||
|
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));
|
||||||
|
if let Some(stderr) = server_stderr {
|
||||||
|
tokio::spawn(Self::err(stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
(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);
|
||||||
|
if reader.read_line(buffer).await? == 0 {
|
||||||
|
return Err(Error::StreamClosed);
|
||||||
|
};
|
||||||
|
|
||||||
|
if buffer == "\r\n" {
|
||||||
|
// look for an empty CRLF line
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = buffer.trim();
|
||||||
|
let parts = header.split_once(": ");
|
||||||
|
|
||||||
|
match parts {
|
||||||
|
Some(("Content-Length", value)) => {
|
||||||
|
content_length = Some(value.parse().context("invalid content length")?);
|
||||||
|
}
|
||||||
|
Some((_, _)) => {}
|
||||||
|
None => {
|
||||||
|
// Workaround: Some non-conformant language servers will output logging and other garbage
|
||||||
|
// into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn
|
||||||
|
// the server. Skip such lines and log a warning.
|
||||||
|
|
||||||
|
// warn!("Failed to parse header: {:?}", header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 recv_server_error(
|
||||||
|
err: &mut (impl AsyncBufRead + Unpin + Send),
|
||||||
|
buffer: &mut String,
|
||||||
|
) -> Result<()> {
|
||||||
|
buffer.truncate(0);
|
||||||
|
if err.read_line(buffer).await? == 0 {
|
||||||
|
return Err(Error::StreamClosed);
|
||||||
|
};
|
||||||
|
error!("err <- {}", buffer);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_payload_to_server(
|
||||||
|
&self,
|
||||||
|
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||||
|
mut payload: Payload,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Payload::Request(request) = &mut payload {
|
||||||
|
if let Some(back) = request.back_ch.take() {
|
||||||
|
self.pending_requests.lock().await.insert(request.seq, back);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string(&payload)?;
|
||||||
|
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<Payload>,
|
||||||
|
) {
|
||||||
|
while let Some(payload) = client_rx.recv().await {
|
||||||
|
transport
|
||||||
|
.send_payload_to_server(&mut server_stdin, payload)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn err(mut server_stderr: Box<dyn AsyncBufRead + Unpin + Send>) {
|
||||||
|
let mut recv_buffer = String::new();
|
||||||
|
loop {
|
||||||
|
match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("err: <- {:?}", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,707 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
|
||||||
|
)]
|
||||||
|
pub struct ThreadId(isize);
|
||||||
|
|
||||||
|
impl std::fmt::Display for ThreadId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ty: 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>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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: ThreadId,
|
||||||
|
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 ty: 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: ThreadId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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: ThreadId,
|
||||||
|
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: ThreadId,
|
||||||
|
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: ThreadId,
|
||||||
|
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: ThreadId,
|
||||||
|
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: ThreadId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 ty: 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SetExceptionBreakpointsArguments {
|
||||||
|
pub filters: Vec<String>,
|
||||||
|
// pub filterOptions: Option<Vec<ExceptionFilterOptions>>, // needs capability
|
||||||
|
// pub exceptionOptions: Option<Vec<ExceptionOptions>>, // needs capability
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SetExceptionBreakpointsResponse {
|
||||||
|
pub breakpoints: Option<Vec<Breakpoint>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SetExceptionBreakpoints {}
|
||||||
|
|
||||||
|
impl Request for SetExceptionBreakpoints {
|
||||||
|
type Arguments = SetExceptionBreakpointsArguments;
|
||||||
|
type Result = SetExceptionBreakpointsResponse;
|
||||||
|
const COMMAND: &'static str = "setExceptionBreakpoints";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse Requests
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RunInTerminalResponse {
|
||||||
|
pub process_id: Option<u32>,
|
||||||
|
pub shell_process_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RunInTerminalArguments {
|
||||||
|
pub kind: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub cwd: Option<String>,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub env: Option<HashMap<String, Option<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RunInTerminal {}
|
||||||
|
|
||||||
|
impl Request for RunInTerminal {
|
||||||
|
type Arguments = RunInTerminalArguments;
|
||||||
|
type Result = RunInTerminalResponse;
|
||||||
|
const COMMAND: &'static str = "runInTerminal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<ThreadId>,
|
||||||
|
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: ThreadId,
|
||||||
|
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: ThreadId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ThreadId>,
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit bd50ccf66b42c55252ac8efc1086af4ac6bab8cd
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 86985bde399c5f40b00bc75f7ab70a6c69a5f9c3
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 0e4f0baf90b57e5aeb62dcdbf03062c6315d43ea
|
@ -1 +1 @@
|
|||||||
Subproject commit 2a83dfdd759a632651f852aa4dc0af2525fae5cd
|
Subproject commit 0fa917a7022d1cd2e9b779a6a8fc5dc7fad69c75
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 5e66e961eee421786bdda8495ed1db045e06b5fe
|
@ -1 +1 @@
|
|||||||
Subproject commit 237f4eb4417c28f643a29d795ed227246afb66f9
|
Subproject commit b6ec26f181dd059eedd506fa5fbeae1b8e5556c8
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 3ec55082cf0be015d03148be8edfdfa8c56e77f9
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit d98426109258b266e1e92358c5f11716d2e8f638
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 06fabca19454b2dc00c1b211a7cb7ad0bc2585f1
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit a4b9187417d6be349ee5fd4b6e77b4172c6827dd
|
@ -1 +1 @@
|
|||||||
Subproject commit 0d63eaf94e8d6c0694551b016c802787e61b3fb2
|
Subproject commit 57f855461aeeca73bd4218754fb26b5ac143f98f
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e1cfca3c79896ff79842f057ea13e529b66af636
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 761eb9126b65e078b1b5770ac296b4af8870f933
|
@ -0,0 +1 @@
|
|||||||
|
Subproject commit b7444181fb38e603e25ea8fcdac55f9492e49c27
|
@ -1 +1 @@
|
|||||||
Subproject commit 1f27fd1dfe7f352408f01b4894c7825f3a1d6c47
|
Subproject commit 93331b8bd8b4ebee2b575490b2758f16ad4e9f30
|
@ -1,12 +1,17 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let git_hash = Command::new("git")
|
let git_hash = Command::new("git")
|
||||||
.args(&["describe", "--dirty"])
|
.args(&["rev-parse", "HEAD"])
|
||||||
.output()
|
.output()
|
||||||
.map(|x| String::from_utf8(x.stdout).ok())
|
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.and_then(|x| String::from_utf8(x.stdout).ok());
|
||||||
.unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
|
|
||||||
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
|
let version: Cow<_> = match git_hash {
|
||||||
|
Some(git_hash) => format!("{} ({})", env!("CARGO_PKG_VERSION"), &git_hash[..8]).into(),
|
||||||
|
None => env!("CARGO_PKG_VERSION").into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,827 @@
|
|||||||
|
use super::{align_view, Align, Context, Editor};
|
||||||
|
use crate::{
|
||||||
|
compositor::{self, Compositor},
|
||||||
|
job::{Callback, Jobs},
|
||||||
|
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
|
||||||
|
};
|
||||||
|
use helix_core::{
|
||||||
|
syntax::{DebugArgumentValue, DebugConfigCompletion},
|
||||||
|
Selection,
|
||||||
|
};
|
||||||
|
use helix_dap::{self as dap, Client, ThreadId};
|
||||||
|
use helix_lsp::block_on;
|
||||||
|
use helix_view::editor::Breakpoint;
|
||||||
|
|
||||||
|
use serde_json::{to_value, Value};
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! debugger {
|
||||||
|
($editor:expr) => {{
|
||||||
|
match &mut $editor.debugger {
|
||||||
|
Some(debugger) => debugger,
|
||||||
|
None => return,
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// general utils:
|
||||||
|
pub fn dap_pos_to_pos(doc: &helix_core::Rope, line: usize, column: usize) -> Option<usize> {
|
||||||
|
// 1-indexing to 0 indexing
|
||||||
|
let line = doc.try_line_to_char(line - 1).ok()?;
|
||||||
|
let pos = line + column.saturating_sub(1);
|
||||||
|
// TODO: this is probably utf-16 offsets
|
||||||
|
Some(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn select_thread_id(editor: &mut Editor, thread_id: ThreadId, force: bool) {
|
||||||
|
let debugger = debugger!(editor);
|
||||||
|
|
||||||
|
if !force && debugger.thread_id.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugger.thread_id = Some(thread_id);
|
||||||
|
fetch_stack_trace(debugger, thread_id).await;
|
||||||
|
|
||||||
|
let frame = debugger.stack_frames[&thread_id].get(0).cloned();
|
||||||
|
if let Some(frame) = &frame {
|
||||||
|
jump_to_stack_frame(editor, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_stack_trace(debugger: &mut Client, thread_id: ThreadId) {
|
||||||
|
let (frames, _) = match debugger.stack_trace(thread_id).await {
|
||||||
|
Ok(frames) => frames,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
debugger.stack_frames.insert(thread_id, frames);
|
||||||
|
debugger.active_frame = Some(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) {
|
||||||
|
let path = if let Some(helix_dap::Source {
|
||||||
|
path: Some(ref path),
|
||||||
|
..
|
||||||
|
}) = frame.source
|
||||||
|
{
|
||||||
|
path.clone()
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = editor.open(path, helix_view::editor::Action::Replace) {
|
||||||
|
editor.set_error(format!("Unable to jump to stack frame: {}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
let text_end = doc.text().len_chars().saturating_sub(1);
|
||||||
|
let start = dap_pos_to_pos(doc.text(), frame.line, frame.column).unwrap_or(0);
|
||||||
|
let end = frame
|
||||||
|
.end_line
|
||||||
|
.and_then(|end_line| dap_pos_to_pos(doc.text(), end_line, frame.end_column.unwrap_or(0)))
|
||||||
|
.unwrap_or(start);
|
||||||
|
|
||||||
|
let selection = Selection::single(start.min(text_end), end.min(text_end));
|
||||||
|
doc.set_selection(view.id, selection);
|
||||||
|
align_view(doc, view, Align::Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thread_picker(
|
||||||
|
cx: &mut Context,
|
||||||
|
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
|
||||||
|
) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
let future = debugger.threads();
|
||||||
|
dap_callback(
|
||||||
|
cx.jobs,
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
response: dap::requests::ThreadsResponse| {
|
||||||
|
let threads = response.threads;
|
||||||
|
if threads.len() == 1 {
|
||||||
|
callback_fn(editor, &threads[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let debugger = debugger!(editor);
|
||||||
|
|
||||||
|
let thread_states = debugger.thread_states.clone();
|
||||||
|
let picker = FilePicker::new(
|
||||||
|
threads,
|
||||||
|
move |thread| {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
thread.name,
|
||||||
|
thread_states
|
||||||
|
.get(&thread.id)
|
||||||
|
.map(|state| state.as_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
},
|
||||||
|
move |cx, thread, _action| callback_fn(cx.editor, thread),
|
||||||
|
move |editor, thread| {
|
||||||
|
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||||
|
let frame = frames.get(0)?;
|
||||||
|
let path = frame.source.as_ref()?.path.clone()?;
|
||||||
|
let pos = Some((
|
||||||
|
frame.line.saturating_sub(1),
|
||||||
|
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||||
|
));
|
||||||
|
Some((path, pos))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
compositor.push(Box::new(picker));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_breakpoint_at_current_line(editor: &mut Editor) -> Option<(usize, Breakpoint)> {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
|
||||||
|
let line = doc.selection(view.id).primary().cursor_line(text);
|
||||||
|
let path = doc.path()?;
|
||||||
|
editor.breakpoints.get(path).and_then(|breakpoints| {
|
||||||
|
let i = breakpoints.iter().position(|b| b.line == line);
|
||||||
|
i.map(|i| (i, breakpoints[i].clone()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- DAP
|
||||||
|
|
||||||
|
fn dap_callback<T, F>(
|
||||||
|
jobs: &mut Jobs,
|
||||||
|
call: impl Future<Output = helix_dap::Result<serde_json::Value>> + 'static + Send,
|
||||||
|
callback: F,
|
||||||
|
) where
|
||||||
|
T: for<'de> serde::Deserialize<'de> + Send + 'static,
|
||||||
|
F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
|
||||||
|
{
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let json = call.await?;
|
||||||
|
let response = serde_json::from_value(json)?;
|
||||||
|
let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||||
|
callback(editor, compositor, response)
|
||||||
|
});
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
jobs.callback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_start_impl(
|
||||||
|
cx: &mut compositor::Context,
|
||||||
|
name: Option<&str>,
|
||||||
|
socket: Option<std::net::SocketAddr>,
|
||||||
|
params: Option<Vec<std::borrow::Cow<str>>>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let doc = doc!(cx.editor);
|
||||||
|
|
||||||
|
let config = doc
|
||||||
|
.language_config()
|
||||||
|
.and_then(|config| config.debugger.as_ref())
|
||||||
|
.ok_or(anyhow!("No debug adapter available for language"))?;
|
||||||
|
|
||||||
|
let result = match socket {
|
||||||
|
Some(socket) => block_on(Client::tcp(socket, 0)),
|
||||||
|
None => block_on(Client::process(
|
||||||
|
&config.transport,
|
||||||
|
&config.command,
|
||||||
|
config.args.iter().map(|arg| arg.as_str()).collect(),
|
||||||
|
config.port_arg.as_deref(),
|
||||||
|
0,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut debugger, events) = match result {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => bail!("Failed to start debug session: {}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = debugger.initialize(config.name.clone());
|
||||||
|
if let Err(e) = block_on(request) {
|
||||||
|
bail!("Failed to initialize debug adapter: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugger.quirks = config.quirks.clone();
|
||||||
|
|
||||||
|
// TODO: avoid refetching all of this... pass a config in
|
||||||
|
let template = match name {
|
||||||
|
Some(name) => config.templates.iter().find(|t| t.name == name),
|
||||||
|
None => config.templates.get(0),
|
||||||
|
}
|
||||||
|
.ok_or(anyhow!("No debug config with given name"))?;
|
||||||
|
|
||||||
|
let mut args: HashMap<&str, Value> = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(params) = params {
|
||||||
|
for (k, t) in &template.args {
|
||||||
|
let mut value = t.clone();
|
||||||
|
for (i, x) in params.iter().enumerate() {
|
||||||
|
let mut param = x.to_string();
|
||||||
|
if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) {
|
||||||
|
if matches!(cfg.completion.as_deref(), Some("filename" | "directory")) {
|
||||||
|
param = std::fs::canonicalize(x.as_ref())
|
||||||
|
.ok()
|
||||||
|
.and_then(|pb| pb.into_os_string().into_string().ok())
|
||||||
|
.unwrap_or_else(|| x.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For param #0 replace {0} in args
|
||||||
|
let pattern = format!("{{{}}}", i);
|
||||||
|
value = match value {
|
||||||
|
// TODO: just use toml::Value -> json::Value
|
||||||
|
DebugArgumentValue::String(v) => {
|
||||||
|
DebugArgumentValue::String(v.replace(&pattern, ¶m))
|
||||||
|
}
|
||||||
|
DebugArgumentValue::Array(arr) => DebugArgumentValue::Array(
|
||||||
|
arr.iter().map(|v| v.replace(&pattern, ¶m)).collect(),
|
||||||
|
),
|
||||||
|
DebugArgumentValue::Boolean(_) => value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match value {
|
||||||
|
DebugArgumentValue::String(string) => {
|
||||||
|
if let Ok(integer) = string.parse::<usize>() {
|
||||||
|
args.insert(k, to_value(integer).unwrap());
|
||||||
|
} else {
|
||||||
|
args.insert(k, to_value(string).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebugArgumentValue::Array(arr) => {
|
||||||
|
args.insert(k, to_value(arr).unwrap());
|
||||||
|
}
|
||||||
|
DebugArgumentValue::Boolean(bool) => {
|
||||||
|
args.insert(k, to_value(bool).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = to_value(args).unwrap();
|
||||||
|
|
||||||
|
let callback = |_editor: &mut Editor, _compositor: &mut Compositor, _response: Value| {
|
||||||
|
// if let Err(e) = result {
|
||||||
|
// editor.set_error(format!("Failed {} target: {}", template.request, e));
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
match &template.request[..] {
|
||||||
|
"launch" => {
|
||||||
|
let call = debugger.launch(args);
|
||||||
|
dap_callback(cx.jobs, call, callback);
|
||||||
|
}
|
||||||
|
"attach" => {
|
||||||
|
let call = debugger.attach(args);
|
||||||
|
dap_callback(cx.jobs, call, callback);
|
||||||
|
}
|
||||||
|
request => bail!("Unsupported request '{}'", request),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: either await "initialized" or buffer commands until event is received
|
||||||
|
cx.editor.debugger = Some(debugger);
|
||||||
|
let stream = UnboundedReceiverStream::new(events);
|
||||||
|
cx.editor.debugger_events.push(stream);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_launch(cx: &mut Context) {
|
||||||
|
if cx.editor.debugger.is_some() {
|
||||||
|
cx.editor.set_error("Debugger is already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = doc!(cx.editor);
|
||||||
|
|
||||||
|
let config = match doc
|
||||||
|
.language_config()
|
||||||
|
.and_then(|config| config.debugger.as_ref())
|
||||||
|
{
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
cx.editor
|
||||||
|
.set_error("No debug adapter available for language");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let templates = config.templates.clone();
|
||||||
|
|
||||||
|
cx.push_layer(Box::new(overlayed(Picker::new(
|
||||||
|
templates,
|
||||||
|
|template| template.name.as_str().into(),
|
||||||
|
|cx, template, _action| {
|
||||||
|
let completions = template.completion.clone();
|
||||||
|
let name = template.name.clone();
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback =
|
||||||
|
Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||||
|
let prompt = debug_parameter_prompt(completions, name, Vec::new());
|
||||||
|
compositor.push(Box::new(prompt));
|
||||||
|
});
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
},
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_parameter_prompt(
|
||||||
|
completions: Vec<DebugConfigCompletion>,
|
||||||
|
config_name: String,
|
||||||
|
mut params: Vec<String>,
|
||||||
|
) -> Prompt {
|
||||||
|
let completion = completions.get(params.len()).unwrap();
|
||||||
|
let field_type = if let DebugConfigCompletion::Advanced(cfg) = completion {
|
||||||
|
cfg.completion.as_deref().unwrap_or("")
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let name = match completion {
|
||||||
|
DebugConfigCompletion::Advanced(cfg) => cfg.name.as_deref().unwrap_or(field_type),
|
||||||
|
DebugConfigCompletion::Named(name) => name.as_str(),
|
||||||
|
};
|
||||||
|
let default_val = match completion {
|
||||||
|
DebugConfigCompletion::Advanced(cfg) => cfg.default.as_deref().unwrap_or(""),
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let completer = match field_type {
|
||||||
|
"filename" => ui::completers::filename,
|
||||||
|
"directory" => ui::completers::directory,
|
||||||
|
_ => |_input: &str| Vec::new(),
|
||||||
|
};
|
||||||
|
Prompt::new(
|
||||||
|
format!("{}: ", name).into(),
|
||||||
|
None,
|
||||||
|
completer,
|
||||||
|
move |cx, input: &str, event: PromptEvent| {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut value = input.to_owned();
|
||||||
|
if value.is_empty() {
|
||||||
|
value = default_val.clone();
|
||||||
|
}
|
||||||
|
params.push(value);
|
||||||
|
|
||||||
|
if params.len() < completions.len() {
|
||||||
|
let completions = completions.clone();
|
||||||
|
let config_name = config_name.clone();
|
||||||
|
let params = params.clone();
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback =
|
||||||
|
Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||||
|
let prompt = debug_parameter_prompt(completions, config_name, params);
|
||||||
|
compositor.push(Box::new(prompt));
|
||||||
|
});
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
} else if let Err(e) = dap_start_impl(
|
||||||
|
cx,
|
||||||
|
Some(&config_name),
|
||||||
|
None,
|
||||||
|
Some(params.iter().map(|x| x.into()).collect()),
|
||||||
|
) {
|
||||||
|
cx.editor.set_error(e.to_string());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_toggle_breakpoint(cx: &mut Context) {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
let path = match doc.path() {
|
||||||
|
Some(path) => path.clone(),
|
||||||
|
None => {
|
||||||
|
cx.editor
|
||||||
|
.set_error("Can't set breakpoint: document has no path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
let line = doc.selection(view.id).primary().cursor_line(text);
|
||||||
|
dap_toggle_breakpoint_impl(cx, path, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn breakpoints_changed(
|
||||||
|
debugger: &mut dap::Client,
|
||||||
|
path: PathBuf,
|
||||||
|
breakpoints: &mut [Breakpoint],
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
// TODO: handle capabilities correctly again, by filterin breakpoints when emitting
|
||||||
|
// if breakpoint.condition.is_some()
|
||||||
|
// && !debugger
|
||||||
|
// .caps
|
||||||
|
// .as_ref()
|
||||||
|
// .unwrap()
|
||||||
|
// .supports_conditional_breakpoints
|
||||||
|
// .unwrap_or_default()
|
||||||
|
// {
|
||||||
|
// bail!(
|
||||||
|
// "Can't edit breakpoint: debugger does not support conditional breakpoints"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// if breakpoint.log_message.is_some()
|
||||||
|
// && !debugger
|
||||||
|
// .caps
|
||||||
|
// .as_ref()
|
||||||
|
// .unwrap()
|
||||||
|
// .supports_log_points
|
||||||
|
// .unwrap_or_default()
|
||||||
|
// {
|
||||||
|
// bail!("Can't edit breakpoint: debugger does not support logpoints")
|
||||||
|
// }
|
||||||
|
let source_breakpoints = breakpoints
|
||||||
|
.iter()
|
||||||
|
.map(|breakpoint| helix_dap::SourceBreakpoint {
|
||||||
|
line: breakpoint.line + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let request = debugger.set_breakpoints(path, source_breakpoints);
|
||||||
|
match block_on(request) {
|
||||||
|
Ok(Some(dap_breakpoints)) => {
|
||||||
|
for (breakpoint, dap_breakpoint) in breakpoints.iter_mut().zip(dap_breakpoints) {
|
||||||
|
breakpoint.id = dap_breakpoint.id;
|
||||||
|
breakpoint.verified = dap_breakpoint.verified;
|
||||||
|
breakpoint.message = dap_breakpoint.message;
|
||||||
|
// TODO: handle breakpoint.message
|
||||||
|
// TODO: verify source matches
|
||||||
|
breakpoint.line = dap_breakpoint.line.unwrap_or(0).saturating_sub(1); // convert to 0-indexing
|
||||||
|
// TODO: no unwrap
|
||||||
|
breakpoint.column = dap_breakpoint.column;
|
||||||
|
// TODO: verify end_linef/col instruction reference, offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => anyhow::bail!("Failed to set breakpoints: {}", e),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_toggle_breakpoint_impl(cx: &mut Context, path: PathBuf, line: usize) {
|
||||||
|
// TODO: need to map breakpoints over edits and update them?
|
||||||
|
// we shouldn't really allow editing while debug is running though
|
||||||
|
|
||||||
|
let breakpoints = cx.editor.breakpoints.entry(path.clone()).or_default();
|
||||||
|
// TODO: always keep breakpoints sorted and use binary search to determine insertion point
|
||||||
|
if let Some(pos) = breakpoints
|
||||||
|
.iter()
|
||||||
|
.position(|breakpoint| breakpoint.line == line)
|
||||||
|
{
|
||||||
|
breakpoints.remove(pos);
|
||||||
|
} else {
|
||||||
|
breakpoints.push(Breakpoint {
|
||||||
|
line,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if let Err(e) = breakpoints_changed(debugger, path, breakpoints) {
|
||||||
|
cx.editor
|
||||||
|
.set_error(format!("Failed to set breakpoints: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_continue(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if let Some(thread_id) = debugger.thread_id {
|
||||||
|
let request = debugger.continue_thread(thread_id);
|
||||||
|
|
||||||
|
dap_callback(
|
||||||
|
cx.jobs,
|
||||||
|
request,
|
||||||
|
|editor, _compositor, _response: dap::requests::ContinueResponse| {
|
||||||
|
debugger!(editor).resume_application();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cx.editor
|
||||||
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_pause(cx: &mut Context) {
|
||||||
|
thread_picker(cx, |editor, thread| {
|
||||||
|
let debugger = debugger!(editor);
|
||||||
|
let request = debugger.pause(thread.id);
|
||||||
|
// NOTE: we don't need to set active thread id here because DAP will emit a "stopped" event
|
||||||
|
if let Err(e) = block_on(request) {
|
||||||
|
editor.set_error(format!("Failed to pause: {}", e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_step_in(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if let Some(thread_id) = debugger.thread_id {
|
||||||
|
let request = debugger.step_in(thread_id);
|
||||||
|
|
||||||
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
||||||
|
debugger!(editor).resume_application();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cx.editor
|
||||||
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_step_out(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if let Some(thread_id) = debugger.thread_id {
|
||||||
|
let request = debugger.step_out(thread_id);
|
||||||
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
||||||
|
debugger!(editor).resume_application();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cx.editor
|
||||||
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_next(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if let Some(thread_id) = debugger.thread_id {
|
||||||
|
let request = debugger.next(thread_id);
|
||||||
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
||||||
|
debugger!(editor).resume_application();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cx.editor
|
||||||
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_variables(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if debugger.thread_id.is_none() {
|
||||||
|
cx.editor
|
||||||
|
.set_status("Cannot access variables while target is running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
|
||||||
|
(Some(frame), Some(thread_id)) => (frame, thread_id),
|
||||||
|
_ => {
|
||||||
|
cx.editor
|
||||||
|
.set_status("Cannot find current stack frame to access variables");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let frame_id = debugger.stack_frames[&thread_id][frame].id;
|
||||||
|
let scopes = match block_on(debugger.scopes(frame_id)) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
cx.editor.set_error(format!("Failed to get scopes: {}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: allow expanding variables into sub-fields
|
||||||
|
let mut variables = Vec::new();
|
||||||
|
|
||||||
|
let theme = &cx.editor.theme;
|
||||||
|
let scope_style = theme.get("ui.linenr.selected");
|
||||||
|
let type_style = theme.get("ui.text");
|
||||||
|
let text_style = theme.get("ui.text.focus");
|
||||||
|
|
||||||
|
for scope in scopes.iter() {
|
||||||
|
// use helix_view::graphics::Style;
|
||||||
|
use tui::text::{Span, Spans};
|
||||||
|
let response = block_on(debugger.variables(scope.variables_reference));
|
||||||
|
|
||||||
|
variables.push(Spans::from(Span::styled(
|
||||||
|
format!("▸ {}", scope.name),
|
||||||
|
scope_style,
|
||||||
|
)));
|
||||||
|
|
||||||
|
if let Ok(vars) = response {
|
||||||
|
variables.reserve(vars.len());
|
||||||
|
for var in vars {
|
||||||
|
let mut spans = Vec::with_capacity(5);
|
||||||
|
|
||||||
|
spans.push(Span::styled(var.name.to_owned(), text_style));
|
||||||
|
if let Some(ty) = var.ty {
|
||||||
|
spans.push(Span::raw(": "));
|
||||||
|
spans.push(Span::styled(ty.to_owned(), type_style));
|
||||||
|
}
|
||||||
|
spans.push(Span::raw(" = "));
|
||||||
|
spans.push(Span::styled(var.value.to_owned(), text_style));
|
||||||
|
variables.push(Spans::from(spans));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = Text::from(tui::text::Text::from(variables));
|
||||||
|
let popup = Popup::new("dap-variables", contents);
|
||||||
|
cx.push_layer(Box::new(popup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_terminate(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
let request = debugger.disconnect();
|
||||||
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
||||||
|
// editor.set_error(format!("Failed to disconnect: {}", e));
|
||||||
|
editor.debugger = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_enable_exceptions(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
let filters = match &debugger.capabilities().exception_breakpoint_filters {
|
||||||
|
Some(filters) => filters.iter().map(|f| f.filter.clone()).collect(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = debugger.set_exception_breakpoints(filters);
|
||||||
|
|
||||||
|
dap_callback(
|
||||||
|
cx.jobs,
|
||||||
|
request,
|
||||||
|
|_editor, _compositor, _response: dap::requests::SetExceptionBreakpointsResponse| {
|
||||||
|
// editor.set_error(format!("Failed to set up exception breakpoints: {}", e));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_disable_exceptions(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
let request = debugger.set_exception_breakpoints(Vec::new());
|
||||||
|
|
||||||
|
dap_callback(
|
||||||
|
cx.jobs,
|
||||||
|
request,
|
||||||
|
|_editor, _compositor, _response: dap::requests::SetExceptionBreakpointsResponse| {
|
||||||
|
// editor.set_error(format!("Failed to set up exception breakpoints: {}", e));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: both edit condition and edit log need to be stable: we might get new breakpoints from the debugger which can change offsets
|
||||||
|
pub fn dap_edit_condition(cx: &mut Context) {
|
||||||
|
if let Some((pos, breakpoint)) = get_breakpoint_at_current_line(cx.editor) {
|
||||||
|
let path = match doc!(cx.editor).path() {
|
||||||
|
Some(path) => path.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback =
|
||||||
|
Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||||
|
let mut prompt = Prompt::new(
|
||||||
|
"condition:".into(),
|
||||||
|
None,
|
||||||
|
|_input: &str| Vec::new(),
|
||||||
|
move |cx, input: &str, event: PromptEvent| {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap();
|
||||||
|
breakpoints[pos].condition = match input {
|
||||||
|
"" => None,
|
||||||
|
input => Some(input.to_owned()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints)
|
||||||
|
{
|
||||||
|
cx.editor
|
||||||
|
.set_error(format!("Failed to set breakpoints: {}", e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Some(condition) = breakpoint.condition {
|
||||||
|
prompt.insert_str(&condition)
|
||||||
|
}
|
||||||
|
compositor.push(Box::new(prompt));
|
||||||
|
});
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_edit_log(cx: &mut Context) {
|
||||||
|
if let Some((pos, breakpoint)) = get_breakpoint_at_current_line(cx.editor) {
|
||||||
|
let path = match doc!(cx.editor).path() {
|
||||||
|
Some(path) => path.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback =
|
||||||
|
Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||||
|
let mut prompt = Prompt::new(
|
||||||
|
"log-message:".into(),
|
||||||
|
None,
|
||||||
|
|_input: &str| Vec::new(),
|
||||||
|
move |cx, input: &str, event: PromptEvent| {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap();
|
||||||
|
breakpoints[pos].log_message = match input {
|
||||||
|
"" => None,
|
||||||
|
input => Some(input.to_owned()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints)
|
||||||
|
{
|
||||||
|
cx.editor
|
||||||
|
.set_error(format!("Failed to set breakpoints: {}", e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Some(log_message) = breakpoint.log_message {
|
||||||
|
prompt.insert_str(&log_message);
|
||||||
|
}
|
||||||
|
compositor.push(Box::new(prompt));
|
||||||
|
});
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dap_switch_thread(cx: &mut Context) {
|
||||||
|
thread_picker(cx, |editor, thread| {
|
||||||
|
block_on(select_thread_id(editor, thread.id, true));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn dap_switch_stack_frame(cx: &mut Context) {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
|
||||||
|
let thread_id = match debugger.thread_id {
|
||||||
|
Some(thread_id) => thread_id,
|
||||||
|
None => {
|
||||||
|
cx.editor.set_error("No thread is currently active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let frames = debugger.stack_frames[&thread_id].clone();
|
||||||
|
|
||||||
|
let picker = FilePicker::new(
|
||||||
|
frames,
|
||||||
|
|frame| frame.name.clone().into(), // TODO: include thread_states in the label
|
||||||
|
move |cx, frame, _action| {
|
||||||
|
let debugger = debugger!(cx.editor);
|
||||||
|
// TODO: this should be simpler to find
|
||||||
|
let pos = debugger.stack_frames[&thread_id]
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.id == frame.id);
|
||||||
|
debugger.active_frame = pos;
|
||||||
|
|
||||||
|
let frame = debugger.stack_frames[&thread_id]
|
||||||
|
.get(pos.unwrap_or(0))
|
||||||
|
.cloned();
|
||||||
|
if let Some(frame) = &frame {
|
||||||
|
jump_to_stack_frame(cx.editor, frame);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |_editor, frame| {
|
||||||
|
frame
|
||||||
|
.source
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|source| source.path.clone())
|
||||||
|
.map(|path| {
|
||||||
|
(
|
||||||
|
path,
|
||||||
|
Some((
|
||||||
|
frame.line.saturating_sub(1),
|
||||||
|
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.push_layer(Box::new(picker))
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
use crossterm::event::Event;
|
||||||
|
use helix_core::Position;
|
||||||
|
use helix_view::{
|
||||||
|
graphics::{CursorKind, Rect},
|
||||||
|
Editor,
|
||||||
|
};
|
||||||
|
use tui::buffer::Buffer;
|
||||||
|
|
||||||
|
use crate::compositor::{Component, Context, EventResult};
|
||||||
|
|
||||||
|
/// Contains a component placed in the center of the parent component
|
||||||
|
pub struct Overlay<T> {
|
||||||
|
/// Child component
|
||||||
|
pub content: T,
|
||||||
|
/// Function to compute the size and position of the child component
|
||||||
|
pub calc_child_size: Box<dyn Fn(Rect) -> Rect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom
|
||||||
|
pub fn overlayed<T>(content: T) -> Overlay<T> {
|
||||||
|
Overlay {
|
||||||
|
content,
|
||||||
|
calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect {
|
||||||
|
fn mul_and_cast(size: u16, factor: u8) -> u16 {
|
||||||
|
((size as u32) * (factor as u32) / 100).try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner_w = mul_and_cast(rect.width, percent_horizontal);
|
||||||
|
let inner_h = mul_and_cast(rect.height, percent_vertical);
|
||||||
|
|
||||||
|
let offset_x = rect.width.saturating_sub(inner_w) / 2;
|
||||||
|
let offset_y = rect.height.saturating_sub(inner_h) / 2;
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
x: rect.x + offset_x,
|
||||||
|
y: rect.y + offset_y,
|
||||||
|
width: inner_w,
|
||||||
|
height: inner_h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component + 'static> Component for Overlay<T> {
|
||||||
|
fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) {
|
||||||
|
let dimensions = (self.calc_child_size)(area);
|
||||||
|
self.content.render(dimensions, frame, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
|
||||||
|
let area = Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
let dimensions = (self.calc_child_size)(area);
|
||||||
|
let viewport = (dimensions.width, dimensions.height);
|
||||||
|
let _ = self.content.required_size(viewport)?;
|
||||||
|
Some((width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
|
||||||
|
self.content.handle_event(event, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
|
||||||
|
let dimensions = (self.calc_child_size)(area);
|
||||||
|
self.content.cursor(dimensions, ctx)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue