aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOmnikar2021-12-12 12:16:48 +0000
committerGitHub2021-12-12 12:16:48 +0000
commite91d357fae04766b9781fe51a0809d35175fe1cf (patch)
tree338045ab80409343359b2bd7d980beb083843ff8
parent3156577fbf1a97e07e90e11b51c66155f122c3b7 (diff)
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
-rw-r--r--book/src/keymap.md2
-rw-r--r--helix-term/src/commands.rs59
-rw-r--r--helix-term/src/compositor.rs9
-rw-r--r--helix-term/src/keymap.rs3
-rw-r--r--helix-term/src/ui/editor.rs22
-rw-r--r--helix-term/src/ui/menu.rs2
-rw-r--r--helix-term/src/ui/picker.rs2
-rw-r--r--helix-term/src/ui/popup.rs2
-rw-r--r--helix-term/src/ui/prompt.rs2
-rw-r--r--helix-view/src/editor.rs3
-rw-r--r--helix-view/src/input.rs20
11 files changed, 116 insertions, 10 deletions
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<dyn Component>) {
- 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::<Vec<_>>()
+ .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::<KeyEvent>)
+ .collect::<Result<Vec<_>, _>>()
+ .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<dyn FnOnce(&mut Compositor)>;
+pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
// --> 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<T: Item + 'static> Component for Menu<T> {
_ => 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<T: 'static> Component for Picker<T> {
_ => 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<T: Component> Component for Popup<T> {
_ => 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<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
+ pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>,
@@ -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<crossterm::event::KeyEvent> for KeyEvent {
}
}
+#[cfg(feature = "term")]
+impl From<KeyEvent> 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::*;