diff options
Diffstat (limited to 'helix-term/src')
-rw-r--r-- | helix-term/src/application.rs | 96 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 203 | ||||
-rw-r--r-- | helix-term/src/commands/dap.rs | 445 | ||||
-rw-r--r-- | helix-term/src/keymap.rs | 17 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 265 |
5 files changed, 1017 insertions, 9 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1fcca681..e64389ed 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,11 +1,11 @@ use helix_core::{merge_toml_values, syntax}; +use helix_dap::Payload; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{theme, Editor}; use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; use log::error; - use std::{ io::{stdout, Write}, sync::Arc, @@ -191,6 +191,9 @@ impl Application { last_render = Instant::now(); } } + Some(payload) = self.editor.debugger_events.next() => { + self.handle_debugger_message(payload).await; + } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); @@ -252,6 +255,97 @@ impl Application { } } + pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) { + use crate::commands::dap::select_thread_id; + use helix_dap::{events, Event}; + let mut debugger = match self.editor.debugger.as_mut() { + Some(debugger) => debugger, + None => return, + }; + + match payload { + Payload::Event(ev) => match ev { + Event::Stopped(events::Stopped { + thread_id, + description, + text, + reason, + all_threads_stopped, + .. + }) => { + debugger.is_running = false; + + // whichever thread stops is made "current" (if no previously selected thread). + if thread_id.is_none() || all_threads_stopped.unwrap_or_default() { + let main = debugger.threads().await.ok().and_then(|threads| { + // Workaround for debugging Go tests. Main thread has * in beginning of its name + let mut main = + threads.iter().find(|t| t.name.starts_with('*')).cloned(); + if main.is_none() { + main = threads.get(0).cloned(); + } + main.map(|t| t.id) + }); + if let Some(id) = main { + select_thread_id(&mut self.editor, id, true).await; + } + } else { + select_thread_id(&mut self.editor, thread_id.unwrap(), true).await; + } + + let scope = match thread_id { + Some(id) => format!("Thread {}", id), + None => "Target".to_owned(), + }; + + let mut status = format!("{} stopped because of {}", scope, reason); + if let Some(desc) = description { + status.push_str(&format!(" {}", desc)); + } + if let Some(text) = text { + status.push_str(&format!(" {}", text)); + } + if all_threads_stopped.unwrap_or_default() { + status.push_str(" (all threads stopped)"); + } + + self.editor.set_status(status); + } + Event::Output(events::Output { + category, output, .. + }) => { + let prefix = match category { + Some(category) => { + if &category == "telemetry" { + return; + } + format!("Debug ({}):", category) + } + None => "Debug:".to_owned(), + }; + + self.editor.set_status(format!("{} {}", prefix, output)); + } + Event::Initialized => { + self.editor + .set_status("Debugged application started".to_owned()); + } + Event::Continued(_) => { + debugger.is_running = true; + debugger.active_frame = None; + debugger.thread_id = None; + } + ev => { + log::warn!("Unhandled event {:?}", ev); + return; // return early to skip render + } + }, + Payload::Response(_) => unreachable!(), + Payload::Request(_) => todo!(), + } + self.render(); + } + pub async fn handle_language_server_message( &mut self, call: helix_lsp::Call, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 116f39bd..7b2fd295 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,3 +1,7 @@ +pub(crate) mod dap; + +pub use dap::*; + use helix_core::{ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent, indent::IndentStyle, @@ -33,7 +37,7 @@ use crate::{ use crate::job::{self, Job, Jobs}; use futures_util::FutureExt; use std::num::NonZeroUsize; -use std::{fmt, future::Future}; +use std::{collections::HashMap, fmt, future::Future}; use std::{ borrow::Cow, @@ -97,13 +101,13 @@ impl<'a> Context<'a> { } } -enum Align { +pub enum Align { Top, Center, Bottom, } -fn align_view(doc: &Document, view: &mut View, align: Align) { +pub fn align_view(doc: &Document, view: &mut View, align: Align) { let pos = doc .selection(view.id) .primary() @@ -304,6 +308,17 @@ impl Command { surround_delete, "Surround delete", select_textobject_around, "Select around object", select_textobject_inner, "Select inside object", + dap_launch, "Launch debug target", + dap_toggle_breakpoint, "Toggle breakpoint", + dap_run, "Begin program execution", + dap_continue, "Continue program execution", + dap_pause, "Pause program execution", + dap_step_in, "Step in", + dap_step_out, "Step out", + dap_next, "Step to next", + dap_variables, "List variables", + dap_terminate, "End debug session", + dap_switch_thread, "Switch current thread", shell_pipe, "Pipe selections through shell command", shell_pipe_to, "Pipe selections into shell command, ignoring command output", shell_insert_output, "Insert output of shell command before each selection", @@ -1339,7 +1354,6 @@ fn append_mode(cx: &mut Context) { mod cmd { use super::*; - use std::collections::HashMap; use helix_view::editor::Action; use ui::completers::{self, Completer}; @@ -1941,6 +1955,152 @@ mod cmd { Ok(()) } + fn debug_eval( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + use helix_lsp::block_on; + if let Some(debugger) = cx.editor.debugger.as_mut() { + let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) { + (Some(frame), Some(thread_id)) => (frame, thread_id), + _ => { + bail!("Cannot find current stack frame to access variables") + } + }; + + // TODO: support no frame_id + + let frame_id = debugger.stack_frames[&thread_id][frame].id; + let response = block_on(debugger.eval(args.join(" "), Some(frame_id)))?; + cx.editor.set_status(response.result); + } + Ok(()) + } + + fn edit_breakpoint_impl( + cx: &mut compositor::Context, + condition: Option<String>, + log_message: Option<String>, + ) -> anyhow::Result<()> { + use helix_lsp::block_on; + + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let pos = doc.selection(view.id).primary().cursor(text); + let breakpoint = helix_dap::SourceBreakpoint { + line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init) + condition, + log_message, + ..Default::default() + }; + let path = match doc.path() { + Some(path) => path.to_path_buf(), + None => { + bail!("Can't edit breakpoint: document has no path") + } + }; + if let Some(debugger) = &mut cx.editor.debugger { + 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 breakpoints = debugger.breakpoints.entry(path.clone()).or_default(); + if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) { + breakpoints.remove(pos); + breakpoints.push(breakpoint); + + let breakpoints = breakpoints.clone(); + + let request = debugger.set_breakpoints(path, breakpoints); + if let Err(e) = block_on(request) { + bail!("Failed to set breakpoints: {:?}", e) + } + } + } + Ok(()) + } + + fn debug_start( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let mut args = args.to_owned(); + let name = match args.len() { + 0 => None, + _ => Some(args.remove(0)), + }; + dap_start_impl(&mut cx.editor, name, None, Some(args)); + Ok(()) + } + + fn debug_remote( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let mut args = args.to_owned(); + let address = match args.len() { + 0 => None, + _ => Some(args.remove(0).parse()?), + }; + let name = match args.len() { + 0 => None, + _ => Some(args.remove(0)), + }; + dap_start_impl(&mut cx.editor, name, address, Some(args)); + + Ok(()) + } + + fn debug_breakpoint_condition( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let condition = args.join(" "); + let condition = if condition.is_empty() { + None + } else { + Some(condition) + }; + + edit_breakpoint_impl(cx, condition, None) + } + + fn debug_set_logpoint( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let log_message = args.join(" "); + let log_message = if log_message.is_empty() { + None + } else { + Some(log_message) + }; + + edit_breakpoint_impl(cx, None, log_message) + } + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2181,6 +2341,41 @@ mod cmd { completer: None, }, TypableCommand { + name: "debug-start", + alias: Some("dbg"), + doc: "Start a debug session from a given template with given parameters.", + fun: debug_start, + completer: Some(completers::filename), + }, + TypableCommand { + name: "debug-remote", + alias: Some("dbg-tcp"), + doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.", + fun: debug_remote, + completer: Some(completers::filename), + }, + TypableCommand { + name: "debug-eval", + alias: None, + doc: "Evaluate expression in current debug context.", + fun: debug_eval, + completer: None, + }, + TypableCommand { + name: "debug-breakpoint-condition", + alias: None, + doc: "Set current breakpoint condition.", + fun: debug_breakpoint_condition, + completer: None, + }, + TypableCommand { + name: "debug-set-logpoint", + alias: None, + doc: "Make current breakpoint a log point.", + fun: debug_set_logpoint, + completer: None, + }, + TypableCommand { name: "vsplit", alias: Some("vs"), doc: "Open the file in a vertical split.", diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs new file mode 100644 index 00000000..a1558dde --- /dev/null +++ b/helix-term/src/commands/dap.rs @@ -0,0 +1,445 @@ +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 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 + + if let Some(debugger) = &mut cx.editor.debugger { + let breakpoints = debugger.breakpoints.entry(path.clone()).or_default(); + if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) { + breakpoints.remove(pos); + } else { + breakpoints.push(breakpoint); + } + + let breakpoints = breakpoints.clone(); + + let request = debugger.set_breakpoints(path, breakpoints); + if let Err(e) = block_on(request) { + cx.editor + .set_error(format!("Failed to set breakpoints: {:?}", e)); + } + } +} + +pub fn dap_run(cx: &mut Context) { + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + let request = debugger.configuration_done(); + 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) { + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + if let Some(thread_id) = debugger.thread_id { + let request = debugger.continue_thread(debugger.thread_id.unwrap()); + if let Err(e) = block_on(request) { + cx.editor.set_error(format!("Failed to continue: {:?}", e)); + return; + } + debugger.is_running = true; + 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) { + if let Some(debugger) = &mut cx.editor.debugger { + if !debugger.is_running { + cx.editor.set_status("Debuggee is not running".to_owned()); + return; + } + + // FIXME: correct number here + let request = debugger.pause(0); + if let Err(e) = block_on(request) { + cx.editor.set_error(format!("Failed to pause: {:?}", e)); + } + } +} + +pub fn dap_step_in(cx: &mut Context) { + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.step_in(debugger.thread_id.unwrap()); + if let Err(e) = block_on(request) { + cx.editor.set_error(format!("Failed to step: {:?}", e)); + } + } +} + +pub fn dap_step_out(cx: &mut Context) { + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.step_out(debugger.thread_id.unwrap()); + if let Err(e) = block_on(request) { + cx.editor.set_error(format!("Failed to step: {:?}", e)); + } + } +} + +pub fn dap_next(cx: &mut Context) { + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Debuggee is already running".to_owned()); + return; + } + + let request = debugger.next(debugger.thread_id.unwrap()); + if let Err(e) = block_on(request) { + cx.editor.set_error(format!("Failed to step: {:?}", e)); + } + } +} + +pub fn dap_variables(cx: &mut Context) { + if let Some(debugger) = &mut cx.editor.debugger { + if debugger.is_running { + cx.editor + .set_status("Cannot access variables while target is running".to_owned()); + return; + } + 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 { + 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) { + if let Some(debugger) = &mut cx.editor.debugger { + 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) { + if let Some(debugger) = &mut cx.editor.debugger { + 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)) + } +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 71ac01a9..1b04b5cd 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -487,6 +487,23 @@ impl Default for Keymaps { "s" => symbol_picker, "a" => code_action, "'" => last_picker, + "d" => { "Debug" + "l" => dap_launch, + "b" => dap_toggle_breakpoint, + "r" => dap_run, + "c" => dap_continue, + "h" => dap_pause, + "i" => dap_step_in, + "o" => dap_step_out, + "n" => dap_next, + "v" => dap_variables, + "t" => dap_terminate, + "s" => { "Switch" + "t" => dap_switch_thread, + // f = stack frame + // sl, sb + }, + }, "w" => { "Window" "C-w" | "w" => rotate_view, "C-h" | "h" => hsplit, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4b9c56e7..17a2df3d 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,6 +1,7 @@ use crate::{ commands, - compositor::{Component, Context, EventResult}, + compositor::{Component, Compositor, Context, EventResult}, + job::Callback, key, keymap::{KeymapResult, Keymaps}, ui::{Completion, ProgressSpinners}, @@ -10,11 +11,12 @@ use helix_core::{ coords_at_pos, graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, - syntax::{self, HighlightEvent}, + syntax::{self, DebugConfigCompletion, HighlightEvent}, unicode::segmentation::UnicodeSegmentation, unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, }; +use helix_dap::{SourceBreakpoint, StackFrame}; use helix_view::{ document::Mode, editor::LineNumber, @@ -29,6 +31,8 @@ use std::borrow::Cow; use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; use tui::buffer::Buffer as Surface; +use super::{Prompt, PromptEvent}; + pub struct EditorView { keymaps: Keymaps, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, @@ -71,6 +75,7 @@ impl EditorView { is_focused: bool, loader: &syntax::Loader, config: &helix_view::editor::Config, + debugger: &Option<helix_dap::Client>, ) { let inner = view.inner_area(); let area = view.area; @@ -87,7 +92,9 @@ impl EditorView { }; Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights); - Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config); + Self::render_gutter( + doc, view, view.area, surface, theme, is_focused, config, debugger, + ); if is_focused { Self::render_focused_view_elements(view, doc, inner, theme, surface); @@ -106,7 +113,7 @@ impl EditorView { } } - self.render_diagnostics(doc, view, inner, surface, theme); + self.render_diagnostics(doc, view, inner, surface, theme, debugger); let statusline_area = view .area @@ -409,6 +416,7 @@ impl EditorView { theme: &Theme, is_focused: bool, config: &helix_view::editor::Config, + debugger: &Option<helix_dap::Client>, ) { let text = doc.text().slice(..); let last_line = view.last_line(doc); @@ -438,6 +446,32 @@ impl EditorView { .map(|range| range.cursor_line(text)) .collect(); + let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None; + let mut stack_frame: Option<&StackFrame> = None; + if let Some(debugger) = debugger { + if let Some(path) = doc.path() { + breakpoints = debugger.breakpoints.get(path); + + // if we have a frame, and the frame path matches document + if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) + { + let frame = debugger + .stack_frames + .get(&thread_id) + .and_then(|bt| bt.get(frame)); // TODO: drop the clone.. + if let Some(StackFrame { + source: Some(source), + .. + }) = &frame + { + if source.path.as_ref() == Some(path) { + stack_frame = frame; + } + }; + }; + } + } + for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { use helix_core::diagnostic::Severity; if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) { @@ -457,6 +491,30 @@ impl EditorView { let selected = cursors.contains(&line); + if let Some(bps) = breakpoints.as_ref() { + if let Some(breakpoint) = bps.iter().find(|breakpoint| breakpoint.line - 1 == line) + { + let style = if breakpoint.condition.is_some() { + error + } else if breakpoint.log_message.is_some() { + info + } else { + warning + }; + surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style); + } + } + + if let Some(frame) = stack_frame { + if frame.line - 1 == line { + surface.set_style( + Rect::new(viewport.x, viewport.y + i as u16, 6, 1), + helix_view::graphics::Style::default() + .bg(helix_view::graphics::Color::LightYellow), + ); + } + } + let text = if line == last_line && !draw_last { " ~".into() } else { @@ -493,6 +551,7 @@ impl EditorView { viewport: Rect, surface: &mut Surface, theme: &Theme, + debugger: &Option<helix_dap::Client>, ) { use helix_core::diagnostic::Severity; use tui::{ @@ -530,6 +589,31 @@ impl EditorView { lines.extend(text.lines); } + if let Some(debugger) = debugger { + if let Some(path) = doc.path() { + if let Some(breakpoints) = debugger.breakpoints.get(path) { + let line = doc.text().char_to_line(cursor); + if let Some(breakpoint) = breakpoints + .iter() + .find(|breakpoint| breakpoint.line - 1 == line) + { + if let Some(condition) = &breakpoint.condition { + lines.extend( + Text::styled(condition, info.add_modifier(Modifier::UNDERLINED)) + .lines, + ); + } + if let Some(log_message) = &breakpoint.log_message { + lines.extend( + Text::styled(log_message, info.add_modifier(Modifier::UNDERLINED)) + .lines, + ); + } + } + } + } + } + let paragraph = Paragraph::new(lines).alignment(Alignment::Right); let width = 80.min(viewport.width); let height = 15.min(viewport.height); @@ -636,6 +720,78 @@ impl EditorView { ); } + fn debug_parameter_prompt( + completions: Vec<DebugConfigCompletion>, + config_name: String, + mut params: Vec<String>, + ) -> Prompt { + let i = params.len(); + let completion = completions.get(i).unwrap(); + let field_type = if let DebugConfigCompletion::Advanced(cfg) = completion { + cfg.completion.clone().unwrap_or_else(|| "".to_owned()) + } else { + "".to_owned() + }; + let name = match completion { + DebugConfigCompletion::Advanced(cfg) => { + cfg.name.clone().unwrap_or_else(|| field_type.to_owned()) + } + DebugConfigCompletion::Named(name) => name.clone(), + }; + let default_val = match completion { + DebugConfigCompletion::Advanced(cfg) => { + cfg.default.clone().unwrap_or_else(|| "".to_owned()) + } + _ => "".to_owned(), + }; + + let noop = |_input: &str| Vec::new(); + let completer = match &field_type[..] { + "filename" => super::completers::filename, + "directory" => super::completers::directory, + _ => noop, + }; + Prompt::new( + format!("{}: ", name).into(), + None, + completer, + move |cx: &mut crate::compositor::Context, 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 = + Self::debug_parameter_prompt(completions, config_name, params); + compositor.push(Box::new(prompt)); + }); + Ok(call) + }); + cx.jobs.callback(callback); + } else { + commands::dap_start_impl( + cx.editor, + Some(&config_name), + None, + Some(params.iter().map(|x| x.as_str()).collect()), + ); + } + }, + ) + } + /// Handle events by looking them up in `self.keymaps`. Returns None /// if event was handled (a command was executed or a subkeymap was /// activated). Only KeymapResult::{NotFound, Cancelled} is returned @@ -647,6 +803,57 @@ impl EditorView { event: KeyEvent, ) -> Option<KeymapResult> { self.autoinfo = None; + + if let Some(picker) = cxt.editor.debug_config_picker.clone() { + match event { + KeyEvent { + code: KeyCode::Esc, .. + } => {} + KeyEvent { + code: KeyCode::Char(char), + .. + } => { + let (i, name) = match picker.iter().position(|t| t.starts_with(char)) { + Some(pos) => (pos, picker.get(pos).unwrap().clone()), + None => return None, + }; + let completions = cxt.editor.debug_config_completions.clone().unwrap(); + let completion = completions.get(i).unwrap().clone(); + if !completion.is_empty() { + let prompt = Self::debug_parameter_prompt(completion, name, Vec::new()); + cxt.push_layer(Box::new(prompt)); + } + } + _ => return None, + } + cxt.editor.debug_config_picker = None; + return None; + } + + if cxt.editor.variables.is_some() { + match event { + KeyEvent { + code: KeyCode::Char('h'), + .. + } => { + cxt.editor.variables_page = cxt.editor.variables_page.saturating_sub(1); + } + KeyEvent { + code: KeyCode::Char('l'), + .. + } => { + cxt.editor.variables_page = cxt.editor.variables_page.saturating_add(1); + } + KeyEvent { + code: KeyCode::Esc, .. + } => { + cxt.editor.variables = None; + } + _ => {} + } + return None; + } + match self.keymaps.get_mut(&mode).unwrap().get(event) { KeymapResult::Matched(command) => command.execute(cxt), KeymapResult::Pending(node) => self.autoinfo = Some(node.into()), @@ -1016,9 +1223,59 @@ impl Component for EditorView { is_focused, loader, &cx.editor.config, + &cx.editor.debugger, ); } + if let Some(ref vars) = cx.editor.variables { + let mut text = String::new(); + let mut height = 0; + let mut max_len = 20; + + let per_page = 15; + let num_vars = vars.len(); + let start = (per_page * cx.editor.variables_page).min(num_vars); + let end = (start + per_page).min(num_vars); + for line in vars[start..end].to_vec() { + max_len = max_len.max(line.len() as u16); + height += 1; + text.push_str(&line); + } + + if vars.len() > per_page { + text += "\nMove h, l"; + height += 1; + } + + let mut info = Info { + height: 20.min(height + 2), + width: 70.min(max_len), + title: format!("{} variables", num_vars), + text: text + "\nExit Esc", + }; + info.render(area, surface, cx); + } + + if let Some(ref configs) = cx.editor.debug_config_picker { + let mut text = String::new(); + let mut height = 0; + let mut max_len = 20; + + for line in configs { + max_len = max_len.max(line.len() as u16 + 2); + height += 1; + text.push_str(&format!("{} {}\n", line.chars().next().unwrap(), line)); + } + + let mut info = Info { + height: 20.min(height + 1), + width: 70.min(max_len), + title: "Debug targets".to_owned(), + text: text + "Exit Esc", + }; + info.render(area, surface, cx); + } + if let Some(ref mut info) = self.autoinfo { info.render(area, surface, cx); } |