Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Macros #1234

Merged
merged 8 commits into from
Dec 12, 2021
Merged

Macros #1234

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 76,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

Expand Down
59 changes: 57 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}));
}
Expand Down Expand Up @@ -394,6 394,8 @@ impl MappableCommand {
rename_symbol, "Rename symbol",
increment, "Increment",
decrement, "Decrement",
record_macro, "Record macro",
play_macro, "Play macro",
);
}

Expand Down Expand Up @@ -3437,7 3439,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);
}
Expand Down Expand Up @@ -5860,3 5862,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));
Comment on lines 5879 to 5882
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does our macro reuse the same register as the normal registers? IIRC vim macros have separate registers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We reuse the normal register since that means you can edit the macro as text (paste it in the buffer, edit, yank it back). I think vim also allowed for that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vim macros do not have separate registers.

}
}

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))
Comment on lines 5891 to 5892
Copy link
Contributor

@pickfire pickfire Dec 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just use cx.editor.registers.read(reg) helper.

.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);
}
}
},
));
}
9 changes: 7 additions & 2 deletions helix-term/src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -130,12 130,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) {
Omnikar marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down
3 changes: 3 additions & 0 deletions helix-term/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 593,9 @@ impl Default for Keymaps {
// paste_all
"P" => paste_before,

"q" => record_macro,
"Q" => play_macro,
Comment on lines 596 to 597
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be the reverse, Q should record macro and q should play macro like in vim.

Since it's a macro, it's expected to play more than record. And since play is expected to be used more frequently, it would be best use without extra shift.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, kakoune has this as well. I didn't realize it during review

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Vim, record is q and play is @; I changed it here because I figured it's more consistent. But yeah, swapping q and Q here does make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(it's also "replay" instead of "play" but I didn't have strong opinions there)

Copy link
Contributor Author

@Omnikar Omnikar Dec 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah sorry if I get stuff inconsistent with Kakoune, I've never used it and am not very familiar with it. Though in this situation, I do believe "play" makes very slightly more sense than "replay", since it is possible to play a macro without actually recording it, in which case playing it would not be a "replay".


">" => indent,
"<" => unindent,
"=" => format_selections,
Expand Down
22 changes: 20 additions & 2 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
})));
Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
})));
Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/ui/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 95,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();
})));
Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/ui/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
})));
Expand Down
3 changes: 3 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions helix-view/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
archseer marked this conversation as resolved.
Show resolved Hide resolved
} else {
crossterm::event::KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down