aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui/editor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui/editor.rs')
-rw-r--r--helix-term/src/ui/editor.rs377
1 files changed, 370 insertions, 7 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 9234bb96..037f04b8 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -1,6 +1,7 @@
use crate::{
commands,
- compositor::{Component, Context, EventResult},
+ compositor::{Component, Compositor, Context, EventResult},
+ job::Callback,
key,
keymap::{KeymapResult, KeymapResultKind, Keymaps},
ui::{Completion, ProgressSpinners},
@@ -10,25 +11,28 @@ use helix_core::{
coords_at_pos,
graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
movement::Direction,
- syntax::{self, HighlightEvent},
+ syntax::{self, DebugConfigCompletion, HighlightEvent},
unicode::segmentation::UnicodeSegmentation,
unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection,
};
+use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
use helix_view::{
document::Mode,
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;
+use super::{Prompt, PromptEvent};
+
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
@@ -71,6 +75,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 +94,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 +124,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 +426,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 +458,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 +502,77 @@ impl EditorView {
let selected = cursors.contains(&line);
+ 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 unverified = match dbg_breakpoints {
+ Some(_) => debugger_breakpoint.map(|b| !b.verified).unwrap_or(true),
+ // We cannot mark breakpoint as unverified unless we have a debugger
+ None => 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 unverified {
+ // 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 +609,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 +647,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);
let width = 80.min(viewport.width);
let height = 15.min(viewport.height);
@@ -635,6 +776,79 @@ impl EditorView {
);
}
+ fn debug_parameter_prompt(
+ completions: Vec<DebugConfigCompletion>,
+ config_name: String,
+ mut params: Vec<String>,
+ ) -> Prompt {
+ let i = params.len();
+ let completion = completions.get(i).unwrap();
+ let field_type = if let DebugConfigCompletion::Advanced(cfg) = completion {
+ cfg.completion.clone().unwrap_or_else(|| "".to_owned())
+ } else {
+ "".to_owned()
+ };
+ let name = match completion {
+ DebugConfigCompletion::Advanced(cfg) => {
+ cfg.name.clone().unwrap_or_else(|| field_type.to_owned())
+ }
+ DebugConfigCompletion::Named(name) => name.clone(),
+ };
+ let default_val = match completion {
+ DebugConfigCompletion::Advanced(cfg) => {
+ cfg.default.clone().unwrap_or_else(|| "".to_owned())
+ }
+ _ => "".to_owned(),
+ };
+
+ let noop = |_input: &str| Vec::new();
+ let completer = match &field_type[..] {
+ "filename" => super::completers::filename,
+ "directory" => super::completers::directory,
+ _ => noop,
+ };
+ Prompt::new(
+ format!("{}: ", name).into(),
+ None,
+ completer,
+ move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+
+ let mut value = input.to_owned();
+ if value.is_empty() {
+ value = default_val.clone();
+ }
+ params.push(value);
+
+ if params.len() < completions.len() {
+ let completions = completions.clone();
+ let config_name = config_name.clone();
+ let params = params.clone();
+ let callback = Box::pin(async move {
+ let call: Callback =
+ Box::new(move |_editor: &mut Editor, compositor: &mut Compositor| {
+ let prompt =
+ Self::debug_parameter_prompt(completions, config_name, params);
+ compositor.push(Box::new(prompt));
+ });
+ Ok(call)
+ });
+ cx.jobs.callback(callback);
+ } else {
+ commands::dap_start_impl(
+ cx.editor,
+ Some(&config_name),
+ None,
+ Some(params.iter().map(|x| x.as_str()).collect()),
+ );
+ }
+ },
+ None,
+ )
+ }
+
/// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned
@@ -645,7 +859,56 @@ impl EditorView {
cxt: &mut commands::Context,
event: KeyEvent,
) -> Option<KeymapResult> {
- self.autoinfo = None;
+ if let Some(picker) = cxt.editor.debug_config_picker.clone() {
+ match event {
+ KeyEvent {
+ code: KeyCode::Esc, ..
+ } => {}
+ KeyEvent {
+ code: KeyCode::Char(char),
+ ..
+ } => {
+ let (i, name) = match picker.iter().position(|t| t.starts_with(char)) {
+ Some(pos) => (pos, picker.get(pos).unwrap().clone()),
+ None => return None,
+ };
+ let completions = cxt.editor.debug_config_completions.clone().unwrap();
+ let completion = completions.get(i).unwrap().clone();
+ if !completion.is_empty() {
+ let prompt = Self::debug_parameter_prompt(completion, name, Vec::new());
+ cxt.push_layer(Box::new(prompt));
+ }
+ }
+ _ => return None,
+ }
+ cxt.editor.debug_config_picker = None;
+ return None;
+ }
+
+ if cxt.editor.variables.is_some() {
+ match event {
+ KeyEvent {
+ code: KeyCode::Char('h'),
+ ..
+ } => {
+ cxt.editor.variables_page = cxt.editor.variables_page.saturating_sub(1);
+ }
+ KeyEvent {
+ code: KeyCode::Char('l'),
+ ..
+ } => {
+ cxt.editor.variables_page = cxt.editor.variables_page.saturating_add(1);
+ }
+ KeyEvent {
+ code: KeyCode::Esc, ..
+ } => {
+ cxt.editor.variables = None;
+ }
+ _ => {}
+ }
+ return None;
+ }
+
let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
self.autoinfo = key_result.sticky.map(|node| node.infobox());
@@ -778,6 +1041,23 @@ 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 = &mut editor.documents[editor.tree.get(view_id).doc];
+ 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
}
@@ -854,6 +1134,36 @@ 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 = &mut cxt.editor.documents[cxt.editor.tree.get(view_id).doc];
+ 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,
@@ -1022,6 +1332,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,
@@ -1031,9 +1342,61 @@ impl Component for EditorView {
is_focused,
loader,
&cx.editor.config,
+ &cx.editor.debugger,
+ &cx.editor.breakpoints,
+ &dbg_breakpoints,
);
}
+ 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);
}