Skip to main content

vimltui/
lib.rs

1//! # vimltui
2//!
3//! A self-contained, embeddable Vim editor for [Ratatui](https://ratatui.rs) TUI applications.
4//!
5//! `vimltui` provides a fully functional Vim editing experience that you can drop into any
6//! Ratatui-based terminal application. Each [`VimEditor`] instance owns its own text buffer,
7//! cursor, mode, undo/redo history, search state, and registers — completely independent
8//! from your application state.
9//!
10//! ## Features
11//!
12//! - **Normal / Insert / Visual** modes (Char, Line, Block)
13//! - **Operator + Motion** composition (`dw`, `ci"`, `y$`, `>j`, `gUw`, ...)
14//! - **f / F / t / T** character find motions
15//! - **Search** with `/`, `?`, `n`, `N` and match highlighting
16//! - **Dot repeat** (`.`) for last edit
17//! - **Undo / Redo** with snapshot stack
18//! - **Command mode** (`:w`, `:q`, `:wq`, `:123`)
19//! - **Registers** and system clipboard integration
20//! - **Text objects** (`iw`, `i"`, `i(`)
21//! - **Relative line numbers** in the built-in renderer
22//! - **Pluggable syntax highlighting** via the [`SyntaxHighlighter`] trait
23//!
24//! ## Quick Start
25//!
26//! ```rust
27//! use vimltui::{VimEditor, VimModeConfig};
28//!
29//! // Create an editor with full editing capabilities
30//! let mut editor = VimEditor::new("Hello, Vim!", VimModeConfig::default());
31//!
32//! // Create a read-only viewer (visual selection only, no insert)
33//! let mut viewer = VimEditor::new("Read only content", VimModeConfig::read_only());
34//!
35//! // Get the current content back
36//! let text = editor.content();
37//! ```
38//!
39//! ## Handling Input
40//!
41//! ```rust,no_run
42//! use vimltui::{VimEditor, VimModeConfig, EditorAction};
43//! use crossterm::event::KeyEvent;
44//!
45//! let mut editor = VimEditor::new("", VimModeConfig::default());
46//!
47//! // In your event loop:
48//! // let action = editor.handle_key(key_event);
49//! // match action {
50//! //     EditorAction::Handled => { /* editor consumed the key */ }
51//! //     EditorAction::Unhandled(key) => { /* pass to your app's handler */ }
52//! //     EditorAction::Save => { /* user typed :w */ }
53//! //     EditorAction::Close => { /* user typed :q */ }
54//! //     EditorAction::ForceClose => { /* user typed :q! */ }
55//! //     EditorAction::SaveAndClose => { /* user typed :wq */ }
56//! // }
57//! ```
58//!
59//! ## Rendering
60//!
61//! Use the built-in renderer with your own [`SyntaxHighlighter`]:
62//!
63//! ```rust
64//! use vimltui::{VimTheme, PlainHighlighter, SyntaxHighlighter};
65//! use ratatui::style::Color;
66//! use ratatui::text::Span;
67//!
68//! // Built-in PlainHighlighter for no syntax coloring
69//! let highlighter = PlainHighlighter;
70//!
71//! // Or implement your own:
72//! struct SqlHighlighter;
73//! impl SyntaxHighlighter for SqlHighlighter {
74//!     fn highlight_line<'a>(&self, line: &'a str, spans: &mut Vec<Span<'a>>) {
75//!         spans.push(Span::raw(line));
76//!     }
77//! }
78//! ```
79
80pub mod editor;
81pub mod render;
82
83use std::collections::HashMap;
84use crossterm::event::KeyEvent;
85use ratatui::style::Color;
86use ratatui::text::Span;
87
88// Re-export the primary type for convenience
89pub use editor::VimEditor;
90
91/// Vim editing mode.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum VimMode {
94    Normal,
95    Insert,
96    Replace,
97    Visual(VisualKind),
98}
99
100/// Cursor shape hint for renderers.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum CursorShape {
103    Block,
104    Bar,
105    Underline,
106}
107
108/// Visual selection kind.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum VisualKind {
111    Char,
112    Line,
113    Block,
114}
115
116/// Configures which Vim modes are available in an editor instance.
117#[derive(Debug, Clone)]
118pub struct VimModeConfig {
119    pub insert_allowed: bool,
120    pub visual_allowed: bool,
121}
122
123impl Default for VimModeConfig {
124    fn default() -> Self {
125        Self {
126            insert_allowed: true,
127            visual_allowed: true,
128        }
129    }
130}
131
132impl VimModeConfig {
133    /// Read-only mode: visual selection is allowed but insert is disabled.
134    pub fn read_only() -> Self {
135        Self {
136            insert_allowed: false,
137            visual_allowed: true,
138        }
139    }
140}
141
142/// Actions returned from [`VimEditor::handle_key()`] to inform the parent application.
143pub enum EditorAction {
144    /// The editor consumed the key — no further action needed.
145    Handled,
146    /// The editor does not handle this key — bubble up to the parent.
147    Unhandled(KeyEvent),
148    /// Save buffer (`:w` or `Ctrl+S`).
149    Save,
150    /// Close buffer (`:q`).
151    Close,
152    /// Force close without saving (`:q!`).
153    ForceClose,
154    /// Save and close (`:wq`, `:x`).
155    SaveAndClose,
156}
157
158/// Leader key (space by default, like modern Neovim setups).
159pub const LEADER_KEY: char = ' ';
160
161/// Operator waiting for a motion (e.g., `d` waits for `w` → `dw`).
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum Operator {
164    Delete,
165    Yank,
166    Change,
167    Indent,
168    Dedent,
169    Uppercase,
170    Lowercase,
171    ToggleCase,
172}
173
174/// The range affected by a motion, used by operators.
175#[derive(Debug, Clone)]
176pub struct MotionRange {
177    pub start_row: usize,
178    pub start_col: usize,
179    pub end_row: usize,
180    pub end_col: usize,
181    pub linewise: bool,
182}
183
184/// Snapshot of editor state for undo/redo.
185#[derive(Debug, Clone)]
186pub struct Snapshot {
187    pub lines: Vec<String>,
188    pub cursor_row: usize,
189    pub cursor_col: usize,
190}
191
192/// Register content (for yank/paste).
193#[derive(Debug, Clone, Default)]
194pub struct Register {
195    pub content: String,
196    pub linewise: bool,
197}
198
199/// State for incremental search (`/` and `?`).
200#[derive(Debug, Clone)]
201pub struct SearchState {
202    pub pattern: String,
203    pub forward: bool,
204    pub active: bool,
205    pub input_buffer: String,
206}
207
208impl Default for SearchState {
209    fn default() -> Self {
210        Self {
211            pattern: String::new(),
212            forward: true,
213            active: false,
214            input_buffer: String::new(),
215        }
216    }
217}
218
219/// Recorded keystrokes for dot-repeat (`.`).
220#[derive(Debug, Clone)]
221pub struct EditRecord {
222    pub keys: Vec<KeyEvent>,
223}
224
225/// Temporary highlight for yanked text (like Neovim's `vim.highlight.on_yank()`).
226#[derive(Debug, Clone)]
227pub struct YankHighlight {
228    pub start_row: usize,
229    pub start_col: usize,
230    pub end_row: usize,
231    pub end_col: usize,
232    pub linewise: bool,
233    pub created_at: std::time::Instant,
234}
235
236impl YankHighlight {
237    /// Duration the highlight stays visible.
238    const DURATION_MS: u128 = 150;
239
240    pub fn is_expired(&self) -> bool {
241        self.created_at.elapsed().as_millis() > Self::DURATION_MS
242    }
243}
244
245/// A gutter sign for a specific line, used for diff indicators.
246///
247/// Consumers populate [`VimEditor::gutter_signs`] with these values.
248/// When empty, the gutter renders exactly as before (zero overhead).
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum GutterSign {
251    /// Line was added (new) — green `│` and green line number.
252    Added,
253    /// Line was modified — yellow `│` and yellow line number.
254    Modified,
255    /// Lines were deleted above this position — red `▲`.
256    DeletedAbove,
257    /// Lines were deleted below this position — red `▼`.
258    DeletedBelow,
259}
260
261/// Configuration for gutter diff signs.
262///
263/// Set [`VimEditor::gutter`] to `Some(GutterConfig { .. })` to enable
264/// diff indicators in the gutter. When `None` (the default), rendering
265/// is unchanged.
266#[derive(Debug, Clone)]
267pub struct GutterConfig {
268    /// Signs per line index.
269    pub signs: HashMap<usize, GutterSign>,
270    /// Color for "added" signs and line numbers (default: `Green`).
271    pub sign_added: Color,
272    /// Color for "modified" signs and line numbers (default: `Yellow`).
273    pub sign_modified: Color,
274    /// Color for "deleted" signs (default: `Red`).
275    pub sign_deleted: Color,
276}
277
278impl Default for GutterConfig {
279    fn default() -> Self {
280        Self {
281            signs: HashMap::new(),
282            sign_added: Color::Green,
283            sign_modified: Color::Yellow,
284            sign_deleted: Color::Red,
285        }
286    }
287}
288
289/// Direction for `f`/`F`/`t`/`T` character find motions.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum FindDirection {
292    Forward,
293    Backward,
294}
295
296/// Theme colors used by the built-in [`render`] module.
297///
298/// Each application maps its own theme struct to `VimTheme` before calling
299/// [`render::render()`].
300#[derive(Debug, Clone)]
301pub struct VimTheme {
302    pub border_focused: Color,
303    pub border_unfocused: Color,
304    pub border_insert: Color,
305    pub editor_bg: Color,
306    pub line_nr: Color,
307    pub line_nr_active: Color,
308    pub visual_bg: Color,
309    pub visual_fg: Color,
310    pub dim: Color,
311    pub accent: Color,
312    /// Background for search matches (all occurrences).
313    pub search_match_bg: Color,
314    /// Background for the current search match (where the cursor is).
315    pub search_current_bg: Color,
316    /// Foreground for search match text.
317    pub search_match_fg: Color,
318    /// Background for yank highlight flash.
319    pub yank_highlight_bg: Color,
320    /// Background for live substitution replacement preview.
321    pub substitute_preview_bg: Color,
322}
323
324/// Trait for language-specific syntax highlighting.
325///
326/// Implement this for your language (SQL, JSON, YAML, etc.) and pass it to the
327/// [`render`] module. See [`PlainHighlighter`] for a no-op reference implementation.
328pub trait SyntaxHighlighter {
329    /// Highlight a full line and append styled [`Span`]s.
330    fn highlight_line<'a>(&self, line: &'a str, spans: &mut Vec<Span<'a>>);
331
332    /// Highlight a segment of a line (used when part of the line has visual selection).
333    /// Defaults to [`highlight_line`](SyntaxHighlighter::highlight_line).
334    fn highlight_segment<'a>(&self, text: &'a str, spans: &mut Vec<Span<'a>>) {
335        self.highlight_line(text, spans);
336    }
337}
338
339/// No-op highlighter — renders text without any syntax coloring.
340pub struct PlainHighlighter;
341
342impl SyntaxHighlighter for PlainHighlighter {
343    fn highlight_line<'a>(&self, line: &'a str, spans: &mut Vec<Span<'a>>) {
344        if !line.is_empty() {
345            spans.push(Span::raw(line));
346        }
347    }
348}
349
350/// Number of lines kept visible above/below the cursor when scrolling.
351pub const SCROLLOFF: usize = 3;