diff options
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r-- | helix-term/src/ui/editor.rs | 377 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 1 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 1 | ||||
-rw-r--r-- | helix-term/src/ui/prompt.rs | 3 |
4 files changed, 374 insertions, 8 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 0605e2c7..128fe948 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()); @@ -769,6 +1032,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 } @@ -845,6 +1125,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, @@ -1010,6 +1320,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, @@ -1019,9 +1330,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); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 810a9966..e66673ca 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -87,6 +87,7 @@ pub fn regex_prompt( } } }, + None, ) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c5b90a9c..ee1ec177 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -212,6 +212,7 @@ impl<T> Picker<T> { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // }, + None, ); let mut picker = Self { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 1d512ad2..56335fb3 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -59,10 +59,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, |