* 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
pull/1255/head
Omnikar 3 years ago committed by GitHub
parent 3156577fbf
commit e91d357fae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -77,6 +77,8 @@
| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` | | `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
| `Ctrl-a` | Increment object (number) under cursor | `increment` | | `Ctrl-a` | Increment object (number) under cursor | `increment` |
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | | `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 #### Shell

@ -70,7 +70,7 @@ pub struct Context<'a> {
impl<'a> Context<'a> { impl<'a> Context<'a> {
/// Push a new component onto the compositor. /// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) { 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) compositor.push(component)
})); }));
} }
@ -395,6 +395,8 @@ impl MappableCommand {
rename_symbol, "Rename symbol", rename_symbol, "Rename symbol",
increment, "Increment", increment, "Increment",
decrement, "Decrement", decrement, "Decrement",
record_macro, "Record macro",
play_macro, "Play macro",
); );
} }
@ -3441,7 +3443,7 @@ fn apply_workspace_edit(
fn last_picker(cx: &mut Context) { fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker // 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() { if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker); compositor.push(picker);
} }
@ -5870,3 +5872,56 @@ fn increment_impl(cx: &mut Context, amount: i64) {
doc.append_changes_to_history(view.id); 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);
}
}
},
));
}

@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event; use crossterm::event::Event;
use tui::buffer::Buffer as Surface; 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(), // --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer. // .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 { 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 // propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling) // run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() { for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) { match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => { EventResult::Consumed(Some(callback)) => {
callback(self); callback(self, cx);
return true; return true;
} }
EventResult::Consumed(None) => return true, EventResult::Consumed(None) => return true,

@ -593,6 +593,9 @@ impl Default for Keymaps {
// paste_all // paste_all
"P" => paste_before, "P" => paste_before,
"q" => record_macro,
"Q" => play_macro,
">" => indent, ">" => indent,
"<" => unindent, "<" => unindent,
"=" => format_selections, "=" => format_selections,

@ -1100,13 +1100,31 @@ impl Component for EditorView {
disp.push_str(&s); 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( 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), area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..) disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp), .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() { if let Some(completion) = self.completion.as_mut() {

@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));

@ -404,7 +404,7 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.last_picker = compositor.pop(); compositor.last_picker = compositor.pop();
}))); })));

@ -100,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));

@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));

@ -2,6 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::SCRATCH_BUFFER_NAME, document::SCRATCH_BUFFER_NAME,
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
input::KeyEvent,
theme::{self, Theme}, theme::{self, Theme},
tree::{self, Tree}, tree::{self, Tree},
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
@ -160,6 +161,7 @@ pub struct Editor {
pub count: Option<std::num::NonZeroUsize>, pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>, pub selected_register: Option<char>,
pub registers: Registers, pub registers: Registers,
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme, pub theme: Theme,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>, pub clipboard_provider: Box<dyn ClipboardProvider>,
@ -203,6 +205,7 @@ impl Editor {
documents: BTreeMap::new(), documents: BTreeMap::new(),
count: None, count: None,
selected_register: None, selected_register: None,
macro_recording: None,
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers, language_servers,
syn_loader, syn_loader,

@ -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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

Loading…
Cancel
Save