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
23mod buf_helpers;
24mod buffer_impl;
25mod discipline;
26mod editor;
27pub mod input;
28pub mod keymap_motion;
29pub mod motions;
30mod registers;
31pub mod search;
32pub mod substitute;
33pub mod types;
34mod viewport_math;
35pub mod vim;
36
37pub use discipline::{DisciplineState, NoDiscipline};
38pub use editor::{Editor, LspIntent, MarkJump, UndoGranularity};
39pub use input::{Input, Key, decode_macro, from_planned as decode_planned_input};
40pub use registers::{Registers, Slot};
41
42pub use buffer_impl::{BufferFoldProvider, BufferFoldProviderMut};
43pub use keymap_motion::MotionKind;
44pub use substitute::{
45 SubstError, SubstFlags, SubstituteCmd, SubstituteMatch, SubstituteOutcome,
46 apply_collected_matches, apply_substitute, collect_substitute_matches, parse_substitute,
47};
48pub use types::{
49 Attrs, Buffer, BufferEdit, BufferId, Color, ContentEdit, Cursor, CursorShape, DefaultHost,
50 Edit, EditorSnapshot, EngineError, FoldOp, FoldProvider, Highlight, HighlightKind, Host,
51 Input as PlannedInput, Mode, Modifiers, MouseEvent, MouseKind, NoopFoldProvider, OptionValue,
52 Options, Pos, Query, RenderFrame, Search, Selection, SelectionKind, SelectionSet, SnapshotMode,
53 SpecialKey, Style, Viewport, WrapMode,
54};
55pub use vim::{
56 Abbrev, AbbrevTrigger, InsertDir, InsertEntry, InsertReason, InsertSession, LastChange,
57 LastVisual, Motion, Operator, Pending, RangeKind, ScrollDir, SearchPrompt, op_is_change,
58 parse_motion,
59};
60
61/// The FSM-internal mode discriminator used by `Editor::fsm_mode()` and
62/// `Editor::set_fsm_mode()`. Re-exported as `FsmMode` to avoid clashing with
63/// the `types::Mode` buffer-side enum that is already exported as `Mode`.
64///
65/// Used by `hjkl-vim::normal` and `hjkl-vim::dispatch_input` for mode
66/// comparisons.
67pub use vim::Mode as FsmMode;
68
69// 0.0.32 dropped the `#[deprecated]` re-export aliases introduced at
70// 0.0.31 (`SpecBuffer`, `SpecBufferEdit`, `EditOp`, `PlannedViewport`).
71// Consumers must use the canonical names: `Buffer`, `BufferEdit`,
72// `Edit`, `Viewport`.
73
74/// Which keyboard discipline the editor uses.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub enum KeybindingMode {
77 #[default]
78 Vim,
79 /// Non-modal VSCode-style editing: always in "insert" mode, Ctrl+S saves,
80 /// Ctrl+Z/Y undo/redo. Selection/clipboard/find are tracked separately.
81 Vscode,
82}
83
84impl KeybindingMode {
85 /// Parse a config string into a [`KeybindingMode`]. Unrecognised values
86 /// fall back to `Vim` (same pattern as `hjkl_icons::IconMode::from_config`).
87 pub fn from_config(s: &str) -> Self {
88 match s {
89 "vscode" => KeybindingMode::Vscode,
90 _ => KeybindingMode::Vim,
91 }
92 }
93}
94
95#[cfg(feature = "serde")]
96impl serde::Serialize for KeybindingMode {
97 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
98 match self {
99 KeybindingMode::Vim => s.serialize_str("vim"),
100 KeybindingMode::Vscode => s.serialize_str("vscode"),
101 }
102 }
103}
104
105#[cfg(feature = "serde")]
106impl<'de> serde::Deserialize<'de> for KeybindingMode {
107 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
108 let raw = String::deserialize(d)?;
109 Ok(KeybindingMode::from_config(&raw))
110 }
111}
112
113/// Coarse vim-mode a host app can display in its status line.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum VimMode {
116 #[default]
117 Normal,
118 Insert,
119 Visual,
120 VisualLine,
121 VisualBlock,
122}
123
124/// Discipline-agnostic coarse mode for app chrome (status badge, cursor
125/// shape) that must work the same whether the active keybinding discipline is
126/// vim, vscode, or a future helix/emacs. Unlike [`VimMode`] — which names
127/// vim-specific states — `CoarseMode` is the projection every discipline can
128/// express: "are we inserting text, selecting, in a command prompt, or idle?"
129///
130/// This is the seam app chrome reads instead of `VimMode` (epic #265 G3): the
131/// vim discipline maps its modes onto these; non-modal disciplines (vscode)
132/// project their own state. Today it is derived from [`VimMode`]; once the FSM
133/// state is pluggable, each discipline supplies its own projection.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum CoarseMode {
136 /// Idle / command-ready (vim Normal).
137 #[default]
138 Normal,
139 /// Text is being inserted at the caret (vim Insert).
140 Insert,
141 /// A character-wise selection is active (vim Visual).
142 Select,
143 /// A line-wise selection is active (vim VisualLine).
144 SelectLine,
145 /// A block / column selection is active (vim VisualBlock).
146 SelectBlock,
147}
148
149/// A read-only *view* layered over the real input [`VimMode`]. Unlike a vim
150/// mode (which decides how keystrokes are interpreted), a `ViewMode` only
151/// changes what the buffer presents — input is still interpreted as Normal.
152///
153/// `Blame` is the git-blame overlay: the editor is read-only and the host
154/// renders per-commit framing. It is only meaningful while the input mode is
155/// `Normal`; any transition to Insert/Visual/etc. drops it back to `Normal`
156/// (see [`Editor::is_blame`]). New read-only overlays (diff, conflict, …)
157/// become additional variants here without touching `VimMode`.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
159pub enum ViewMode {
160 #[default]
161 Normal,
162 Blame,
163}
164
165#[cfg(test)]
166mod tests {
167 use super::KeybindingMode;
168
169 // ── KeybindingMode::from_config ────────────────────────────────────────
170
171 #[test]
172 fn from_config_vim_maps_to_vim() {
173 assert_eq!(KeybindingMode::from_config("vim"), KeybindingMode::Vim);
174 }
175
176 #[test]
177 fn from_config_vscode_maps_to_vscode() {
178 assert_eq!(
179 KeybindingMode::from_config("vscode"),
180 KeybindingMode::Vscode
181 );
182 }
183
184 #[test]
185 fn from_config_unknown_falls_back_to_vim() {
186 assert_eq!(KeybindingMode::from_config("emacs"), KeybindingMode::Vim);
187 assert_eq!(KeybindingMode::from_config(""), KeybindingMode::Vim);
188 assert_eq!(KeybindingMode::from_config("VSCode"), KeybindingMode::Vim);
189 }
190
191 #[test]
192 fn default_is_vim() {
193 assert_eq!(KeybindingMode::default(), KeybindingMode::Vim);
194 }
195
196 // ── Serde round-trip ───────────────────────────────────────────────────
197
198 #[cfg(feature = "serde")]
199 #[test]
200 fn serde_vim_round_trip() {
201 let json = serde_json::to_string(&KeybindingMode::Vim).unwrap();
202 assert_eq!(json, "\"vim\"");
203 let back: KeybindingMode = serde_json::from_str(&json).unwrap();
204 assert_eq!(back, KeybindingMode::Vim);
205 }
206
207 #[cfg(feature = "serde")]
208 #[test]
209 fn serde_vscode_round_trip() {
210 let json = serde_json::to_string(&KeybindingMode::Vscode).unwrap();
211 assert_eq!(json, "\"vscode\"");
212 let back: KeybindingMode = serde_json::from_str(&json).unwrap();
213 assert_eq!(back, KeybindingMode::Vscode);
214 }
215}