From 3712df333eb4f88ea5346c304cbef7d7b0c12f4f Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 5 Dec 2021 16:55:04 -0500 Subject: [PATCH] Macros WIP `helix-term::compositor::Callback` changed to take a `&mut Context` as a parameter for use by `play_macro` --- helix-term/src/commands.rs | 59 ++++++++++++++++++++++++++++++++++-- helix-term/src/compositor.rs | 4 +-- helix-term/src/keymap.rs | 3 ++ helix-term/src/ui/editor.rs | 4 +++ 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 | 2 ++ helix-view/src/input.rs | 10 ++++++ 10 files changed, 82 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3d583ba8a40da..d32081b9379cc 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) })); } @@ -394,6 +394,8 @@ impl MappableCommand { rename_symbol, "Rename symbol", increment, "Increment", decrement, "Decrement", + record_macro, "Toggle macro recording", + play_macro, "Play back a recorded macro", ); } @@ -3430,7 +3432,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); } @@ -5849,3 +5851,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 3a644750e73e4..978c5bc24339c 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,7 +131,7 @@ impl Compositor { 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 9debbbacf8ac3..70e0f448c9348 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 39ee15b4ccf3b..b8c3430288903 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -947,6 +947,10 @@ impl Component for EditorView { let (_, doc) = current!(cxt.editor); let mode = doc.mode(); + if let Some((_, keys)) = &mut cxt.editor.macro_recording { + keys.push(key); + } + if let Some(on_next_key) = self.on_next_key.take() { // if there's a command waiting input, do that first on_next_key(&mut cxt, key); diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index e891c1492e616..0b0afbb25718e 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 16bf08a3eedbc..977f039bc9d8c 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 8f7921a116d9e..cd2ac63024f36 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -93,7 +93,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 e90b0772785ea..9221b34503b7b 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 9034d12c8979c..7382cd4122519 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -160,6 +160,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 +204,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 580204ccca7c7..4126f531080b9 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -230,6 +230,16 @@ impl From for KeyEvent { } } +#[cfg(feature = "term")] +impl From for crossterm::event::KeyEvent { + fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self { + crossterm::event::KeyEvent { + code: code.into(), + modifiers: modifiers.into(), + } + } +} + #[cfg(test)] mod test { use super::*;