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
35pub use vim::VimLineEditor;
36
37use std::ops::Range;
38
39/// The contract between a line editor and its host application.
40///
41/// Implementations handle key interpretation and cursor management,
42/// while the host owns the text buffer and handles rendering.
43pub trait LineEditor {
44 /// Process a key event, returning edits to apply and any action requested.
45 ///
46 /// The `text` parameter is the current content - the editor uses it to
47 /// calculate motions but never modifies it directly.
48 fn handle_key(&mut self, key: Key, text: &str) -> EditResult;
49
50 /// Current cursor position as a byte offset into the text.
51 fn cursor(&self) -> usize;
52
53 /// Status text for display (e.g., "NORMAL", "INSERT", "-- VISUAL --").
54 fn status(&self) -> &str;
55
56 /// Selection range for highlighting, if in visual mode.
57 fn selection(&self) -> Option<Range<usize>>;
58
59 /// Reset editor state (call after submitting/clearing input).
60 fn reset(&mut self);
61
62 /// Set cursor position, clamped to valid bounds within text.
63 fn set_cursor(&mut self, pos: usize, text: &str);
64}
65
66/// Result of processing a key event.
67#[derive(Debug, Clone, Default)]
68pub struct EditResult {
69 /// Text mutations to apply, in order.
70 pub edits: Vec<TextEdit>,
71 /// Text that was yanked, if any (host can sync to clipboard).
72 pub yanked: Option<String>,
73 /// Action requested by the editor.
74 pub action: Option<Action>,
75}
76
77impl EditResult {
78 /// Create an empty result (no changes).
79 pub fn none() -> Self {
80 Self::default()
81 }
82
83 /// Create a result with a single action.
84 pub fn action(action: Action) -> Self {
85 Self {
86 action: Some(action),
87 ..Default::default()
88 }
89 }
90
91 /// Create a result with a cursor move (no text change).
92 pub fn cursor_only() -> Self {
93 Self::default()
94 }
95
96 /// Create a result with a single edit.
97 pub fn edit(edit: TextEdit) -> Self {
98 Self {
99 edits: vec![edit],
100 ..Default::default()
101 }
102 }
103
104 /// Create a result with edits and yanked text.
105 pub fn edit_and_yank(edit: TextEdit, yanked: String) -> Self {
106 Self {
107 edits: vec![edit],
108 yanked: Some(yanked),
109 ..Default::default()
110 }
111 }
112}
113
114/// Actions the editor can request from the host.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum Action {
117 /// User wants to submit the current input.
118 Submit,
119 /// User wants previous history entry.
120 HistoryPrev,
121 /// User wants next history entry.
122 HistoryNext,
123 /// User wants to cancel/abort.
124 Cancel,
125 /// User submitted an Ex-style command line (without the leading `:`).
126 SubmitCommand(String),
127}
128
129/// A single text mutation.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum TextEdit {
132 /// Delete text in the given byte range.
133 Delete { start: usize, end: usize },
134 /// Insert text at the given byte position.
135 Insert { at: usize, text: String },
136}
137
138impl TextEdit {
139 /// Apply this edit to a string.
140 pub fn apply(&self, s: &mut String) {
141 match self {
142 TextEdit::Delete { start, end } => {
143 s.replace_range(*start..*end, "");
144 }
145 TextEdit::Insert { at, text } => {
146 s.insert_str(*at, text);
147 }
148 }
149 }
150}
151
152/// A key event.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct Key {
155 pub code: KeyCode,
156 pub ctrl: bool,
157 pub alt: bool,
158 pub shift: bool,
159}
160
161impl Key {
162 /// Create a plain character key.
163 pub fn char(c: char) -> Self {
164 Self {
165 code: KeyCode::Char(c),
166 ctrl: false,
167 alt: false,
168 shift: false,
169 }
170 }
171
172 /// Create a key with just a code (no modifiers).
173 pub fn code(code: KeyCode) -> Self {
174 Self {
175 code,
176 ctrl: false,
177 alt: false,
178 shift: false,
179 }
180 }
181
182 /// Add ctrl modifier.
183 pub fn ctrl(mut self) -> Self {
184 self.ctrl = true;
185 self
186 }
187
188 /// Add shift modifier.
189 pub fn shift(mut self) -> Self {
190 self.shift = true;
191 self
192 }
193
194 /// Add alt modifier.
195 pub fn alt(mut self) -> Self {
196 self.alt = true;
197 self
198 }
199}
200
201/// Key codes for non-character keys.
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum KeyCode {
204 Char(char),
205 Escape,
206 Backspace,
207 Delete,
208 Left,
209 Right,
210 Up,
211 Down,
212 Home,
213 End,
214 Tab,
215 Enter,
216}