mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 17:36:19 +04:00
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
This commit is contained in:
parent
3156577fbf
commit
e91d357fae
@ -77,6 +77,8 @@ ### Changes
|
||||
| `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
|
||||
|
||||
|
@ -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 @@ pub fn doc(&self) -> &str {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
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 @@ pub fn pop(&mut self) -> Option<Box<dyn Component>> {
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -593,6 +593,9 @@ fn default() -> Keymaps {
|
||||
// paste_all
|
||||
"P" => paste_before,
|
||||
|
||||
"q" => record_macro,
|
||||
"Q" => play_macro,
|
||||
|
||||
">" => indent,
|
||||
"<" => unindent,
|
||||
"=" => format_selections,
|
||||
|
@ -1100,13 +1100,31 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
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() {
|
||||
|
@ -190,7 +190,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
_ => 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();
|
||||
})));
|
||||
|
@ -404,7 +404,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
_ => 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();
|
||||
})));
|
||||
|
@ -100,7 +100,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
_ => 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();
|
||||
})));
|
||||
|
@ -426,7 +426,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
_ => 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();
|
||||
})));
|
||||
|
@ -2,6 +2,7 @@
|
||||
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 @@ pub fn new(
|
||||
documents: BTreeMap::new(),
|
||||
count: None,
|
||||
selected_register: None,
|
||||
macro_recording: None,
|
||||
theme: theme_loader.default(),
|
||||
language_servers,
|
||||
syn_loader,
|
||||
|
@ -234,6 +234,26 @@ fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEve
|
||||
}
|
||||
}
|
||||
|
||||
#[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::*;
|
||||
|
Loading…
Reference in New Issue
Block a user