From 055edfe96f42c585995afd9360466af51cf35e79 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 26 Jul 2026 10:07:10 +0900 Subject: [PATCH] =?UTF-8?q?tlc:=20Round=203=20=E2=80=94=20Macro=20replay?= =?UTF-8?q?=20engine=20(rebase=20fixup)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../tui/tlc/source/src/editor/dispatch.rs | 13 +- .../tui/tlc/source/src/editor/macro.rs | 132 ++++++++++++++++++ .../recipes/tui/tlc/source/src/editor/mod.rs | 8 +- local/recipes/tui/tlc/source/src/key/mod.rs | 23 +++ 4 files changed, 172 insertions(+), 4 deletions(-) diff --git a/local/recipes/tui/tlc/source/src/editor/dispatch.rs b/local/recipes/tui/tlc/source/src/editor/dispatch.rs index a299511616..b33a157590 100644 --- a/local/recipes/tui/tlc/source/src/editor/dispatch.rs +++ b/local/recipes/tui/tlc/source/src/editor/dispatch.rs @@ -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()); } diff --git a/local/recipes/tui/tlc/source/src/editor/macro.rs b/local/recipes/tui/tlc/source/src/editor/macro.rs index f53962c9cb..0bbeee4e16 100644 --- a/local/recipes/tui/tlc/source/src/editor/macro.rs +++ b/local/recipes/tui/tlc/source/src/editor/macro.rs @@ -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 { + 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 { + 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), +} + +/// In-flight macro replay state held by the editor. +#[derive(Debug, Default)] +pub struct MacroReplayEngine { + current: Option, + cached: Vec, + 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(&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) { + 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 { + 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) + } +} diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index c75eb5618e..3797eda201 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -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, } /// 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, } } diff --git a/local/recipes/tui/tlc/source/src/key/mod.rs b/local/recipes/tui/tlc/source/src/key/mod.rs index e090ee7486..2337b1f3d6 100644 --- a/local/recipes/tui/tlc/source/src/key/mod.rs +++ b/local/recipes/tui/tlc/source/src/key/mod.rs @@ -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(), + }; +}