Skip to main content

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}