Skip to main content

hjkl_vim/
lib.rs

1pub mod cmd;
2pub mod count;
3#[cfg(debug_assertions)]
4mod curswant;
5pub mod descriptors;
6pub mod editor_ext;
7pub mod insert;
8pub mod motion;
9pub mod normal;
10pub mod operator;
11pub mod pending;
12pub mod search_prompt;
13mod step;
14pub mod vim;
15mod vim_state;
16
17pub use cmd::EngineCmd;
18pub use count::CountAccumulator;
19pub use editor_ext::VimEditorExt;
20pub use operator::OperatorKind;
21pub use pending::{Key, Outcome, PendingState, step};
22/// The byte budget one `p` / `P` may insert. Public so a host can report the
23/// limit it just hit rather than restating the number.
24pub use vim::command::MAX_PASTE_BYTES;
25/// The range a charwise Visual selection covers, for a host that runs the
26/// Visual operators itself: a selection ending on a line's NUL takes the line
27/// break, as vim's does.
28pub use vim::visual_ops::charwise_visual_range;
29/// Build an `Editor` that interprets keys as vim, or retro-fit the discipline
30/// onto one that already exists.
31///
32/// `Editor::new` leaves the discipline slot empty (the engine cannot name a
33/// concrete discipline), so an editor built through it ignores vim keys. Every
34/// vim-driven editor goes through one of these two (#267).
35pub use vim::{install as install_vim_discipline, vim_editor};
36
37/// Mode discriminator for the hjkl editor stack.
38///
39/// Used as the mode parameter in `hjkl-keymap`'s generic `Keymap<A, M: Mode>`.
40/// Satisfies the `hjkl_keymap::Mode` trait via its blanket impl for any
41/// `Copy + Eq + Hash + Debug` type.
42#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
43pub enum Mode {
44    Normal,
45    Insert,
46    Visual,
47    VisualLine,
48    VisualBlock,
49    OpPending,
50    CommandLine,
51}
52
53/// Drive the vim FSM with a [`hjkl_engine::PlannedInput`]. Translates the
54/// planned input to engine [`hjkl_engine::Input`], dispatches through
55/// [`dispatch_input`], and emits cursor-shape changes.
56///
57/// Returns `true` if the engine consumed the keystroke. Returns `false` for
58/// variants the legacy FSM does not dispatch (`Mouse`, `Paste`, `FocusGained`,
59/// `FocusLost`, `Resize`) and for special-key variants that map to `Key::Null`.
60pub fn feed_input<H: hjkl_engine::Host>(
61    editor: &mut hjkl_engine::Editor<hjkl_buffer::View, H>,
62    input: hjkl_engine::PlannedInput,
63) -> bool {
64    let Some(event) = hjkl_engine::decode_planned_input(input) else {
65        return false;
66    };
67    let consumed = dispatch_input(editor, event);
68    editor.emit_cursor_shape_if_changed();
69    consumed
70}
71
72/// Drive the vim FSM with one [`hjkl_engine::Input`].
73///
74/// This is the sole entry-point that decouples callers from the engine's
75/// internal FSM. Returns `true` if the engine consumed the keystroke.
76///
77/// # Phase 6.6c / 6.6d / 6.6e
78///
79/// Search-prompt mode (6.6c) is intercepted here before `begin_step` because
80/// it is a true short-circuit (no prelude/epilogue needed).
81///
82/// Insert mode (6.6d) is hosted in `hjkl-vim::insert::step_insert`.
83///
84/// Normal / Visual / VisualLine / VisualBlock / operator-pending modes (6.6e)
85/// are hosted in `hjkl-vim::normal::step_normal`. Both are wrapped with
86/// `begin_step` / `end_step` so macro recording, viewport scrolling, and
87/// `current_mode` sync all fire correctly.
88///
89/// In debug builds this is also where the `curswant` invariant is checked —
90/// see [`crate::curswant`]. Every keystroke that reaches the vim FSM passes
91/// through here, from the app (`hjkl_vim_tui::handle_key`), the compat-oracle
92/// driver, `:normal`, and macro replay alike, which makes it the one place
93/// the check has to live.
94pub fn dispatch_input<H: hjkl_engine::Host>(
95    editor: &mut hjkl_engine::Editor<hjkl_buffer::View, H>,
96    input: hjkl_engine::Input,
97) -> bool {
98    #[cfg(debug_assertions)]
99    let pre = curswant::capture(editor);
100    let consumed = dispatch_input_inner(editor, input);
101    #[cfg(debug_assertions)]
102    curswant::assert_invariant(editor, pre, input);
103    consumed
104}
105
106fn dispatch_input_inner<H: hjkl_engine::Host>(
107    editor: &mut hjkl_engine::Editor<hjkl_buffer::View, H>,
108    input: hjkl_engine::Input,
109) -> bool {
110    // Search-prompt intercept: short-circuits before begin_step because it
111    // needs no prelude/epilogue, except the recorder: a macro holds the
112    // pattern and its `<CR>` (`qq/g<CR>q` records `/g<CR>`, not `/`).
113    if editor.search_prompt_state().is_some() {
114        editor.record_input(input);
115        return search_prompt::step_search_prompt(editor, input);
116    }
117    // Run the prelude (timestamps, chord-timeout, macro-stop, snapshots).
118    let bk = match step::begin_step(editor, input) {
119        Ok(bk) => bk,
120        Err(consumed) => return consumed,
121    };
122    // Per-mode FSM dispatch — hjkl-vim hosts all modes.
123    let consumed = match editor.vim_mode() {
124        hjkl_engine::VimMode::Insert => insert::step_insert(editor, input),
125        _ => normal::step_normal(editor, input),
126    };
127    // Run the epilogue (marks, one-shot-normal, sync, recorder, mode sync).
128    step::end_step(editor, input, bk, consumed)
129}