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, Copy, 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}
126
127/// A single text mutation.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum TextEdit {
130 /// Delete text in the given byte range.
131 Delete { start: usize, end: usize },
132 /// Insert text at the given byte position.
133 Insert { at: usize, text: String },
134}
135
136impl TextEdit {
137 /// Apply this edit to a string.
138 pub fn apply(&self, s: &mut String) {
139 match self {
140 TextEdit::Delete { start, end } => {
141 s.replace_range(*start..*end, "");
142 }
143 TextEdit::Insert { at, text } => {
144 s.insert_str(*at, text);
145 }
146 }
147 }
148}
149
150/// A key event.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct Key {
153 pub code: KeyCode,
154 pub ctrl: bool,
155 pub alt: bool,
156 pub shift: bool,
157}
158
159impl Key {
160 /// Create a plain character key.
161 pub fn char(c: char) -> Self {
162 Self {
163 code: KeyCode::Char(c),
164 ctrl: false,
165 alt: false,
166 shift: false,
167 }
168 }
169
170 /// Create a key with just a code (no modifiers).
171 pub fn code(code: KeyCode) -> Self {
172 Self {
173 code,
174 ctrl: false,
175 alt: false,
176 shift: false,
177 }
178 }
179
180 /// Add ctrl modifier.
181 pub fn ctrl(mut self) -> Self {
182 self.ctrl = true;
183 self
184 }
185
186 /// Add shift modifier.
187 pub fn shift(mut self) -> Self {
188 self.shift = true;
189 self
190 }
191
192 /// Add alt modifier.
193 pub fn alt(mut self) -> Self {
194 self.alt = true;
195 self
196 }
197}
198
199/// Key codes for non-character keys.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum KeyCode {
202 Char(char),
203 Escape,
204 Backspace,
205 Delete,
206 Left,
207 Right,
208 Up,
209 Down,
210 Home,
211 End,
212 Tab,
213 Enter,
214}