From 29c053e84e2624feb786f520ebae4c752bc23279 Mon Sep 17 00:00:00 2001 From: Kirawi Date: Wed, 8 Dec 2021 02:11:18 -0500 Subject: Only use a single documentation popup (#1241) --- helix-term/src/ui/popup.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'helix-term/src/ui/popup.rs') diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 8f7921a1..a5310b23 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -16,15 +16,17 @@ pub struct Popup { position: Option, size: (u16, u16), scroll: usize, + id: &'static str, } impl Popup { - pub fn new(contents: T) -> Self { + pub fn new(id: &'static str, contents: T) -> Self { Self { contents, position: None, size: (0, 0), scroll: 0, + id, } } @@ -143,4 +145,8 @@ impl Component for Popup { self.contents.render(area, surface, cx); } + + fn id(&self) -> Option<&'static str> { + Some(self.id) + } } -- cgit v1.2.3-70-g09d2 From 3307f44ce20a71273614f9b30dafb08822e557a1 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 10 Dec 2021 19:23:34 +0900 Subject: ui: popup: Don't allow scrolling past the end of content --- helix-term/src/compositor.rs | 5 +++-- helix-term/src/ui/markdown.rs | 5 ----- helix-term/src/ui/popup.rs | 19 ++++++++++++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) (limited to 'helix-term/src/ui/popup.rs') diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 37e67973..30554ebb 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -55,9 +55,10 @@ pub trait Component: Any + AnyComponent { /// May be used by the parent component to compute the child area. /// viewport is the maximum allowed area, and the child should stay within those bounds. + /// + /// The returned size might be larger than the viewport if the child is too big to fit. + /// In this case the parent can use the values to calculate scroll. fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { - // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context - // that way render can use it None } diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index ca8303dd..46657fb9 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -241,11 +241,6 @@ impl Component for Markdown { } else if content_width > text_width { text_width = content_width; } - - if height >= viewport.1 { - height = viewport.1; - break; - } } Some((text_width + padding, height)) diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index a5310b23..c55f030f 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -15,6 +15,7 @@ pub struct Popup { contents: T, position: Option, size: (u16, u16), + child_size: (u16, u16), scroll: usize, id: &'static str, } @@ -25,6 +26,7 @@ impl Popup { contents, position: None, size: (0, 0), + child_size: (0, 0), scroll: 0, id, } @@ -70,6 +72,9 @@ impl Popup { pub fn scroll(&mut self, offset: usize, direction: bool) { if direction { self.scroll += offset; + + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = (self.scroll + offset).min(max_offset as usize); } else { self.scroll = self.scroll.saturating_sub(offset); } @@ -117,13 +122,21 @@ impl Component for Popup { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } - fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let max_width = 120.min(viewport.0); + let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport + let (width, height) = self .contents - .required_size((120, 26)) // max width, max height + .required_size((max_width, max_height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); - self.size = (width, height); + self.child_size = (width, height); + self.size = (width.min(max_width), height.min(max_height)); + + // re-clamp scroll offset + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = self.scroll.min(max_offset as usize); Some(self.size) } -- cgit v1.2.3-70-g09d2 From e91d357fae04766b9781fe51a0809d35175fe1cf Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 12 Dec 2021 07:16:48 -0500 Subject: Macros (#1234) * Macros WIP `helix_term::compositor::Callback` changed to take a `&mut Context` as a parameter for use by `play_macro` * Default to `@` register for macros * Import `KeyEvent` * Special-case shift-tab -> backtab in `KeyEvent` conversion * Move key recording to the compositor * Add comment * Add persistent display of macro recording status When macro recording is active, the pending keys display will be shifted 3 characters left, and the register being recorded to will be displayed between brackets — e.g., `[@]` — right of the pending keys display. * Fix/add documentation--- book/src/keymap.md | 2 ++ helix-term/src/commands.rs | 59 ++++++++++++++++++++++++++++++++++++++++++-- helix-term/src/compositor.rs | 9 +++++-- helix-term/src/keymap.rs | 3 +++ helix-term/src/ui/editor.rs | 22 +++++++++++++++-- helix-term/src/ui/menu.rs | 2 +- helix-term/src/ui/picker.rs | 2 +- helix-term/src/ui/popup.rs | 2 +- helix-term/src/ui/prompt.rs | 2 +- helix-view/src/editor.rs | 3 +++ helix-view/src/input.rs | 20 +++++++++++++++ 11 files changed, 116 insertions(+), 10 deletions(-) (limited to 'helix-term/src/ui/popup.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 5a804c3c..f0a2cb30 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -77,6 +77,8 @@ | `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` | | `Ctrl-a` | Increment object (number) under cursor | `increment` | | `Ctrl-x` | Decrement object (number) under cursor | `decrement` | +| `q` | Start/stop macro recording to the selected register | `record_macro` | +| `Q` | Play back a recorded macro from the selected register | `play_macro` | #### Shell diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 314cd11f..50554731 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -70,7 +70,7 @@ pub struct Context<'a> { impl<'a> Context<'a> { /// Push a new component onto the compositor. pub fn push_layer(&mut self, component: Box) { - self.callback = Some(Box::new(|compositor: &mut Compositor| { + self.callback = Some(Box::new(|compositor: &mut Compositor, _| { compositor.push(component) })); } @@ -395,6 +395,8 @@ impl MappableCommand { rename_symbol, "Rename symbol", increment, "Increment", decrement, "Decrement", + record_macro, "Record macro", + play_macro, "Play macro", ); } @@ -3441,7 +3443,7 @@ fn apply_workspace_edit( fn last_picker(cx: &mut Context) { // TODO: last picker does not seem to work well with buffer_picker - cx.callback = Some(Box::new(|compositor: &mut Compositor| { + cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { if let Some(picker) = compositor.last_picker.take() { compositor.push(picker); } @@ -5870,3 +5872,56 @@ fn increment_impl(cx: &mut Context, amount: i64) { doc.append_changes_to_history(view.id); } } + +fn record_macro(cx: &mut Context) { + if let Some((reg, mut keys)) = cx.editor.macro_recording.take() { + // Remove the keypress which ends the recording + keys.pop(); + let s = keys + .into_iter() + .map(|key| format!("{}", key)) + .collect::>() + .join(" "); + cx.editor.registers.get_mut(reg).write(vec![s]); + cx.editor + .set_status(format!("Recorded to register {}", reg)); + } else { + let reg = cx.register.take().unwrap_or('@'); + cx.editor.macro_recording = Some((reg, Vec::new())); + cx.editor + .set_status(format!("Recording to register {}", reg)); + } +} + +fn play_macro(cx: &mut Context) { + let reg = cx.register.unwrap_or('@'); + let keys = match cx + .editor + .registers + .get(reg) + .and_then(|reg| reg.read().get(0)) + .context("Register empty") + .and_then(|s| { + s.split_whitespace() + .map(str::parse::) + .collect::, _>>() + .context("Failed to parse macro") + }) { + Ok(keys) => keys, + Err(e) => { + cx.editor.set_error(format!("{}", e)); + return; + } + }; + let count = cx.count(); + + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, cx: &mut compositor::Context| { + for _ in 0..count { + for &key in keys.iter() { + compositor.handle_event(crossterm::event::Event::Key(key.into()), cx); + } + } + }, + )); +} diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 30554ebb..321f56a5 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect}; use crossterm::event::Event; use tui::buffer::Buffer as Surface; -pub type Callback = Box; +pub type Callback = Box; // --> EventResult should have a callback that takes a context with methods like .popup(), // .prompt() etc. That way we can abstract it from the renderer. @@ -131,12 +131,17 @@ impl Compositor { } pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { + // If it is a key event and a macro is being recorded, push the key event to the recording. + if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) { + keys.push(key.into()); + } + // propagate events through the layers until we either find a layer that consumes it or we // run out of layers (event bubbling) for layer in self.layers.iter_mut().rev() { match layer.handle_event(event, cx) { EventResult::Consumed(Some(callback)) => { - callback(self); + callback(self, cx); return true; } EventResult::Consumed(None) => return true, diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b1613252..257d5f29 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -593,6 +593,9 @@ impl Default for Keymaps { // paste_all "P" => paste_before, + "q" => record_macro, + "Q" => play_macro, + ">" => indent, "<" => unindent, "=" => format_selections, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 39ee15b4..bac1f171 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1100,13 +1100,31 @@ impl Component for EditorView { disp.push_str(&s); } } + let style = cx.editor.theme.get("ui.text"); + let macro_width = if cx.editor.macro_recording.is_some() { + 3 + } else { + 0 + }; surface.set_string( - area.x + area.width.saturating_sub(key_width), + area.x + area.width.saturating_sub(key_width + macro_width), area.y + area.height.saturating_sub(1), disp.get(disp.len().saturating_sub(key_width as usize)..) .unwrap_or(&disp), - cx.editor.theme.get("ui.text"), + style, ); + if let Some((reg, _)) = cx.editor.macro_recording { + let disp = format!("[{}]", reg); + let style = style + .fg(helix_view::graphics::Color::Yellow) + .add_modifier(Modifier::BOLD); + surface.set_string( + area.x + area.width.saturating_sub(3), + area.y + area.height.saturating_sub(1), + &disp, + style, + ); + } } if let Some(completion) = self.completion.as_mut() { diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 9a885a36..69053db3 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -190,7 +190,7 @@ impl Component for Menu { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 1c963f97..1ef94df0 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -404,7 +404,7 @@ impl Component for Picker { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.last_picker = compositor.pop(); }))); diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index c55f030f..bf7510a2 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -100,7 +100,7 @@ impl Component for Popup { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index a7ef231c..07e1b33c 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -426,7 +426,7 @@ impl Component for Prompt { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9034d12c..dcbcbe4f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2,6 +2,7 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, document::SCRATCH_BUFFER_NAME, graphics::{CursorKind, Rect}, + input::KeyEvent, theme::{self, Theme}, tree::{self, Tree}, Document, DocumentId, View, ViewId, @@ -160,6 +161,7 @@ pub struct Editor { pub count: Option, pub selected_register: Option, pub registers: Registers, + pub macro_recording: Option<(char, Vec)>, pub theme: Theme, pub language_servers: helix_lsp::Registry, pub clipboard_provider: Box, @@ -203,6 +205,7 @@ impl Editor { documents: BTreeMap::new(), count: None, selected_register: None, + macro_recording: None, theme: theme_loader.default(), language_servers, syn_loader, diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index b207c3ed..92caa517 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -234,6 +234,26 @@ impl From for KeyEvent { } } +#[cfg(feature = "term")] +impl From for crossterm::event::KeyEvent { + fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self { + if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) { + // special case for Shift-Tab -> BackTab + let mut modifiers = modifiers; + modifiers.remove(KeyModifiers::SHIFT); + crossterm::event::KeyEvent { + code: crossterm::event::KeyCode::BackTab, + modifiers: modifiers.into(), + } + } else { + crossterm::event::KeyEvent { + code: code.into(), + modifiers: modifiers.into(), + } + } + } +} + #[cfg(test)] mod test { use super::*; -- cgit v1.2.3-70-g09d2 From 094a0aa3f9877e3b1049f262e65c8efea2b7b73e Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 31 Jan 2022 13:42:32 +0900 Subject: Render code actions as a menu, allow adding padding to popup --- helix-term/src/commands.rs | 40 ++++++++++++++++++++++++++-------------- helix-term/src/ui/menu.rs | 15 +++++++++++---- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/popup.rs | 24 ++++++++++++++++++++---- helix-tui/src/widgets/block.rs | 2 +- 5 files changed, 59 insertions(+), 24 deletions(-) (limited to 'helix-term/src/ui/popup.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 96acb424..6bc2b9b6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -39,7 +39,7 @@ use movement::Movement; use crate::{ args, compositor::{self, Component, Compositor}, - ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent}, + ui::{self, FilePicker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; @@ -3463,6 +3463,15 @@ fn workspace_symbol_picker(cx: &mut Context) { ) } +impl ui::menu::Item for lsp::CodeActionOrCommand { + fn label(&self) -> &str { + match self { + lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(), + lsp::CodeActionOrCommand::Command(command) => command.title.as_str(), + } + } +} + pub fn code_action(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -3491,16 +3500,15 @@ pub fn code_action(cx: &mut Context) { return; } - let picker = Picker::new( - false, - actions, - |action| match action { - lsp::CodeActionOrCommand::CodeAction(action) => { - action.title.as_str().into() - } - lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), - }, - move |editor, code_action, _action| match code_action { + let picker = ui::Menu::new(actions, move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; + } + + // always present here + let code_action = code_action.unwrap(); + + match code_action { lsp::CodeActionOrCommand::Command(command) => { log::debug!("code action command: {:?}", command); execute_lsp_command(editor, command.clone()); @@ -3518,9 +3526,13 @@ pub fn code_action(cx: &mut Context) { execute_lsp_command(editor, command.clone()); } } - }, - ); - let popup = Popup::new("code-action", picker); + } + }); + let popup = + Popup::new("code-action", picker).margin(helix_view::graphics::Margin { + vertical: 1, + horizontal: 1, + }); compositor.push(Box::new(popup)) } }, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 9758732c..e127e09b 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -14,11 +14,18 @@ use helix_view::{graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item { - fn sort_text(&self) -> &str; - fn filter_text(&self) -> &str; - fn label(&self) -> &str; - fn row(&self) -> Row; + + fn sort_text(&self) -> &str { + self.label() + } + fn filter_text(&self) -> &str { + self.label() + } + + fn row(&self) -> Row { + Row::new(vec![Cell::from(self.label())]) + } } pub struct Menu { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 9ff9118f..5d650c65 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -2,7 +2,7 @@ mod completion; pub(crate) mod editor; mod info; mod markdown; -mod menu; +pub mod menu; mod picker; mod popup; mod prompt; diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index bf7510a2..4d319423 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -6,7 +6,7 @@ use crossterm::event::Event; use tui::buffer::Buffer as Surface; use helix_core::Position; -use helix_view::graphics::Rect; +use helix_view::graphics::{Margin, Rect}; // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box) @@ -14,6 +14,7 @@ use helix_view::graphics::Rect; pub struct Popup { contents: T, position: Option, + margin: Margin, size: (u16, u16), child_size: (u16, u16), scroll: usize, @@ -25,6 +26,10 @@ impl Popup { Self { contents, position: None, + margin: Margin { + vertical: 0, + horizontal: 0, + }, size: (0, 0), child_size: (0, 0), scroll: 0, @@ -36,6 +41,11 @@ impl Popup { self.position = pos; } + pub fn margin(mut self, margin: Margin) -> Self { + self.margin = margin; + self + } + pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { let position = self .position @@ -126,13 +136,18 @@ impl Component for Popup { let max_width = 120.min(viewport.0); let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport + let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin); + let (width, height) = self .contents - .required_size((max_width, max_height)) + .required_size((inner.width, inner.height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); self.child_size = (width, height); - self.size = (width.min(max_width), height.min(max_height)); + self.size = ( + (width + self.margin.horizontal * 2).min(max_width), + (height + self.margin.vertical * 2).min(max_height), + ); // re-clamp scroll offset let max_offset = self.child_size.1.saturating_sub(self.size.1); @@ -156,7 +171,8 @@ impl Component for Popup { let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - self.contents.render(area, surface, cx); + let inner = area.inner(&self.margin); + self.contents.render(inner, surface, cx); } fn id(&self) -> Option<&'static str> { diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs index 26223c3e..f084a324 100644 --- a/helix-tui/src/widgets/block.rs +++ b/helix-tui/src/widgets/block.rs @@ -134,7 +134,7 @@ impl<'a> Widget for Block<'a> { if area.area() == 0 { return; } - buf.set_style(area, self.style); + buf.clear_with(area, self.style); let symbols = BorderType::line_symbols(self.border_type); // Sides -- cgit v1.2.3-70-g09d2