diff options
Diffstat (limited to 'helix-term')
-rw-r--r-- | helix-term/Cargo.toml | 4 | ||||
-rw-r--r-- | helix-term/src/application.rs | 169 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 159 | ||||
-rw-r--r-- | helix-term/src/commands/dap.rs | 852 | ||||
-rw-r--r-- | helix-term/src/keymap.rs | 20 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 203 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 7 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 13 | ||||
-rw-r--r-- | helix-term/src/ui/prompt.rs | 3 |
9 files changed, 1389 insertions, 41 deletions
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index a0079feb..43268291 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -24,6 +24,7 @@ path = "src/main.rs" helix-core = { version = "0.5", path = "../helix-core" } helix-view = { version = "0.5", path = "../helix-view" } helix-lsp = { version = "0.5", path = "../helix-lsp" } +helix-dap = { version = "0.5", path = "../helix-dap" } anyhow = "1" once_cell = "1.8" @@ -33,7 +34,7 @@ num_cpus = "1" tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } crossterm = { version = "0.22", features = ["event-stream"] } signal-hook = "0.3" - +tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } # Logging @@ -58,7 +59,6 @@ serde = { version = "1.0", features = ["derive"] } # ripgrep for global search grep-regex = "0.1.9" grep-searcher = "0.1.8" -tokio-stream = "0.1.8" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 78b93cd9..242dc837 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,11 +1,13 @@ 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 crate::{ + args::Args, commands::fetch_stack_trace, compositor::Compositor, config::Config, job::Jobs, ui, +}; use log::{error, warn}; - use std::{ io::{stdin, stdout, Write}, sync::Arc, @@ -219,6 +221,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(); @@ -313,6 +318,166 @@ impl Application { } } + pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) { + use crate::commands::dap::{resume_application, select_thread_id}; + use helix_dap::{events, Event}; + let 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, + .. + }) => { + let all_threads_stopped = all_threads_stopped.unwrap_or_default(); + + if all_threads_stopped { + if let Ok(threads) = debugger.threads().await { + for thread in threads { + fetch_stack_trace(debugger, thread.id).await; + } + select_thread_id( + &mut self.editor, + thread_id.unwrap_or_default(), + false, + ) + .await; + } + } else if let Some(thread_id) = thread_id { + debugger.thread_states.insert(thread_id, reason.clone()); // TODO: dap uses "type" || "reason" here + + // whichever thread stops is made "current" (if no previously selected thread). + select_thread_id(&mut self.editor, thread_id, false).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 { + status.push_str(" (all threads stopped)"); + } + + self.editor.set_status(status); + } + Event::Continued(events::Continued { thread_id, .. }) => { + debugger + .thread_states + .insert(thread_id, "running".to_owned()); + if debugger.thread_id == Some(thread_id) { + resume_application(debugger) + } + } + Event::Thread(_) => { + // TODO: update thread_states, make threads request + } + Event::Breakpoint(events::Breakpoint { reason, breakpoint }) => match &reason[..] { + "new" => { + debugger.breakpoints.push(breakpoint); + } + "changed" => { + match debugger + .breakpoints + .iter() + .position(|b| b.id == breakpoint.id) + { + Some(i) => { + let item = debugger.breakpoints.get_mut(i).unwrap(); + item.verified = breakpoint.verified; + // TODO: wasteful clones + item.message = breakpoint.message.or_else(|| item.message.clone()); + item.source = breakpoint.source.or_else(|| item.source.clone()); + item.line = breakpoint.line.or(item.line); + item.column = breakpoint.column.or(item.column); + item.end_line = breakpoint.end_line.or(item.end_line); + item.end_column = breakpoint.end_column.or(item.end_column); + item.instruction_reference = breakpoint + .instruction_reference + .or_else(|| item.instruction_reference.clone()); + item.offset = breakpoint.offset.or(item.offset); + } + None => { + warn!("Changed breakpoint with id {:?} not found", breakpoint.id); + } + } + } + "removed" => { + match debugger + .breakpoints + .iter() + .position(|b| b.id == breakpoint.id) + { + Some(i) => { + debugger.breakpoints.remove(i); + } + None => { + warn!("Removed breakpoint with id {:?} not found", breakpoint.id); + } + } + } + reason => { + warn!("Unknown breakpoint event: {}", reason); + } + }, + Event::Output(events::Output { + category, output, .. + }) => { + let prefix = match category { + Some(category) => { + if &category == "telemetry" { + return; + } + format!("Debug ({}):", category) + } + None => "Debug:".to_owned(), + }; + + log::info!("{}", output); + self.editor.set_status(format!("{} {}", prefix, output)); + } + Event::Initialized => { + // send existing breakpoints + for (path, breakpoints) in &self.editor.breakpoints { + // TODO: call futures in parallel, await all + debugger.breakpoints = debugger + .set_breakpoints(path.clone(), breakpoints.clone()) + .await + .unwrap() + .unwrap(); + } + // TODO: fetch breakpoints (in case we're attaching) + + if debugger.configuration_done().await.is_ok() { + self.editor + .set_status("Debugged application started".to_owned()); + }; // TODO: do we need to handle error? + } + 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 431265cd..56b31b67 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, history::UndoKind, @@ -42,7 +46,7 @@ use crate::{ use crate::job::{self, Job, Jobs}; use futures_util::{FutureExt, StreamExt}; use std::num::NonZeroUsize; -use std::{fmt, future::Future}; +use std::{collections::HashMap, fmt, future::Future}; use std::{ borrow::Cow, @@ -111,13 +115,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() @@ -349,6 +353,21 @@ 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_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_edit_condition, "Edit condition of the breakpoint on the current line", + dap_edit_log, "Edit log message of the breakpoint on the current line", + dap_switch_thread, "Switch current thread", + dap_switch_stack_frame, "Switch stack frame", + dap_enable_exceptions, "Enable exception breakpoints", + dap_disable_exceptions, "Disable exception breakpoints", 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", @@ -1537,11 +1556,11 @@ fn global_search(cx: &mut Context) { relative_path.into() } }, - move |editor: &mut Editor, (line_num, path), action| { - match editor.open(path.into(), action) { + move |cx, (line_num, path), action| { + match cx.editor.open(path.into(), action) { Ok(_) => {} Err(e) => { - editor.set_error(format!( + cx.editor.set_error(format!( "Failed to open file '{}': {}", path.display(), e @@ -1551,7 +1570,7 @@ fn global_search(cx: &mut Context) { } let line_num = *line_num; - let (view, doc) = current!(editor); + let (view, doc) = current!(cx.editor); let text = doc.text(); let start = text.line_to_char(line_num); let end = text.line_to_char((line_num + 1).min(text.len_lines())); @@ -1722,8 +1741,8 @@ fn append_mode(cx: &mut Context) { mod cmd { use super::*; - use std::collections::HashMap; + use helix_dap::SourceBreakpoint; use helix_view::editor::Action; use ui::completers::{self, Completer}; @@ -2362,6 +2381,79 @@ mod cmd { Ok(()) } + fn debug_eval( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + 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(()) + } + + pub fn get_breakpoint_at_current_line( + editor: &mut Editor, + ) -> Option<(usize, SourceBreakpoint)> { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let pos = doc.selection(view.id).primary().cursor(text); + let line = text.char_to_line(pos) + 1; // 1-indexing in DAP, 0-indexing in Helix + let path = match doc.path() { + Some(path) => path, + None => return None, + }; + editor.breakpoints.get(path).and_then(|breakpoints| { + let i = breakpoints.iter().position(|b| b.line == line); + i.map(|i| (i, breakpoints[i].clone())) + }) + } + + 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 tutor( cx: &mut compositor::Context, _args: &[&str], @@ -2635,6 +2727,27 @@ mod cmd { completer: None, }, TypableCommand { + name: "debug-start", + aliases: &["dbg"], + doc: "Start a debug session from a given template with given parameters.", + fun: debug_start, + completer: None, + }, + TypableCommand { + name: "debug-remote", + aliases: &["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: None, + }, + TypableCommand { + name: "debug-eval", + aliases: &[], + doc: "Evaluate expression in current debug context.", + fun: debug_eval, + completer: None, + }, + TypableCommand { name: "vsplit", aliases: &["vs"], doc: "Open the file in a vertical split.", @@ -2726,6 +2839,7 @@ fn command_mode(cx: &mut Context) { .set_error(format!("no such command: '{}'", parts[0])); }; }, + None, ); prompt.doc_fn = Box::new(|input: &str| { let part = input.split(' ').next().unwrap_or_default(); @@ -2798,8 +2912,8 @@ fn buffer_picker(cx: &mut Context) { .map(|(_, doc)| new_meta(doc)) .collect(), BufferMeta::format, - |editor: &mut Editor, meta, _action| { - editor.switch(meta.id, Action::Replace); + |cx, meta, _action| { + cx.editor.switch(meta.id, Action::Replace); }, |editor, meta| { let doc = &editor.documents.get(&meta.id)?; @@ -2866,9 +2980,9 @@ fn symbol_picker(cx: &mut Context) { let mut picker = FilePicker::new( symbols, |symbol| (&symbol.name).into(), - move |editor: &mut Editor, symbol, _action| { - push_jump(editor); - let (view, doc) = current!(editor); + move |cx, symbol, _action| { + push_jump(cx.editor); + let (view, doc) = current!(cx.editor); if let Some(range) = lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) @@ -2927,10 +3041,10 @@ fn workspace_symbol_picker(cx: &mut Context) { format!("{} ({})", &symbol.name, relative_path).into() } }, - move |editor: &mut Editor, symbol, action| { + move |cx, symbol, action| { let path = symbol.location.uri.to_file_path().unwrap(); - editor.open(path, action).expect("editor.open failed"); - let (view, doc) = current!(editor); + cx.editor.open(path, action).expect("editor.open failed"); + let (view, doc) = current!(cx.editor); if let Some(range) = lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) @@ -2989,15 +3103,15 @@ pub fn code_action(cx: &mut Context) { } lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), }, - move |editor, code_action, _action| match code_action { + move |cx, code_action, _action| match code_action { lsp::CodeActionOrCommand::Command(command) => { log::debug!("code action command: {:?}", command); - editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + cx.editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); } lsp::CodeActionOrCommand::CodeAction(code_action) => { log::debug!("code action: {:?}", code_action); if let Some(ref workspace_edit) = code_action.edit { - apply_workspace_edit(editor, offset_encoding, workspace_edit) + apply_workspace_edit(cx.editor, offset_encoding, workspace_edit) } } }, @@ -3504,9 +3618,7 @@ fn goto_impl( let line = location.range.start.line; format!("{}:{}", file, line).into() }, - move |editor: &mut Editor, location, action| { - jump_to(editor, location, offset_encoding, action) - }, + move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action), |_editor, location| { let path = location.uri.to_file_path().unwrap(); let line = Some(( @@ -5328,6 +5440,7 @@ fn shell_keep_pipe(cx: &mut Context) { let index = index.unwrap_or_else(|| ranges.len() - 1); doc.set_selection(view.id, Selection::new(ranges, index)); }, + None, ); cx.push_layer(Box::new(prompt)); @@ -5431,6 +5544,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { // make sure cursor is in view and update scroll as well view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); }, + None, ); cx.push_layer(Box::new(prompt)); @@ -5508,6 +5622,7 @@ fn rename_symbol(cx: &mut Context) { log::debug!("Edits from LSP: {:?}", edits); apply_workspace_edit(&mut cx.editor, offset_encoding, &edits); }, + None, ); cx.push_layer(Box::new(prompt)); } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs new file mode 100644 index 00000000..719520af --- /dev/null +++ b/helix-term/src/commands/dap.rs @@ -0,0 +1,852 @@ +use super::{align_view, Align, Context, Editor}; +use crate::{ + commands, + compositor::Compositor, + job::Callback, + ui::{self, 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 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) { + if let Some(thread_id) = debugger.thread_id { + debugger + .thread_states + .insert(thread_id, "running".to_string()); + debugger.stack_frames.remove(&thread_id); + } + debugger.active_frame = None; + debugger.thread_id = None; +} + +pub async fn select_thread_id(editor: &mut Editor, thread_id: ThreadId, 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(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; + }; + + 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); +} + +fn thread_picker(cx: &mut Context, callback_fn: impl Fn(&mut Editor, &dap::Thread) + 'static) { + let debugger = match &mut cx.editor.debugger { + Some(debugger) => debugger, + None => return, + }; + + let threads = match block_on(debugger.threads()) { + Ok(threads) => threads, + Err(e) => { + cx.editor + .set_error(format!("Failed to retrieve threads: {}", e)); + return; + } + }; + + if threads.len() == 1 { + callback_fn(cx.editor, &threads[0]); + return; + } + + 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 frame = editor + .debugger + .as_ref() + .and_then(|debugger| debugger.stack_frames.get(&thread.id)) + .and_then(|bt| bt.get(0)); + + if let Some(frame) = 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), + )), + ) + }) + } else { + None + } + }, + ); + cx.push_layer(Box::new(picker)) +} + +// -- DAP + +pub fn dap_start_impl( + editor: &mut Editor, + name: Option<&str>, + socket: Option<std::net::SocketAddr>, + params: Option<Vec<&str>>, +) { + let doc = doc!(editor); + + let config = match doc + .language_config() + .and_then(|config| config.debugger.as_ref()) + { + Some(c) => c, + None => { + editor.set_error("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, + &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) => { + 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; + } + + 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), + }; + let template = match template { + Some(template) => template, + None => { + editor.set_error("No debug config with given name".to_string()); + return; + } + }; + + 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) + .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 { + DebugArgumentValue::String(v) => { + DebugArgumentValue::String(v.replace(&pattern, ¶m)) + } + DebugArgumentValue::Array(arr) => DebugArgumentValue::Array( + arr.iter().map(|v| v.replace(&pattern, ¶m)).collect(), + ), + }; + } + + if let DebugArgumentValue::String(string) = value { + if let Ok(integer) = string.parse::<usize>() { + args.insert(k, to_value(integer).unwrap()); + } else { + args.insert(k, to_value(string).unwrap()); + } + } else if let DebugArgumentValue::Array(arr) = value { + args.insert(k, to_value(arr).unwrap()); + } + } + } + + let args = to_value(args).unwrap(); + + let result = match &template.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 { + let msg = format!("Failed {} target: {}", template.request, e); + editor.set_error(msg); + 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("Debugger is already running".to_string()); + 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".to_string()); + return; + } + }; + + let templates = config.templates.clone(); + + cx.push_layer(Box::new(Picker::new( + true, + 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); + }, + ))); // TODO: wrap in popup with fixed size +} + +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: &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 = 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()), + ); + } + }, + None, + ) +} + +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, + 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 breakpoints = cx.editor.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 debugger = match &mut cx.editor.debugger { + Some(debugger) => debugger, + None => return, + }; + let request = debugger.set_breakpoints(path.clone(), breakpoints); + match block_on(request) { + Ok(Some(breakpoints)) => { + // TODO: avoid this clone here + let old_breakpoints = std::mem::replace(&mut debugger.breakpoints, breakpoints.clone()); + for bp in breakpoints { + if !old_breakpoints.iter().any(|b| b.message == bp.message) { + if let Some(msg) = &bp.message { + cx.editor.set_status(format!("Breakpoint set: {}", msg)); + break; + } + } + } + } + Err(e) => cx + .editor + .set_error(format!("Failed to set breakpoints: {}", e)), + _ => {} + }; +} + +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); + } else { + cx.editor + .set_error("Currently active thread is not stopped. Switch the thread.".into()); + } +} + +pub fn dap_pause(cx: &mut Context) { + thread_picker(cx, |editor, thread| { + let debugger = match &mut editor.debugger { + Some(debugger) => debugger, + None => return, + }; + 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 = 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); + } 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); + } 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); + } 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.thread_id.is_none() { + 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.ty { + Some(data_type) => format!("{} ", data_type), + None => "".to_owned(), + }; + variables.push(format!("{}{} = {}", prefix, var.name, var.value)); + } + } + } + + let contents = Text::new(variables.join("\n")); + let popup = Popup::new(contents); + cx.push_layer(Box::new(popup)); +} + +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_enable_exceptions(cx: &mut Context) { + let debugger = match &mut cx.editor.debugger { + Some(debugger) => debugger, + None => return, + }; + + let filters = match &debugger.capabilities().exception_breakpoint_filters { + Some(filters) => filters.clone(), + None => return, + }; + + if let Err(e) = block_on( + debugger.set_exception_breakpoints(filters.iter().map(|f| f.filter.clone()).collect()), + ) { + cx.editor + .set_error(format!("Failed to set up exception breakpoints: {}", e)); + } +} + +pub fn dap_disable_exceptions(cx: &mut Context) { + let debugger = match &mut cx.editor.debugger { + Some(debugger) => debugger, + None => return, + }; + + if let Err(e) = block_on(debugger.set_exception_breakpoints(vec![])) { + cx.editor + .set_error(format!("Failed to set up exception breakpoints: {}", e)); + } +} + +pub fn dap_edit_condition(cx: &mut Context) { + if let Some((pos, mut bp)) = commands::cmd::get_breakpoint_at_current_line(cx.editor) { + let callback = Box::pin(async move { + let call: Callback = + Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| { + let condition = bp.condition.clone(); + let prompt = Prompt::new( + "condition: ".into(), + None, + |_input: &str| Vec::new(), + move |cx: &mut crate::compositor::Context, + input: &str, + event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + let doc = doc!(cx.editor); + let path = match doc.path() { + Some(path) => path, + None => { + cx.editor.set_status( + "Can't edit breakpoint: document has no path".to_owned(), + ); + return; + } + }; + + let breakpoints = + cx.editor.breakpoints.entry(path.clone()).or_default(); + breakpoints.remove(pos); + bp.condition = match input { + "" => None, + input => Some(input.to_owned()), + }; + breakpoints.push(bp.clone()); + + if let Some(debugger) = &mut cx.editor.debugger { + // 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 request = + debugger.set_breakpoints(path.clone(), breakpoints.clone()); + if let Err(e) = block_on(request) { + cx.editor + .set_status(format!("Failed to set breakpoints: {}", e)) + } + } + }, + condition, + ); + compositor.push(Box::new(prompt)); + }); + Ok(call) + }); + cx.jobs.callback(callback); + } +} + +pub fn dap_edit_log(cx: &mut Context) { + if let Some((pos, mut bp)) = commands::cmd::get_breakpoint_at_current_line(cx.editor) { + let callback = Box::pin(async move { + let call: Callback = + Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| { + let log_message = bp.log_message.clone(); + let prompt = Prompt::new( + "log message: ".into(), + None, + |_input: &str| Vec::new(), + move |cx: &mut crate::compositor::Context, + input: &str, + event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + let doc = doc!(cx.editor); + let path = match doc.path() { + Some(path) => path, + None => { + cx.editor.set_status( + "Can't edit breakpoint: document has no path".to_owned(), + ); + return; + } + }; + + let breakpoints = + cx.editor.breakpoints.entry(path.clone()).or_default(); + breakpoints.remove(pos); + bp.log_message = match input { + "" => None, + input => Some(input.to_owned()), + }; + breakpoints.push(bp.clone()); + + if let Some(debugger) = &mut cx.editor.debugger { + // 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 request = + debugger.set_breakpoints(path.clone(), breakpoints.clone()); + if let Err(e) = block_on(request) { + cx.editor + .set_status(format!("Failed to set breakpoints: {}", e)) + } + } + }, + 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 = match &mut cx.editor.debugger { + Some(debugger) => debugger, + None => return, + }; + + let thread_id = match debugger.thread_id { + Some(thread_id) => thread_id, + None => { + cx.editor + .set_error("No thread is currently active".to_owned()); + 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 = match &mut cx.editor.debugger { + Some(debugger) => debugger, + None => return, + }; + // 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)) +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index bf3b594e..42a62fc2 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -646,6 +646,26 @@ impl Default for Keymaps { "S" => workspace_symbol_picker, "a" => code_action, "'" => last_picker, + "d" => { "Debug" sticky=true + "l" => dap_launch, + "b" => dap_toggle_breakpoint, + "c" => dap_continue, + "h" => dap_pause, + "i" => dap_step_in, + "o" => dap_step_out, + "n" => dap_next, + "v" => dap_variables, + "t" => dap_terminate, + "C-c" => dap_edit_condition, + "C-l" => dap_edit_log, + "s" => { "Switch" + "t" => dap_switch_thread, + "f" => dap_switch_stack_frame, + // sl, sb + }, + "e" => dap_enable_exceptions, + "E" => dap_disable_exceptions, + }, "w" => { "Window" "C-w" | "w" => rotate_view, "C-s" | "s" => hsplit, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 03cd0474..01554c64 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -15,16 +15,17 @@ use helix_core::{ unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, }; +use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame}; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, editor::LineNumber, - graphics::{CursorKind, Modifier, Rect, Style}, + graphics::{Color, CursorKind, Modifier, Rect, Style}, info::Info, input::KeyEvent, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::borrow::Cow; +use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; use tui::buffer::Buffer as Surface; @@ -71,6 +72,9 @@ impl EditorView { is_focused: bool, loader: &syntax::Loader, config: &helix_view::editor::Config, + debugger: &Option<helix_dap::Client>, + all_breakpoints: &HashMap<PathBuf, Vec<SourceBreakpoint>>, + dbg_breakpoints: &Option<Vec<Breakpoint>>, ) { let inner = view.inner_area(); let area = view.area; @@ -87,7 +91,18 @@ 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, + all_breakpoints, + dbg_breakpoints, + ); if is_focused { Self::render_focused_view_elements(view, doc, inner, theme, surface); @@ -106,7 +121,7 @@ impl EditorView { } } - self.render_diagnostics(doc, view, inner, surface, theme); + self.render_diagnostics(doc, view, inner, surface, theme, all_breakpoints); let statusline_area = view .area @@ -408,6 +423,9 @@ impl EditorView { theme: &Theme, is_focused: bool, config: &helix_view::editor::Config, + debugger: &Option<helix_dap::Client>, + all_breakpoints: &HashMap<PathBuf, Vec<SourceBreakpoint>>, + dbg_breakpoints: &Option<Vec<Breakpoint>>, ) { let text = doc.text().slice(..); let last_line = view.last_line(doc); @@ -437,6 +455,31 @@ 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(path) = doc.path() { + breakpoints = all_breakpoints.get(path); + if let Some(debugger) = debugger { + // 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) { @@ -456,6 +499,75 @@ impl EditorView { let selected = cursors.contains(&line); + // TODO: debugger should translate received breakpoints to 0-indexing + + if let Some(user) = breakpoints.as_ref() { + let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() { + debugger.iter().find(|breakpoint| { + if breakpoint.source.is_some() + && doc.path().is_some() + && breakpoint.source.as_ref().unwrap().path == doc.path().cloned() + { + match (breakpoint.line, breakpoint.end_line) { + #[allow(clippy::int_plus_one)] + (Some(l), Some(el)) => l - 1 <= line && line <= el - 1, + (Some(l), None) => l - 1 == line, + _ => false, + } + } else { + false + } + }) + } else { + None + }; + + if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line) + { + let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false); + let mut style = + if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { + error.add_modifier(Modifier::UNDERLINED) + } else if breakpoint.condition.is_some() { + error + } else if breakpoint.log_message.is_some() { + info + } else { + warning + }; + if !verified { + // Faded colors + style = if let Some(Color::Rgb(r, g, b)) = style.fg { + style.fg(Color::Rgb( + ((r as f32) * 0.4).floor() as u8, + ((g as f32) * 0.4).floor() as u8, + ((b as f32) * 0.4).floor() as u8, + )) + } else { + style.fg(Color::Gray) + } + }; + surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style); + } else if let Some(breakpoint) = debugger_breakpoint { + let style = if breakpoint.verified { + info + } else { + info.fg(Color::Gray) + }; + 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 { @@ -492,6 +604,7 @@ impl EditorView { viewport: Rect, surface: &mut Surface, theme: &Theme, + all_breakpoints: &HashMap<PathBuf, Vec<SourceBreakpoint>>, ) { use helix_core::diagnostic::Severity; use tui::{ @@ -529,6 +642,29 @@ impl EditorView { lines.extend(text.lines); } + if let Some(path) = doc.path() { + let line = doc.text().char_to_line(cursor); + if let Some(breakpoints) = all_breakpoints.get(path) { + if let Some(breakpoint) = breakpoints + .iter() + .find(|breakpoint| breakpoint.line - 1 == line) + { + if let Some(condition) = &breakpoint.condition { + lines.extend( + Text::styled(condition, warning.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) .wrap(Wrap { trim: true }); @@ -692,7 +828,6 @@ impl EditorView { cxt: &mut commands::Context, event: KeyEvent, ) -> Option<KeymapResult> { - self.autoinfo = None; let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); self.autoinfo = key_result.sticky.map(|node| node.infobox()); @@ -841,6 +976,26 @@ impl EditorView { return EventResult::Consumed(None); } + let result = editor.tree.views().find_map(|(view, _focus)| { + view.gutter_coords_at_screen_coords(row, column) + .map(|coords| (coords.0, coords.1, view.id)) + }); + + if let Some((line, _, view_id)) = result { + editor.tree.focus = view_id; + + let doc = editor + .documents + .get_mut(&editor.tree.get(view_id).doc) + .unwrap(); + if let Ok(pos) = doc.text().try_line_to_char(line) { + doc.set_selection(view_id, Selection::point(pos)); + commands::dap_toggle_breakpoint(cxt); + + return EventResult::Consumed(None); + } + } + EventResult::Ignored } @@ -917,6 +1072,40 @@ impl EditorView { } MouseEvent { + kind: MouseEventKind::Up(MouseButton::Right), + row, + column, + modifiers, + .. + } => { + let result = cxt.editor.tree.views().find_map(|(view, _focus)| { + view.gutter_coords_at_screen_coords(row, column) + .map(|coords| (coords.0, coords.1, view.id)) + }); + + if let Some((line, _, view_id)) = result { + cxt.editor.tree.focus = view_id; + + let doc = cxt + .editor + .documents + .get_mut(&cxt.editor.tree.get(view_id).doc) + .unwrap(); + if let Ok(pos) = doc.text().try_line_to_char(line) { + doc.set_selection(view_id, Selection::point(pos)); + if modifiers == crossterm::event::KeyModifiers::ALT { + commands::Command::dap_edit_log.execute(cxt); + } else { + commands::Command::dap_edit_condition.execute(cxt); + } + + return EventResult::Consumed(None); + } + } + EventResult::Ignored + } + + MouseEvent { kind: MouseEventKind::Up(MouseButton::Middle), row, column, @@ -1083,6 +1272,7 @@ impl Component for EditorView { for (view, is_focused) in cx.editor.tree.views() { let doc = cx.editor.document(view.doc).unwrap(); let loader = &cx.editor.syn_loader; + let dbg_breakpoints = cx.editor.debugger.as_ref().map(|d| d.breakpoints.clone()); self.render_view( doc, view, @@ -1092,6 +1282,9 @@ impl Component for EditorView { is_focused, loader, &cx.editor.config, + &cx.editor.debugger, + &cx.editor.breakpoints, + &dbg_breakpoints, ); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 62da0dce..0915937d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -21,7 +21,7 @@ pub use text::Text; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; -use helix_view::{Document, Editor, View}; +use helix_view::{Document, View}; use std::path::PathBuf; @@ -90,6 +90,7 @@ pub fn regex_prompt( } } }, + None, ) } @@ -152,8 +153,8 @@ pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> { .unwrap() .into() }, - move |editor: &mut Editor, path: &PathBuf, action| { - editor + move |cx, path: &PathBuf, action| { + cx.editor .open(path.into(), action) .expect("editor.open failed"); }, diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6b1c5832..2b2e47a4 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -86,7 +86,7 @@ impl<T> FilePicker<T> { pub fn new( options: Vec<T>, format_fn: impl Fn(&T) -> Cow<str> + 'static, - callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, + callback_fn: impl Fn(&mut Context, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static, ) -> Self { Self { @@ -284,7 +284,7 @@ pub struct Picker<T> { pub truncate_start: bool, format_fn: Box<dyn Fn(&T) -> Cow<str>>, - callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>, + callback_fn: Box<dyn Fn(&mut Context, &T, Action)>, } impl<T> Picker<T> { @@ -292,7 +292,7 @@ impl<T> Picker<T> { render_centered: bool, options: Vec<T>, format_fn: impl Fn(&T) -> Cow<str> + 'static, - callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, + callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( "".into(), @@ -301,6 +301,7 @@ impl<T> Picker<T> { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // }, + None, ); let mut picker = Self { @@ -421,19 +422,19 @@ impl<T: 'static> Component for Picker<T> { } key!(Enter) => { if let Some(option) = self.selection() { - (self.callback_fn)(&mut cx.editor, option, Action::Replace); + (self.callback_fn)(cx, option, Action::Replace); } return close_fn; } ctrl!('s') => { if let Some(option) = self.selection() { - (self.callback_fn)(&mut cx.editor, option, Action::HorizontalSplit); + (self.callback_fn)(cx, option, Action::HorizontalSplit); } return close_fn; } ctrl!('v') => { if let Some(option) = self.selection() { - (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit); + (self.callback_fn)(cx, option, Action::VerticalSplit); } return close_fn; } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index e90b0772..00ffdccf 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -61,10 +61,11 @@ impl Prompt { history_register: Option<char>, mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, + line: Option<String>, ) -> Self { Self { prompt, - line: String::new(), + line: line.unwrap_or_default(), cursor: 0, completion: completion_fn(""), selection: None, |