mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
483 lines
14 KiB
Rust
483 lines
14 KiB
Rust
use super::{align_view, Align, Context, Editor};
|
|
use crate::ui::Picker;
|
|
use helix_core::Selection;
|
|
use helix_dap::Client;
|
|
use helix_lsp::block_on;
|
|
|
|
use serde_json::{to_value, Value};
|
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
// 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;
|
|
// TODO: this is probably utf-16 offsets
|
|
Some(pos)
|
|
}
|
|
|
|
pub fn resume_application(debugger: &mut Client) {
|
|
debugger.is_running = true; // TODO: set state running for debugger.thread_id
|
|
debugger.active_frame = None;
|
|
debugger.thread_id = None;
|
|
}
|
|
|
|
pub async fn select_thread_id(editor: &mut Editor, thread_id: usize, force: bool) {
|
|
let debugger = match &mut editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if !force && debugger.thread_id.is_some() {
|
|
return;
|
|
}
|
|
|
|
debugger.thread_id = Some(thread_id);
|
|
|
|
// fetch stack trace
|
|
// TODO: handle requesting more total frames
|
|
let (frames, _) = debugger.stack_trace(thread_id).await.unwrap();
|
|
debugger.stack_frames.insert(thread_id, frames);
|
|
debugger.active_frame = Some(0); // TODO: check how to determine this
|
|
|
|
let frame = debugger.stack_frames[&thread_id].get(0).cloned();
|
|
if let Some(frame) = &frame {
|
|
jump_to_stack_frame(editor, frame);
|
|
}
|
|
}
|
|
|
|
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;
|
|
};
|
|
|
|
editor
|
|
.open(path, helix_view::editor::Action::Replace)
|
|
.unwrap(); // TODO: there should be no unwrapping!
|
|
|
|
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);
|
|
}
|
|
|
|
// DAP
|
|
pub fn dap_start_impl(
|
|
editor: &mut Editor,
|
|
name: Option<&str>,
|
|
socket: Option<std::net::SocketAddr>,
|
|
params: Option<Vec<&str>>,
|
|
) {
|
|
let (_, doc) = current!(editor);
|
|
|
|
let path = match doc.path() {
|
|
Some(path) => path.to_path_buf(),
|
|
None => {
|
|
editor.set_error("Can't start debug: document has no path".to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
let config = editor
|
|
.syn_loader
|
|
.language_config_for_file_name(&path)
|
|
.and_then(|x| x.debugger.clone());
|
|
let config = match config {
|
|
Some(c) => c,
|
|
None => {
|
|
editor.set_error(
|
|
"Can't start debug: no debug adapter available for language".to_string(),
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let result = match socket {
|
|
Some(socket) => block_on(Client::tcp(socket, 0)),
|
|
None => block_on(Client::process(
|
|
config.transport.clone(),
|
|
config.command.clone(),
|
|
config.args.clone(),
|
|
config.port_arg.clone(),
|
|
0,
|
|
)),
|
|
};
|
|
|
|
let (mut debugger, events) = match result {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
editor.set_error(format!("Failed to start debug session: {:?}", e));
|
|
return;
|
|
}
|
|
};
|
|
|
|
let request = debugger.initialize(config.name.clone());
|
|
if let Err(e) = block_on(request) {
|
|
editor.set_error(format!("Failed to initialize debug adapter: {:?}", e));
|
|
return;
|
|
}
|
|
|
|
let start_config = match name {
|
|
Some(name) => config.templates.iter().find(|t| t.name == name),
|
|
None => config.templates.get(0),
|
|
};
|
|
let start_config = match start_config {
|
|
Some(c) => c,
|
|
None => {
|
|
editor.set_error("Can't start debug: no debug config with given name".to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
let template = start_config.args.clone();
|
|
let mut args: HashMap<String, Value> = HashMap::new();
|
|
|
|
if let Some(params) = params {
|
|
for (k, t) in template {
|
|
let mut value = t;
|
|
for (i, x) in params.iter().enumerate() {
|
|
// For param #0 replace {0} in args
|
|
value = value.replace(format!("{{{}}}", i).as_str(), x);
|
|
}
|
|
|
|
if let Ok(integer) = value.parse::<usize>() {
|
|
args.insert(k, Value::Number(serde_json::Number::from(integer)));
|
|
} else {
|
|
args.insert(k, Value::String(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
let args = to_value(args).unwrap();
|
|
|
|
let result = match &start_config.request[..] {
|
|
"launch" => block_on(debugger.launch(args)),
|
|
"attach" => block_on(debugger.attach(args)),
|
|
_ => {
|
|
editor.set_error("Unsupported request".to_string());
|
|
return;
|
|
}
|
|
};
|
|
if let Err(e) = result {
|
|
editor.set_error(format!("Failed {} target: {:?}", start_config.request, e));
|
|
return;
|
|
}
|
|
|
|
// TODO: either await "initialized" or buffer commands until event is received
|
|
editor.debugger = Some(debugger);
|
|
let stream = UnboundedReceiverStream::new(events);
|
|
editor.debugger_events.push(stream);
|
|
}
|
|
|
|
pub fn dap_launch(cx: &mut Context) {
|
|
if cx.editor.debugger.is_some() {
|
|
cx.editor
|
|
.set_error("Can't start debug: debugger is running".to_string());
|
|
return;
|
|
}
|
|
|
|
let (_, doc) = current!(cx.editor);
|
|
let path = match doc.path() {
|
|
Some(path) => path.to_path_buf(),
|
|
None => {
|
|
cx.editor
|
|
.set_error("Can't start debug: document has no path".to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
let config = cx
|
|
.editor
|
|
.syn_loader
|
|
.language_config_for_file_name(&path)
|
|
.and_then(|x| x.debugger.clone());
|
|
let config = match config {
|
|
Some(c) => c,
|
|
None => {
|
|
cx.editor.set_error(
|
|
"Can't start debug: no debug adapter available for language".to_string(),
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
cx.editor.debug_config_picker = Some(config.templates.iter().map(|t| t.name.clone()).collect());
|
|
cx.editor.debug_config_completions = Some(
|
|
config
|
|
.templates
|
|
.iter()
|
|
.map(|t| t.completion.clone())
|
|
.collect(),
|
|
);
|
|
}
|
|
|
|
pub fn dap_toggle_breakpoint(cx: &mut Context) {
|
|
let (view, doc) = current!(cx.editor);
|
|
let text = doc.text().slice(..);
|
|
let pos = doc.selection(view.id).primary().cursor(text);
|
|
|
|
let breakpoint = helix_dap::SourceBreakpoint {
|
|
line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
|
|
..Default::default()
|
|
};
|
|
|
|
let path = match doc.path() {
|
|
Some(path) => path.to_path_buf(),
|
|
None => {
|
|
cx.editor
|
|
.set_error("Can't set breakpoint: document has no path".to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
// TODO: need to map breakpoints over edits and update them?
|
|
// we shouldn't really allow editing while debug is running though
|
|
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
let breakpoints = debugger.breakpoints.entry(path.clone()).or_default();
|
|
if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) {
|
|
breakpoints.remove(pos);
|
|
} else {
|
|
breakpoints.push(breakpoint);
|
|
}
|
|
|
|
let breakpoints = breakpoints.clone();
|
|
|
|
let request = debugger.set_breakpoints(path, breakpoints);
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor
|
|
.set_error(format!("Failed to set breakpoints: {:?}", e));
|
|
}
|
|
}
|
|
|
|
pub fn dap_run(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if debugger.is_running {
|
|
cx.editor
|
|
.set_status("Debuggee is already running".to_owned());
|
|
return;
|
|
}
|
|
let request = debugger.configuration_done();
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor.set_error(format!("Failed to run: {:?}", e));
|
|
return;
|
|
}
|
|
debugger.is_running = true;
|
|
}
|
|
|
|
pub fn dap_continue(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
let request = debugger.continue_thread(thread_id);
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor.set_error(format!("Failed to continue: {:?}", e));
|
|
return;
|
|
}
|
|
resume_application(debugger);
|
|
debugger.stack_frames.remove(&thread_id);
|
|
} else {
|
|
cx.editor
|
|
.set_error("Currently active thread is not stopped. Switch the thread.".into());
|
|
}
|
|
}
|
|
|
|
pub fn dap_pause(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if !debugger.is_running {
|
|
cx.editor.set_status("Debuggee is not running".to_owned());
|
|
return;
|
|
}
|
|
|
|
// FIXME: correct number here
|
|
let request = debugger.pause(0);
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor.set_error(format!("Failed to pause: {:?}", e));
|
|
}
|
|
}
|
|
|
|
pub fn dap_step_in(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
let request = debugger.step_in(thread_id);
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor.set_error(format!("Failed to continue: {:?}", e));
|
|
return;
|
|
}
|
|
resume_application(debugger);
|
|
debugger.stack_frames.remove(&thread_id);
|
|
} else {
|
|
cx.editor
|
|
.set_error("Currently active thread is not stopped. Switch the thread.".into());
|
|
}
|
|
}
|
|
|
|
pub fn dap_step_out(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
let request = debugger.step_out(thread_id);
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor.set_error(format!("Failed to continue: {:?}", e));
|
|
return;
|
|
}
|
|
resume_application(debugger);
|
|
debugger.stack_frames.remove(&thread_id);
|
|
} else {
|
|
cx.editor
|
|
.set_error("Currently active thread is not stopped. Switch the thread.".into());
|
|
}
|
|
}
|
|
|
|
pub fn dap_next(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
let request = debugger.next(thread_id);
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor.set_error(format!("Failed to continue: {:?}", e));
|
|
return;
|
|
}
|
|
resume_application(debugger);
|
|
debugger.stack_frames.remove(&thread_id);
|
|
} else {
|
|
cx.editor
|
|
.set_error("Currently active thread is not stopped. Switch the thread.".into());
|
|
}
|
|
}
|
|
|
|
pub fn dap_variables(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
if debugger.is_running {
|
|
cx.editor
|
|
.set_status("Cannot access variables while target is running".to_owned());
|
|
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".to_owned());
|
|
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;
|
|
}
|
|
};
|
|
let mut variables = Vec::new();
|
|
|
|
for scope in scopes.iter() {
|
|
let response = block_on(debugger.variables(scope.variables_reference));
|
|
|
|
if let Ok(vars) = response {
|
|
variables.reserve(vars.len());
|
|
for var in vars {
|
|
let prefix = match var.data_type {
|
|
Some(data_type) => format!("{} ", data_type),
|
|
None => "".to_owned(),
|
|
};
|
|
variables.push(format!("{}{} = {}\n", prefix, var.name, var.value));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !variables.is_empty() {
|
|
cx.editor.variables = Some(variables);
|
|
cx.editor.variables_page = 0;
|
|
}
|
|
}
|
|
|
|
pub fn dap_terminate(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
let request = debugger.disconnect();
|
|
if let Err(e) = block_on(request) {
|
|
cx.editor
|
|
.set_error(format!("Failed to disconnect: {:?}", e));
|
|
return;
|
|
}
|
|
cx.editor.debugger = None;
|
|
}
|
|
|
|
pub fn dap_switch_thread(cx: &mut Context) {
|
|
let debugger = match &mut cx.editor.debugger {
|
|
Some(debugger) => debugger,
|
|
None => return,
|
|
};
|
|
|
|
let request = debugger.threads();
|
|
let threads = match block_on(request) {
|
|
Ok(threads) => threads,
|
|
Err(e) => {
|
|
cx.editor
|
|
.set_error(format!("Failed to retrieve threads: {:?}", e));
|
|
return;
|
|
}
|
|
};
|
|
|
|
let picker = Picker::new(
|
|
true,
|
|
threads,
|
|
|thread| thread.name.clone().into(),
|
|
|editor, thread, _action| {
|
|
block_on(select_thread_id(editor, thread.id, true));
|
|
},
|
|
);
|
|
cx.push_layer(Box::new(picker))
|
|
}
|