aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/Cargo.toml3
-rw-r--r--helix-term/src/application.rs144
-rw-r--r--helix-term/src/commands.rs552
-rw-r--r--helix-term/src/keymap.rs12
-rw-r--r--helix-term/src/ui/editor.rs190
5 files changed, 891 insertions, 10 deletions
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 57d592cc..6e9c0daf 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -24,6 +24,7 @@ path = "src/main.rs"
helix-core = { version = "0.4", path = "../helix-core" }
helix-view = { version = "0.4", path = "../helix-view" }
helix-lsp = { version = "0.4", path = "../helix-lsp" }
+helix-dap = { version = "0.4", 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.21", features = ["event-stream"] }
signal-hook = "0.3"
-
+tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
# Logging
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 1fcca681..9aa98271 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,11 +1,18 @@
-use helix_core::{merge_toml_values, syntax};
+use helix_core::{merge_toml_values, syntax, Range, Selection};
+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::{align_view, Align},
+ compositor::Compositor,
+ config::Config,
+ job::Jobs,
+ ui,
+};
use log::error;
-
use std::{
io::{stdout, Write},
sync::Arc,
@@ -191,6 +198,9 @@ impl Application {
last_render = Instant::now();
}
}
+ Some(payload) = self.editor.debugger_events.next() => {
+ self.handle_debugger_message(payload).await;
+ }
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
@@ -252,6 +262,134 @@ impl Application {
}
}
+ pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) {
+ use helix_dap::{events, Event};
+ let mut debugger = match self.editor.debugger.as_mut() {
+ Some(debugger) => debugger,
+ None => return,
+ };
+
+ match payload {
+ Payload::Event(ev) => match ev {
+ Event::Stopped(events::Stopped {
+ thread_id,
+ description,
+ text,
+ reason,
+ all_threads_stopped,
+ ..
+ }) => {
+ debugger.is_running = false;
+ let main = debugger.threads().await.ok().and_then(|threads| {
+ // Workaround for debugging Go tests. Main thread has * in beginning of its name
+ let mut main = threads.iter().find(|t| t.name.starts_with('*')).cloned();
+ if main.is_none() {
+ main = threads.get(0).cloned();
+ }
+ main
+ });
+
+ if let Some(main) = main {
+ let (bt, _) = debugger.stack_trace(main.id).await.unwrap();
+ debugger.stack_pointer = bt.get(0).cloned();
+ debugger.stopped_thread = Some(main.id);
+ }
+
+ let scope = match thread_id {
+ Some(id) => format!("Thread {}", id),
+ None => "Target".to_owned(),
+ };
+
+ let mut status = format!("{} stopped because of {}", scope, reason);
+ if let Some(desc) = description {
+ status.push_str(&format!(" {}", desc));
+ }
+ if let Some(text) = text {
+ status.push_str(&format!(" {}", text));
+ }
+ if all_threads_stopped.unwrap_or_default() {
+ status.push_str(" (all threads stopped)");
+ }
+
+ if let Some(helix_dap::StackFrame {
+ source:
+ Some(helix_dap::Source {
+ path: Some(ref src),
+ ..
+ }),
+ line,
+ column,
+ end_line,
+ end_column,
+ ..
+ }) = debugger.stack_pointer
+ {
+ let path = src.clone();
+ self.editor
+ .open(path, helix_view::editor::Action::Replace)
+ .unwrap();
+
+ let (view, doc) = current!(self.editor);
+
+ let text_end = doc.text().len_chars().saturating_sub(1);
+ let start = doc.text().try_line_to_char(line - 1).unwrap_or(0) + column;
+ if let Some(end_line) = end_line {
+ let end = doc.text().try_line_to_char(end_line - 1).unwrap_or(0)
+ + end_column.unwrap_or(0);
+ doc.set_selection(
+ view.id,
+ Selection::new(
+ helix_core::SmallVec::from_vec(vec![Range::new(
+ start.min(text_end),
+ end.min(text_end),
+ )]),
+ 0,
+ ),
+ );
+ } else {
+ doc.set_selection(view.id, Selection::point(start.min(text_end)));
+ }
+ align_view(doc, view, Align::Center);
+ }
+ self.editor.set_status(status);
+ }
+ Event::Output(events::Output {
+ category, output, ..
+ }) => {
+ let prefix = match category {
+ Some(category) => {
+ if &category == "telemetry" {
+ return;
+ }
+ format!("Debug ({}):", category)
+ }
+ None => "Debug:".to_owned(),
+ };
+
+ self.editor.set_status(format!("{} {}", prefix, output));
+ }
+ Event::Initialized => {
+ self.editor
+ .set_status("Debugged application started".to_owned());
+ }
+ Event::Continued(_) => {
+ if let Some(debugger) = self.editor.debugger.as_mut() {
+ debugger.stopped_thread = None;
+ debugger.stack_pointer = None;
+ debugger.is_running = true;
+ }
+ }
+ 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 d21bbe42..e5db1624 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -24,16 +24,18 @@ use helix_lsp::{
};
use insert::*;
use movement::Movement;
+use serde_json::Value;
use crate::{
compositor::{self, Component, Compositor},
ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
};
+use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::job::{self, Job, Jobs};
use futures_util::FutureExt;
use std::num::NonZeroUsize;
-use std::{fmt, future::Future};
+use std::{collections::HashMap, fmt, future::Future};
use std::{
borrow::Cow,
@@ -97,13 +99,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()
@@ -302,6 +304,16 @@ impl Command {
surround_delete, "Surround delete",
select_textobject_around, "Select around object",
select_textobject_inner, "Select inside object",
+ dap_launch, "Launch debug target",
+ dap_toggle_breakpoint, "Toggle breakpoint",
+ dap_run, "Begin program execution",
+ dap_continue, "Continue program execution",
+ dap_pause, "Pause program execution",
+ dap_in, "Step in",
+ dap_out, "Step out",
+ dap_next, "Step to next",
+ dap_variables, "List variables",
+ dap_terminate, "End debug session",
suspend, "Suspend"
);
}
@@ -1336,7 +1348,6 @@ fn append_mode(cx: &mut Context) {
mod cmd {
use super::*;
- use std::collections::HashMap;
use helix_view::editor::Action;
use ui::completers::{self, Completer};
@@ -1934,6 +1945,154 @@ mod cmd {
Ok(())
}
+ fn debug_eval(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ use helix_lsp::block_on;
+ if let Some(debugger) = cx.editor.debugger.as_mut() {
+ let id = debugger.stack_pointer.clone().map(|x| x.id);
+ let response = block_on(debugger.eval(args.join(" "), id))?;
+ cx.editor.set_status(response.result);
+ }
+ Ok(())
+ }
+
+ fn edit_breakpoint_impl(
+ cx: &mut compositor::Context,
+ condition: Option<String>,
+ log_message: Option<String>,
+ ) {
+ use helix_lsp::block_on;
+
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let pos = doc.selection(view.id).primary().cursor(text);
+ let breakpoint = helix_dap::SourceBreakpoint {
+ line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
+ condition,
+ log_message,
+ ..Default::default()
+ };
+ let path = match doc.path() {
+ Some(path) => path.to_path_buf(),
+ None => {
+ cx.editor
+ .set_error("Can't edit breakpoint: document has no path".to_string());
+ return;
+ }
+ };
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if breakpoint.condition.is_some()
+ && !debugger
+ .caps
+ .clone()
+ .unwrap()
+ .supports_conditional_breakpoints
+ .unwrap_or_default()
+ {
+ cx.editor.set_error(
+ "Can't edit breakpoint: debugger does not support conditional breakpoints"
+ .to_string(),
+ );
+ return;
+ }
+ if breakpoint.log_message.is_some()
+ && !debugger
+ .caps
+ .clone()
+ .unwrap()
+ .supports_log_points
+ .unwrap_or_default()
+ {
+ cx.editor.set_error(
+ "Can't edit breakpoint: debugger does not support logpoints".to_string(),
+ );
+ return;
+ }
+
+ let breakpoints = debugger.breakpoints.entry(path.clone()).or_default();
+ if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) {
+ breakpoints.remove(pos);
+ breakpoints.push(breakpoint);
+
+ let breakpoints = breakpoints.clone();
+
+ let request = debugger.set_breakpoints(path, breakpoints);
+ if let Err(e) = block_on(request) {
+ cx.editor
+ .set_error(format!("Failed to set breakpoints: {:?}", e));
+ }
+ }
+ }
+ }
+
+ fn debug_start(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let mut args = args.to_owned();
+ let name = match args.len() {
+ 0 => None,
+ _ => Some(args.remove(0)),
+ };
+ dap_start_impl(&mut cx.editor, name, None, Some(args));
+ Ok(())
+ }
+
+ fn debug_remote(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let mut args = args.to_owned();
+ let address = match args.len() {
+ 0 => None,
+ _ => Some(args.remove(0).parse()?),
+ };
+ let name = match args.len() {
+ 0 => None,
+ _ => Some(args.remove(0)),
+ };
+ dap_start_impl(&mut cx.editor, name, address, Some(args));
+
+ Ok(())
+ }
+
+ fn debug_breakpoint_condition(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let condition = args.join(" ");
+ let condition = if condition.is_empty() {
+ None
+ } else {
+ Some(condition)
+ };
+
+ edit_breakpoint_impl(cx, condition, None);
+ Ok(())
+ }
+
+ fn debug_set_logpoint(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let log_message = args.join(" ");
+ let log_message = if log_message.is_empty() {
+ None
+ } else {
+ Some(log_message)
+ };
+
+ edit_breakpoint_impl(cx, None, log_message);
+ Ok(())
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2174,6 +2333,41 @@ mod cmd {
completer: None,
},
TypableCommand {
+ name: "debug-start",
+ alias: Some("dbg"),
+ doc: "Start a debug session from a given template with given parameters.",
+ fun: debug_start,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "debug-remote",
+ alias: Some("dbg-tcp"),
+ doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
+ fun: debug_remote,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "debug-eval",
+ alias: None,
+ doc: "Evaluate expression in current debug context.",
+ fun: debug_eval,
+ completer: None,
+ },
+ TypableCommand {
+ name: "debug-breakpoint-condition",
+ alias: None,
+ doc: "Set current breakpoint condition.",
+ fun: debug_breakpoint_condition,
+ completer: None,
+ },
+ TypableCommand {
+ name: "debug-set-logpoint",
+ alias: None,
+ doc: "Make current breakpoint a log point.",
+ fun: debug_set_logpoint,
+ completer: None,
+ },
+ TypableCommand {
name: "vsplit",
alias: Some("vs"),
doc: "Open the file in a vertical split.",
@@ -4296,3 +4490,353 @@ fn suspend(_cx: &mut Context) {
#[cfg(not(windows))]
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
}
+
+// DAP
+pub fn dap_start_impl(
+ editor: &mut Editor,
+ name: Option<&str>,
+ socket: Option<std::net::SocketAddr>,
+ params: Option<Vec<&str>>,
+) {
+ use helix_dap::Client;
+ use helix_lsp::block_on;
+ use serde_json::to_value;
+
+ let (_, doc) = current!(editor);
+
+ let path = match doc.path() {
+ Some(path) => path.to_path_buf(),
+ None => {
+ editor.set_error("Can't start debug: document has no path".to_string());
+ return;
+ }
+ };
+
+ let config = editor
+ .syn_loader
+ .language_config_for_file_name(&path)
+ .and_then(|x| x.debugger.clone());
+ let config = match config {
+ Some(c) => c,
+ None => {
+ editor.set_error(
+ "Can't start debug: no debug adapter available for language".to_string(),
+ );
+ return;
+ }
+ };
+
+ let result = match socket {
+ Some(socket) => block_on(Client::tcp(socket, 0)),
+ None => block_on(Client::process(config.clone(), 0)),
+ };
+
+ let (mut debugger, events) = match result {
+ Ok(r) => r,
+ Err(e) => {
+ editor.set_error(format!("Failed to start debug session: {:?}", e));
+ return;
+ }
+ };
+
+ let request = debugger.initialize(config.name.clone());
+ if let Err(e) = block_on(request) {
+ editor.set_error(format!("Failed to initialize debug adapter: {:?}", e));
+ return;
+ }
+
+ let start_config = match name {
+ Some(name) => config.templates.iter().find(|t| t.name == name),
+ None => config.templates.get(0),
+ };
+ let start_config = match start_config {
+ Some(c) => c,
+ None => {
+ editor.set_error("Can't start debug: no debug config with given name".to_string());
+ return;
+ }
+ };
+
+ let template = start_config.args.clone();
+ let mut args: HashMap<String, Value> = HashMap::new();
+
+ if let Some(params) = params {
+ for (k, t) in template {
+ let mut value = t;
+ for (i, x) in params.iter().enumerate() {
+ // For param #0 replace {0} in args
+ value = value.replace(format!("{{{}}}", i).as_str(), x);
+ }
+
+ if let Ok(integer) = value.parse::<usize>() {
+ args.insert(k, Value::Number(serde_json::Number::from(integer)));
+ } else {
+ args.insert(k, Value::String(value));
+ }
+ }
+ }
+
+ let args = to_value(args).unwrap();
+
+ let result = match &start_config.request[..] {
+ "launch" => block_on(debugger.launch(args)),
+ "attach" => block_on(debugger.attach(args)),
+ _ => {
+ editor.set_error("Unsupported request".to_string());
+ return;
+ }
+ };
+ if let Err(e) = result {
+ editor.set_error(format!("Failed {} target: {:?}", start_config.request, e));
+ return;
+ }
+
+ // TODO: either await "initialized" or buffer commands until event is received
+ editor.debugger = Some(debugger);
+ let stream = UnboundedReceiverStream::new(events);
+ editor.debugger_events.push(stream);
+}
+
+fn dap_launch(cx: &mut Context) {
+ if cx.editor.debugger.is_some() {
+ cx.editor
+ .set_error("Can't start debug: debugger is running".to_string());
+ return;
+ }
+
+ let (_, doc) = current!(cx.editor);
+ let path = match doc.path() {
+ Some(path) => path.to_path_buf(),
+ None => {
+ cx.editor
+ .set_error("Can't start debug: document has no path".to_string());
+ return;
+ }
+ };
+
+ let config = cx
+ .editor
+ .syn_loader
+ .language_config_for_file_name(&path)
+ .and_then(|x| x.debugger.clone());
+ let config = match config {
+ Some(c) => c,
+ None => {
+ cx.editor.set_error(
+ "Can't start debug: no debug adapter available for language".to_string(),
+ );
+ return;
+ }
+ };
+
+ cx.editor.debug_config_picker = Some(config.templates.iter().map(|t| t.name.clone()).collect());
+}
+
+fn dap_toggle_breakpoint(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let pos = doc.selection(view.id).primary().cursor(text);
+
+ let breakpoint = helix_dap::SourceBreakpoint {
+ line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
+ ..Default::default()
+ };
+
+ let path = match doc.path() {
+ Some(path) => path.to_path_buf(),
+ None => {
+ cx.editor
+ .set_error("Can't set breakpoint: document has no path".to_string());
+ return;
+ }
+ };
+
+ // TODO: need to map breakpoints over edits and update them?
+ // we shouldn't really allow editing while debug is running though
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ let breakpoints = debugger.breakpoints.entry(path.clone()).or_default();
+ if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) {
+ breakpoints.remove(pos);
+ } else {
+ breakpoints.push(breakpoint);
+ }
+
+ let breakpoints = breakpoints.clone();
+
+ let request = debugger.set_breakpoints(path, breakpoints);
+ if let Err(e) = block_on(request) {
+ cx.editor
+ .set_error(format!("Failed to set breakpoints: {:?}", e));
+ }
+ }
+}
+
+fn dap_run(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if debugger.is_running {
+ cx.editor
+ .set_status("Debuggee is already running".to_owned());
+ return;
+ }
+ let request = debugger.configuration_done();
+ if let Err(e) = block_on(request) {
+ cx.editor.set_error(format!("Failed to run: {:?}", e));
+ return;
+ }
+ debugger.is_running = true;
+ }
+}
+
+fn dap_continue(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if debugger.is_running {
+ cx.editor
+ .set_status("Debuggee is already running".to_owned());
+ return;
+ }
+
+ let request = debugger.continue_thread(debugger.stopped_thread.unwrap());
+ if let Err(e) = block_on(request) {
+ cx.editor.set_error(format!("Failed to continue: {:?}", e));
+ return;
+ }
+ debugger.is_running = true;
+ debugger.stack_pointer = None;
+ }
+}
+
+fn dap_pause(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if !debugger.is_running {
+ cx.editor.set_status("Debuggee is not running".to_owned());
+ return;
+ }
+
+ // FIXME: correct number here
+ let request = debugger.pause(0);
+ if let Err(e) = block_on(request) {
+ cx.editor.set_error(format!("Failed to pause: {:?}", e));
+ }
+ }
+}
+
+fn dap_in(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if debugger.is_running {
+ cx.editor
+ .set_status("Debuggee is already running".to_owned());
+ return;
+ }
+
+ let request = debugger.step_in(debugger.stopped_thread.unwrap());
+ if let Err(e) = block_on(request) {
+ cx.editor.set_error(format!("Failed to step: {:?}", e));
+ }
+ }
+}
+
+fn dap_out(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if debugger.is_running {
+ cx.editor
+ .set_status("Debuggee is already running".to_owned());
+ return;
+ }
+
+ let request = debugger.step_out(debugger.stopped_thread.unwrap());
+ if let Err(e) = block_on(request) {
+ cx.editor.set_error(format!("Failed to step: {:?}", e));
+ }
+ }
+}
+
+fn dap_next(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if debugger.is_running {
+ cx.editor
+ .set_status("Debuggee is already running".to_owned());
+ return;
+ }
+
+ let request = debugger.next(debugger.stopped_thread.unwrap());
+ if let Err(e) = block_on(request) {
+ cx.editor.set_error(format!("Failed to step: {:?}", e));
+ }
+ }
+}
+
+fn dap_variables(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ if debugger.is_running {
+ cx.editor
+ .set_status("Cannot access variables while target is running".to_owned());
+ return;
+ }
+ if debugger.stack_pointer.is_none() {
+ cx.editor
+ .set_status("Cannot find current stack pointer to access variables".to_owned());
+ return;
+ }
+
+ let frame_id = debugger.stack_pointer.clone().unwrap().id;
+ let scopes = match block_on(debugger.scopes(frame_id)) {
+ Ok(s) => s,
+ Err(e) => {
+ cx.editor
+ .set_error(format!("Failed to get scopes: {:?}", e));
+ return;
+ }
+ };
+ let mut variables = Vec::new();
+
+ for scope in scopes.iter() {
+ let response = block_on(debugger.variables(scope.variables_reference));
+
+ if let Ok(vars) = response {
+ for var in vars {
+ let prefix = match var.data_type {
+ Some(data_type) => format!("{} ", data_type),
+ None => "".to_owned(),
+ };
+ variables.push(format!("{}{} = {}\n", prefix, var.name, var.value));
+ }
+ }
+ }
+
+ if !variables.is_empty() {
+ cx.editor.variables = Some(variables);
+ cx.editor.variables_page = 0;
+ }
+ }
+}
+
+fn dap_terminate(cx: &mut Context) {
+ use helix_lsp::block_on;
+
+ if let Some(debugger) = &mut cx.editor.debugger {
+ let request = debugger.disconnect();
+ if let Err(e) = block_on(request) {
+ cx.editor
+ .set_error(format!("Failed to disconnect: {:?}", e));
+ return;
+ }
+ cx.editor.debugger = None;
+ }
+}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 492dc292..2aa3f9f3 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -485,6 +485,18 @@ impl Default for Keymaps {
"s" => symbol_picker,
"a" => code_action,
"'" => last_picker,
+ "d" => { "Debug"
+ "s" => dap_launch,
+ "b" => dap_toggle_breakpoint,
+ "r" => dap_run,
+ "c" => dap_continue,
+ "h" => dap_pause,
+ "j" => dap_in,
+ "k" => dap_out,
+ "l" => dap_next,
+ "v" => dap_variables,
+ "t" => dap_terminate,
+ },
"w" => { "Window"
"C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit,
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 72b8adc1..6428870e 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -15,6 +15,7 @@ use helix_core::{
unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection,
};
+use helix_dap::{SourceBreakpoint, StackFrame};
use helix_view::{
document::Mode,
editor::LineNumber,
@@ -29,6 +30,8 @@ use std::borrow::Cow;
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use tui::buffer::Buffer as Surface;
+use super::{Prompt, PromptEvent};
+
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
@@ -71,6 +74,7 @@ impl EditorView {
is_focused: bool,
loader: &syntax::Loader,
config: &helix_view::editor::Config,
+ debugger: &Option<helix_dap::Client>,
) {
let inner = view.inner_area();
let area = view.area;
@@ -87,7 +91,9 @@ impl EditorView {
};
Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
- Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config);
+ Self::render_gutter(
+ doc, view, view.area, surface, theme, is_focused, config, debugger,
+ );
if is_focused {
Self::render_focused_view_elements(view, doc, inner, theme, surface);
@@ -106,7 +112,7 @@ impl EditorView {
}
}
- self.render_diagnostics(doc, view, inner, surface, theme);
+ self.render_diagnostics(doc, view, inner, surface, theme, debugger);
let statusline_area = view
.area
@@ -409,6 +415,7 @@ impl EditorView {
theme: &Theme,
is_focused: bool,
config: &helix_view::editor::Config,
+ debugger: &Option<helix_dap::Client>,
) {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
@@ -438,6 +445,15 @@ impl EditorView {
.map(|range| range.cursor_line(text))
.collect();
+ let mut breakpoints: Option<Vec<SourceBreakpoint>> = None;
+ let mut stack_pointer: Option<StackFrame> = None;
+ if let Some(debugger) = debugger {
+ if let Some(path) = doc.path() {
+ breakpoints = debugger.breakpoints.get(path).cloned();
+ stack_pointer = debugger.stack_pointer.clone()
+ }
+ }
+
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
@@ -457,6 +473,36 @@ impl EditorView {
let selected = cursors.contains(&line);
+ if let Some(bps) = breakpoints.as_ref() {
+ if let Some(breakpoint) = bps.iter().find(|breakpoint| breakpoint.line - 1 == line)
+ {
+ if breakpoint.condition.is_some() {
+ surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, error);
+ } else if breakpoint.log_message.is_some() {
+ surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, info);
+ } else {
+ surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, warning);
+ }
+ }
+ }
+
+ if let Some(sp) = stack_pointer.as_ref() {
+ if let Some(src) = sp.source.as_ref() {
+ if doc
+ .path()
+ .map(|path| src.path == Some(path.clone()))
+ .unwrap_or(false)
+ && sp.line - 1 == line
+ {
+ surface.set_style(
+ Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
+ helix_view::graphics::Style::default()
+ .bg(helix_view::graphics::Color::LightYellow),
+ );
+ }
+ }
+ }
+
let text = if line == last_line && !draw_last {
" ~".into()
} else {
@@ -493,6 +539,7 @@ impl EditorView {
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
+ debugger: &Option<helix_dap::Client>,
) {
use helix_core::diagnostic::Severity;
use tui::{
@@ -530,6 +577,31 @@ impl EditorView {
lines.extend(text.lines);
}
+ if let Some(debugger) = debugger {
+ if let Some(path) = doc.path() {
+ if let Some(breakpoints) = debugger.breakpoints.get(path) {
+ let line = doc.text().char_to_line(cursor);
+ if let Some(breakpoint) = breakpoints
+ .iter()
+ .find(|breakpoint| breakpoint.line - 1 == line)
+ {
+ if let Some(condition) = &breakpoint.condition {
+ lines.extend(
+ Text::styled(condition, info.add_modifier(Modifier::UNDERLINED))
+ .lines,
+ );
+ }
+ if let Some(log_message) = &breakpoint.log_message {
+ lines.extend(
+ Text::styled(log_message, info.add_modifier(Modifier::UNDERLINED))
+ .lines,
+ );
+ }
+ }
+ }
+ }
+ }
+
let paragraph = Paragraph::new(lines).alignment(Alignment::Right);
let width = 80.min(viewport.width);
let height = 15.min(viewport.height);
@@ -647,6 +719,70 @@ impl EditorView {
event: KeyEvent,
) -> Option<KeymapResult> {
self.autoinfo = None;
+
+ if let Some(picker) = cxt.editor.debug_config_picker.clone() {
+ match event {
+ KeyEvent {
+ code: KeyCode::Esc, ..
+ } => {}
+ KeyEvent {
+ code: KeyCode::Char(char),
+ ..
+ } => {
+ let name = match picker.iter().find(|t| t.starts_with(char)) {
+ Some(n) => n.clone(),
+ None => return None,
+ };
+ let prompt = Prompt::new(
+ "arg:".to_owned(),
+ None,
+ |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
+ move |cx: &mut crate::compositor::Context,
+ input: &str,
+ event: PromptEvent| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+ commands::dap_start_impl(
+ cx.editor,
+ Some(&name),
+ None,
+ Some(vec![input]),
+ );
+ },
+ );
+ cxt.push_layer(Box::new(prompt));
+ }
+ _ => return None,
+ }
+ cxt.editor.debug_config_picker = None;
+ return None;
+ }
+
+ if cxt.editor.variables.is_some() {
+ match event {
+ KeyEvent {
+ code: KeyCode::Char('h'),
+ ..
+ } => {
+ cxt.editor.variables_page = cxt.editor.variables_page.saturating_sub(1);
+ }
+ KeyEvent {
+ code: KeyCode::Char('l'),
+ ..
+ } => {
+ cxt.editor.variables_page = cxt.editor.variables_page.saturating_add(1);
+ }
+ KeyEvent {
+ code: KeyCode::Esc, ..
+ } => {
+ cxt.editor.variables = None;
+ }
+ _ => {}
+ }
+ return None;
+ }
+
match self.keymaps.get_mut(&mode).unwrap().get(event) {
KeymapResult::Matched(command) => command.execute(cxt),
KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
@@ -1016,9 +1152,59 @@ impl Component for EditorView {
is_focused,
loader,
&cx.editor.config,
+ &cx.editor.debugger,
);
}
+ if let Some(ref vars) = cx.editor.variables {
+ let mut text = String::new();
+ let mut height = 0;
+ let mut max_len = 20;
+
+ let per_page = 15;
+ let num_vars = vars.len();
+ let start = (per_page * cx.editor.variables_page).min(num_vars);
+ let end = (start + per_page).min(num_vars);
+ for line in vars[start..end].to_vec() {
+ max_len = max_len.max(line.len() as u16);
+ height += 1;
+ text.push_str(&line);
+ }
+
+ if vars.len() > per_page {
+ text += "\nMove h, l";
+ height += 1;
+ }
+
+ let mut info = Info {
+ height: 20.min(height + 2),
+ width: 70.min(max_len),
+ title: format!("{} variables", num_vars),
+ text: text + "\nExit Esc",
+ };
+ info.render(area, surface, cx);
+ }
+
+ if let Some(ref configs) = cx.editor.debug_config_picker {
+ let mut text = String::new();
+ let mut height = 0;
+ let mut max_len = 20;
+
+ for line in configs {
+ max_len = max_len.max(line.len() as u16 + 2);
+ height += 1;
+ text.push_str(&format!("{} {}\n", line.chars().next().unwrap(), line));
+ }
+
+ let mut info = Info {
+ height: 20.min(height + 1),
+ width: 70.min(max_len),
+ title: "Debug targets".to_owned(),
+ text: text + "Exit Esc",
+ };
+ info.render(area, surface, cx);
+ }
+
if let Some(ref mut info) = self.autoinfo {
info.render(area, surface, cx);
}