aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/Cargo.toml4
-rw-r--r--helix-term/src/application.rs169
-rw-r--r--helix-term/src/commands.rs159
-rw-r--r--helix-term/src/commands/dap.rs852
-rw-r--r--helix-term/src/keymap.rs20
-rw-r--r--helix-term/src/ui/editor.rs203
-rw-r--r--helix-term/src/ui/mod.rs7
-rw-r--r--helix-term/src/ui/picker.rs13
-rw-r--r--helix-term/src/ui/prompt.rs3
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, &param))
+ }
+ DebugArgumentValue::Array(arr) => DebugArgumentValue::Array(
+ arr.iter().map(|v| v.replace(&pattern, &param)).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,