tlc: Round 3 — Macro replay engine (rebase fixup)

After the v5.3 driver-manager rebase rolled back the macro_replay
additions from Round 2, this commit re-adds them:

- src/editor/macro.rs: MacroReplayEngine, ReplayPass, replay_key_to_runtime,
  replay_keys_to_runtime, MacroStoreLike. Translate recorded NamedKey
  events to runtime Key events and feed them one per call.
- src/key/mod.rs: Key::HOME, Key::END, Key::PAGEUP, Key::PAGEDOWN
  constants (replay translation needs them).
- src/editor/mod.rs: editor.macro_replay field + re-exports.
- src/editor/dispatch.rs: EditorCmd::MacroRepeat now loads the recorded
  buffer into a MacroReplayEngine and stores the engine on the editor
  for the handler to consume one step at a time.

The next commit will wire the engine into handle_key to actually
play back macros step-by-step.

Tests: 1486 passing (was 1486). No regression.
This commit is contained in:
Sisyphus
2026-07-26 10:07:10 +09:00
parent 47cbc4d34d
commit 055edfe96f
4 changed files with 172 additions and 4 deletions
@@ -526,9 +526,16 @@ impl Editor {
}
EditorCmd::MacroRepeat => {
if let Some(steps) = self.macro_buffer.clone() {
let count = steps.len();
self.message =
Some(format!("Macro repeat requires replay engine (not yet wired; {count} steps cached)"));
if steps.is_empty() {
self.message = Some("Macro buffer is empty".to_string());
} else {
let mut engine = crate::editor::macros::MacroReplayEngine::new();
engine.begin_steps(steps);
let count = engine.remaining();
self.macro_replay = Some(engine);
self.message =
Some(format!("Macro replay started: {count} step(s) queued"));
}
} else {
self.message = Some("No macro recorded".to_string());
}
@@ -682,3 +682,135 @@ mod tests {
);
}
}
/// Translate a recorded [`NamedKey`] into the runtime [`crate::key::Key`]
/// the editor's `handle_key` accepts.
#[must_use]
pub fn replay_key_to_runtime(nk: &NamedKey) -> Option<crate::key::Key> {
use crate::key::{Key, Modifiers};
let mut k = match nk {
NamedKey::Char(c) => Key::from_char(*c),
NamedKey::F(n) => Key::f(*n),
NamedKey::Special(SpecialKey::Enter) => Key::ENTER,
NamedKey::Special(SpecialKey::Backspace) => Key::BACKSPACE,
NamedKey::Special(SpecialKey::Delete) => Key::DELETE,
NamedKey::Special(SpecialKey::Tab) => Key::TAB,
NamedKey::Special(SpecialKey::Esc) => Key::ESCAPE,
NamedKey::Special(SpecialKey::Up) => Key::UP,
NamedKey::Special(SpecialKey::Down) => Key::DOWN,
NamedKey::Special(SpecialKey::Left) => Key::LEFT,
NamedKey::Special(SpecialKey::Right) => Key::RIGHT,
NamedKey::Special(SpecialKey::Home) => Key::HOME,
NamedKey::Special(SpecialKey::End) => Key::END,
NamedKey::Special(SpecialKey::PageUp) => Key::PAGEUP,
NamedKey::Special(SpecialKey::PageDown) => Key::PAGEDOWN,
_ => return None,
};
if let NamedKey::Ctrl(c) = nk {
k.mods |= Modifiers::CTRL;
let _ = c;
}
if let NamedKey::Alt(c) = nk {
k.mods |= Modifiers::ALT;
let _ = c;
}
Some(k)
}
/// Translate a recorded macro into a sequence of runtime keys.
#[must_use]
pub fn replay_keys_to_runtime(keys: &[NamedKey]) -> Vec<crate::key::Key> {
keys.iter().filter_map(replay_key_to_runtime).collect()
}
/// One pass of a macro playback.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplayPass {
Named(String),
Steps(Vec<NamedKey>),
}
/// In-flight macro replay state held by the editor.
#[derive(Debug, Default)]
pub struct MacroReplayEngine {
current: Option<ReplayPass>,
cached: Vec<NamedKey>,
cursor: usize,
skipped: usize,
}
impl MacroReplayEngine {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_idle(&self) -> bool {
self.current.is_none()
}
#[must_use]
pub fn remaining(&self) -> usize {
self.cached.len().saturating_sub(self.cursor)
}
pub fn begin_named<S: MacroStoreLike>(&mut self, name: &str, store: &S) {
if let Some(m) = store.peek_macro(name) {
self.cached = m.keys.clone();
self.cursor = 0;
self.skipped = 0;
self.current = Some(ReplayPass::Named(name.to_string()));
} else {
self.current = None;
}
}
pub fn begin_steps(&mut self, keys: Vec<NamedKey>) {
self.cached = keys;
self.cursor = 0;
self.skipped = 0;
self.current = Some(ReplayPass::Steps(self.cached.clone()));
}
/// Pop the next runtime key to dispatch.
pub fn next_key(&mut self) -> Option<crate::key::Key> {
if self.current.is_none() {
return None;
}
if self.cursor >= self.cached.len() {
return None;
}
let nk = self.cached[self.cursor].clone();
self.cursor += 1;
match replay_key_to_runtime(&nk) {
Some(k) => Some(k),
None => {
self.skipped += 1;
self.next_key()
}
}
}
/// Mark the current pass as complete.
pub fn finish(&mut self) -> Option<(usize, usize)> {
let total = self.cached.len();
let skipped = self.skipped;
self.current = None;
self.cached.clear();
self.cursor = 0;
self.skipped = 0;
Some((total, skipped))
}
}
/// Minimal read-only view of a macro store used by the replay engine.
pub trait MacroStoreLike {
fn peek_macro(&self, name: &str) -> Option<&Macro>;
}
impl MacroStoreLike for MacroStore {
fn peek_macro(&self, name: &str) -> Option<&Macro> {
self.peek(name)
}
}
@@ -72,7 +72,7 @@ pub use completion::{Completer, Completion, CompletionMode};
pub use cursor::Cursor;
pub use cursor_shape::CursorShape;
pub use history::History;
pub use macros::{validate_name, Macro, MacroRecorder, MacroStore, NamedKey, SpecialKey};
pub use macros::{replay_key_to_runtime, replay_keys_to_runtime, validate_name, Macro, MacroRecorder, MacroReplayEngine, MacroStore, NamedKey, ReplayPass, SpecialKey};
pub use mode::{Mode, PromptKind};
pub use prompt::PromptInput;
pub use view::EditorView;
@@ -253,6 +253,10 @@ pub struct Editor {
/// Stack of byte offsets saved before each `Ctrl-]` jump, so
/// `Ctrl-T` can pop back. Drained on close.
declaration_stack: Vec<(usize, u32)>,
/// Macro replay engine: when `Some`, the next `handle_key` call
/// consumes the next key from the active pass and dispatches it.
/// After a pass completes the engine is reset to `None`.
pub macro_replay: Option<macros::MacroReplayEngine>,
}
/// One in-flight smooth-scroll animation.
@@ -333,6 +337,7 @@ impl Editor {
macro_buffer: None,
macro_count: 0,
declaration_stack: Vec::new(),
macro_replay: None,
}
}
@@ -390,6 +395,7 @@ impl Editor {
macro_buffer: None,
macro_count: 0,
declaration_stack: Vec::new(),
macro_replay: None,
}
}
@@ -120,3 +120,26 @@ impl Key {
}
}
}
impl Key {
/// The "Home" key.
pub const HOME: Key = Key {
code: 0x21A1,
mods: Modifiers::empty(),
};
/// The "End" key.
pub const END: Key = Key {
code: 0x21A2,
mods: Modifiers::empty(),
};
/// The "Page Up" key.
pub const PAGEUP: Key = Key {
code: 0x21DE,
mods: Modifiers::empty(),
};
/// The "Page Down" key.
pub const PAGEDOWN: Key = Key {
code: 0x21DF,
mods: Modifiers::empty(),
};
}