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:
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user