vim_line/lib.rs
1//! vim-line: A line-oriented vim motions library for TUI applications
2//!
3//! This crate provides a trait-based interface for line editing with vim-style
4//! keybindings. It's designed for "one-shot" editing scenarios like REPLs,
5//! chat inputs, and command lines - not full buffer/file editing.
6//!
7//! # Design Philosophy
8//!
9//! - **Host-agnostic**: The library doesn't know about terminals or rendering
10//! - **Command-based**: Returns mutations for the host to apply
11//! - **Caller owns text**: The library never stores your text buffer
12//! - **Multi-line aware**: Supports inputs with newlines that grow/shrink
13//!
14//! # Example
15//!
16//! ```
17//! use vim_line::{LineEditor, VimLineEditor, Key, KeyCode};
18//!
19//! let mut editor = VimLineEditor::new();
20//! let mut text = String::from("hello world");
21//!
22//! // Process 'dw' to delete word
23//! let _ = editor.handle_key(Key::char('d'), &text);
24//! let result = editor.handle_key(Key::char('w'), &text);
25//!
26//! // Apply edits
27//! for edit in result.edits.into_iter().rev() {
28//! edit.apply(&mut text);
29//! }
30//! // text is now "world"
31//! ```
32
33mod vim;
34
35#[cfg(feature = "history")]
36pub mod history;
37
38pub use vim::VimLineEditor;
39
40use std::ops::Range;
41
42/// The contract between a line editor and its host application.
43///
44/// Implementations handle key interpretation and cursor management,
45/// while the host owns the text buffer and handles rendering.
46pub trait LineEditor {
47 /// Process a key event, returning edits to apply and any action requested.
48 ///
49 /// The `text` parameter is the current content - the editor uses it to
50 /// calculate motions but never modifies it directly.
51 fn handle_key(&mut self, key: Key, text: &str) -> EditResult;
52
53 /// Current cursor position as a byte offset into the text.
54 fn cursor(&self) -> usize;
55
56 /// Status text for display (e.g., "NORMAL", "INSERT", "-- VISUAL --").
57 fn status(&self) -> &str;
58
59 /// Selection range for highlighting, if in visual mode.
60 fn selection(&self) -> Option<Range<usize>>;
61
62 /// Reset editor state (call after submitting/clearing input).
63 fn reset(&mut self);
64
65 /// Set cursor position, clamped to valid bounds within text.
66 fn set_cursor(&mut self, pos: usize, text: &str);
67}
68
69/// Result of processing a key event.
70#[derive(Debug, Clone, Default)]
71pub struct EditResult {
72 /// Text mutations to apply, in order.
73 pub edits: Vec<TextEdit>,
74 /// Text that was yanked, if any (host can sync to clipboard).
75 pub yanked: Option<String>,
76 /// Action requested by the editor.
77 pub action: Option<Action>,
78}
79
80impl EditResult {
81 /// Create an empty result (no changes).
82 pub fn none() -> Self {
83 Self::default()
84 }
85
86 /// Create a result with a single action.
87 pub fn action(action: Action) -> Self {
88 Self {
89 action: Some(action),
90 ..Default::default()
91 }
92 }
93
94 /// Create a result with a cursor move (no text change).
95 pub fn cursor_only() -> Self {
96 Self::default()
97 }
98
99 /// Create a result with a single edit.
100 pub fn edit(edit: TextEdit) -> Self {
101 Self {
102 edits: vec![edit],
103 ..Default::default()
104 }
105 }
106
107 /// Create a result with edits and yanked text.
108 pub fn edit_and_yank(edit: TextEdit, yanked: String) -> Self {
109 Self {
110 edits: vec![edit],
111 yanked: Some(yanked),
112 ..Default::default()
113 }
114 }
115}
116
117/// Actions the editor can request from the host.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum Action {
120 /// User wants to submit the current input.
121 Submit,
122 /// User wants previous history entry. Emitted by `Up` in any mode and
123 /// by `k` in Normal mode when the cursor is on the first line of the
124 /// buffer (single-line REPL inputs are always at the first line).
125 HistoryPrev,
126 /// User wants next history entry. Emitted by `Down` in any mode and by
127 /// `j` in Normal mode when the cursor is on the last line of the buffer.
128 HistoryNext,
129 /// User is incrementally building a history-search query. The string is
130 /// the *current full query*, not a delta — hosts feed it to
131 /// `history::Store::search` and preview the top match.
132 ///
133 /// Reserved for a future search sub-mode; not currently emitted by
134 /// `vim-line` itself. Hosts that ship their own `/` input loop can
135 /// emit this from their dispatch to keep the contract single-vocabulary.
136 HistorySearch(String),
137 /// User accepted the previewed history match. Reserved for the future
138 /// search sub-mode; not currently emitted by `vim-line`.
139 HistoryAccept,
140 /// User canceled a history search and wants the pre-search input
141 /// restored. Reserved for the future search sub-mode; not currently
142 /// emitted by `vim-line`.
143 HistoryCancel,
144 /// User wants to cancel/abort.
145 Cancel,
146 /// User submitted an Ex-style command line (without the leading `:`).
147 SubmitCommand(String),
148}
149
150/// A single text mutation.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum TextEdit {
153 /// Delete text in the given byte range.
154 Delete { start: usize, end: usize },
155 /// Insert text at the given byte position.
156 Insert { at: usize, text: String },
157}
158
159impl TextEdit {
160 /// Apply this edit to a string.
161 pub fn apply(&self, s: &mut String) {
162 match self {
163 TextEdit::Delete { start, end } => {
164 s.replace_range(*start..*end, "");
165 }
166 TextEdit::Insert { at, text } => {
167 s.insert_str(*at, text);
168 }
169 }
170 }
171}
172
173/// A key event.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct Key {
176 pub code: KeyCode,
177 pub ctrl: bool,
178 pub alt: bool,
179 pub shift: bool,
180}
181
182impl Key {
183 /// Create a plain character key.
184 pub fn char(c: char) -> Self {
185 Self {
186 code: KeyCode::Char(c),
187 ctrl: false,
188 alt: false,
189 shift: false,
190 }
191 }
192
193 /// Create a key with just a code (no modifiers).
194 pub fn code(code: KeyCode) -> Self {
195 Self {
196 code,
197 ctrl: false,
198 alt: false,
199 shift: false,
200 }
201 }
202
203 /// Add ctrl modifier.
204 pub fn ctrl(mut self) -> Self {
205 self.ctrl = true;
206 self
207 }
208
209 /// Add shift modifier.
210 pub fn shift(mut self) -> Self {
211 self.shift = true;
212 self
213 }
214
215 /// Add alt modifier.
216 pub fn alt(mut self) -> Self {
217 self.alt = true;
218 self
219 }
220}
221
222/// Key codes for non-character keys.
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum KeyCode {
225 Char(char),
226 Escape,
227 Backspace,
228 Delete,
229 Left,
230 Right,
231 Up,
232 Down,
233 Home,
234 End,
235 Tab,
236 Enter,
237}