Skip to main content

hjkl_engine/
lib.rs

1//! Vim-mode editor engine built on top of [`hjkl_buffer`].
2//!
3//! Exposes an [`Editor`] that is fully toolkit-agnostic. Covers the bulk
4//! of vim's normal / insert / visual / visual-line / visual-block modes,
5//! text-object operators, dot-repeat, and ex-command handling
6//! (`:s/foo/bar/g`, `:w`, `:q`, `:noh`, ...). Rendering goes through
7//! `hjkl_buffer::BufferView`; selection / gutter highlights are painted in
8//! the same single-pass as text. TUI/crossterm adapters live in the
9//! `hjkl-engine-tui` companion crate.
10//!
11//! Imported wholesale from sqeel-vim with full git history. The trait
12//! extraction (Selection / SelectionSet / Buffer + Host sub-traits) lands
13//! progressively under [`crate::types`]. Pre-1.0 churn — the public surface
14//! may change in patch bumps. See [docs.rs](https://docs.rs/hjkl-engine) for
15//! the canonical API reference.
16//!
17//! The legacy public surface is intentionally narrow:
18//!
19//! - [`Editor`] — the editor widget.
20//! - [`KeybindingMode`] / [`VimMode`] — mode enums used by host apps.
21//! - [`ex::run`] / [`ex::ExEffect`] — drive ex-mode commands.
22
23pub mod abbrev;
24pub mod buf_helpers;
25mod buffer_impl;
26mod discipline;
27mod editor;
28pub mod input;
29pub mod keymap_motion;
30pub mod motions;
31mod registers;
32pub mod rope_util;
33pub mod search;
34pub mod selection_shift;
35pub mod substitute;
36pub mod tag;
37pub mod types;
38mod viewport_math;
39
40pub use discipline::{DisciplineState, NoDiscipline};
41pub use editor::{CursorScrollTarget, Editor, LspIntent, MarkJump, Settings, UndoGranularity};
42pub use input::{Input, Key, decode_macro, from_planned as decode_planned_input};
43pub use registers::{Registers, Slot};
44pub use selection_shift::{Sel, shift_position, shift_sel};
45
46pub use buffer_impl::{BufferFoldProvider, BufferFoldProviderMut, SnapshotFoldProvider};
47pub use keymap_motion::MotionKind;
48pub use substitute::{
49    SubstError, SubstFlags, SubstituteCmd, SubstituteMatch, SubstituteOutcome,
50    apply_collected_matches, apply_substitute, collect_substitute_matches, parse_substitute,
51};
52pub use types::{
53    Attrs, Buffer, BufferEdit, BufferId, Color, ContentEdit, Cursor, CursorShape, DefaultHost,
54    Edit, EditorSnapshot, EngineError, FoldOp, FoldProvider, Highlight, HighlightKind, Host,
55    Input as PlannedInput, Mode, Modifiers, MouseEvent, MouseKind, NoopFoldProvider, OptionValue,
56    Options, Pos, Query, RenderFrame, Search, Selection, SelectionKind, SelectionSet, SnapshotMode,
57    SpecialKey, Style, Viewport, WrapMode,
58};
59// The vim FSM itself now lives in `hjkl-vim` (#267). What stays here is the
60// engine-owned substrate it happens to use — abbreviations, the search prompt,
61// scroll/insert directions — plus the shared vocabulary types from
62// `hjkl-vim-types`, which both crates name and neither owns.
63pub use abbrev::{Abbrev, AbbrevTrigger};
64pub use search::SearchPrompt;
65pub use tag::matching_tag_pair;
66pub use types::{InsertDir, ScrollDir};
67
68pub use hjkl_vim_types::{
69    InsertEntry, InsertReason, InsertSession, LastChange, LastVisual, Motion, Operator, Pending,
70    RangeKind,
71};
72
73/// The FSM-internal mode discriminator used by `Editor::fsm_mode()` and
74/// `Editor::set_fsm_mode()`. Re-exported as `FsmMode` to avoid clashing with
75/// the `types::Mode` buffer-side enum that is already exported as `Mode`.
76///
77/// Used by `hjkl-vim::normal` and `hjkl-vim::dispatch_input` for mode
78/// comparisons.
79pub use hjkl_vim_types::Mode as FsmMode;
80
81// 0.0.32 dropped the `#[deprecated]` re-export aliases introduced at
82// 0.0.31 (`SpecBuffer`, `SpecBufferEdit`, `EditOp`, `PlannedViewport`).
83// Consumers must use the canonical names: `Buffer`, `BufferEdit`,
84// `Edit`, `Viewport`.
85
86/// Which keyboard discipline the editor uses.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum KeybindingMode {
89    #[default]
90    Vim,
91    /// Non-modal VSCode-style editing: always in "insert" mode, Ctrl+S saves,
92    /// Ctrl+Z/Y undo/redo. Selection/clipboard/find are tracked separately.
93    Vscode,
94}
95
96impl KeybindingMode {
97    /// Parse a config string into a [`KeybindingMode`]. Unrecognised values
98    /// fall back to `Vim` (same pattern as `hjkl_icons::IconMode::from_config`).
99    pub fn from_config(s: &str) -> Self {
100        match s {
101            "vscode" => KeybindingMode::Vscode,
102            _ => KeybindingMode::Vim,
103        }
104    }
105}
106
107#[cfg(feature = "serde")]
108impl serde::Serialize for KeybindingMode {
109    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
110        match self {
111            KeybindingMode::Vim => s.serialize_str("vim"),
112            KeybindingMode::Vscode => s.serialize_str("vscode"),
113        }
114    }
115}
116
117#[cfg(feature = "serde")]
118impl<'de> serde::Deserialize<'de> for KeybindingMode {
119    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
120        let raw = String::deserialize(d)?;
121        Ok(KeybindingMode::from_config(&raw))
122    }
123}
124
125/// Coarse vim-mode a host app can display in its status line.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
127pub enum VimMode {
128    #[default]
129    Normal,
130    Insert,
131    Visual,
132    VisualLine,
133    VisualBlock,
134}
135
136/// Discipline-agnostic coarse mode for app chrome (status badge, cursor
137/// shape) that must work the same whether the active keybinding discipline is
138/// vim, vscode, or a future helix/emacs. Unlike [`VimMode`] — which names
139/// vim-specific states — `CoarseMode` is the projection every discipline can
140/// express: "are we inserting text, selecting, in a command prompt, or idle?"
141///
142/// This is the seam app chrome reads instead of `VimMode` (epic #265 G3): the
143/// vim discipline maps its modes onto these; non-modal disciplines (vscode)
144/// project their own state. Today it is derived from [`VimMode`]; once the FSM
145/// state is pluggable, each discipline supplies its own projection.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
147pub enum CoarseMode {
148    /// Idle / command-ready (vim Normal).
149    #[default]
150    Normal,
151    /// Text is being inserted at the caret (vim Insert).
152    Insert,
153    /// A character-wise selection is active (vim Visual).
154    Select,
155    /// A line-wise selection is active (vim VisualLine).
156    SelectLine,
157    /// A block / column selection is active (vim VisualBlock).
158    SelectBlock,
159}
160
161/// A read-only *view* layered over the real input [`VimMode`]. Unlike a vim
162/// mode (which decides how keystrokes are interpreted), a `ViewMode` only
163/// changes what the buffer presents — input is still interpreted as Normal.
164///
165/// `Blame` is the git-blame overlay: the editor is read-only and the host
166/// renders per-commit framing. It is only meaningful while the input mode is
167/// `Normal`; any transition to Insert/Visual/etc. drops it back to `Normal`
168/// (see [`Editor::is_blame`]). New read-only overlays (diff, conflict, …)
169/// become additional variants here without touching `VimMode`.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum ViewMode {
172    #[default]
173    Normal,
174    Blame,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::KeybindingMode;
180
181    // ── KeybindingMode::from_config ────────────────────────────────────────
182
183    #[test]
184    fn from_config_vim_maps_to_vim() {
185        assert_eq!(KeybindingMode::from_config("vim"), KeybindingMode::Vim);
186    }
187
188    #[test]
189    fn from_config_vscode_maps_to_vscode() {
190        assert_eq!(
191            KeybindingMode::from_config("vscode"),
192            KeybindingMode::Vscode
193        );
194    }
195
196    #[test]
197    fn from_config_unknown_falls_back_to_vim() {
198        assert_eq!(KeybindingMode::from_config("emacs"), KeybindingMode::Vim);
199        assert_eq!(KeybindingMode::from_config(""), KeybindingMode::Vim);
200        assert_eq!(KeybindingMode::from_config("VSCode"), KeybindingMode::Vim);
201    }
202
203    #[test]
204    fn default_is_vim() {
205        assert_eq!(KeybindingMode::default(), KeybindingMode::Vim);
206    }
207
208    // ── Serde round-trip ───────────────────────────────────────────────────
209
210    #[cfg(feature = "serde")]
211    #[test]
212    fn serde_vim_round_trip() {
213        let json = serde_json::to_string(&KeybindingMode::Vim).unwrap();
214        assert_eq!(json, "\"vim\"");
215        let back: KeybindingMode = serde_json::from_str(&json).unwrap();
216        assert_eq!(back, KeybindingMode::Vim);
217    }
218
219    #[cfg(feature = "serde")]
220    #[test]
221    fn serde_vscode_round_trip() {
222        let json = serde_json::to_string(&KeybindingMode::Vscode).unwrap();
223        assert_eq!(json, "\"vscode\"");
224        let back: KeybindingMode = serde_json::from_str(&json).unwrap();
225        assert_eq!(back, KeybindingMode::Vscode);
226    }
227}