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;
|
||||
|
||||
fn main() {
|
||||
let git_hash = Command::new("git")
|
||||
.args(&["describe", "--dirty"])
|
||||
.args(&["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.map(|x| String::from_utf8(x.stdout).ok())
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
|
||||
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
|
||||
.and_then(|x| String::from_utf8(x.stdout).ok());
|
||||
|
||||
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