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
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}