Skip to main content

tui_lipan/widgets/text_area/
vim_config.rs

1use std::sync::Arc;
2
3use crate::core::event::{KeyCode, KeyEvent, KeyMods};
4use crate::input::KeyBindings;
5use crate::style::{Style, StyleSlot};
6
7/// TextArea-only Vim-style modal motion mode.
8#[non_exhaustive]
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10pub enum TextAreaVimMode {
11    /// Plain text insertion mode.
12    Insert,
13    /// Vim-style normal/motion mode. This is the initial mode.
14    #[default]
15    Normal,
16    /// Vim-style visual selection mode.
17    ///
18    /// Motions extend the selection from the original visual anchor. Supported
19    /// Vim edit commands can yank or replace the active selection.
20    Visual,
21    /// Vim-style linewise visual selection mode.
22    ///
23    /// Motions extend the selection from the original logical line and keep
24    /// the selected range aligned to whole logical lines.
25    VisualLine,
26}
27
28/// Widget-local Vim key remaps for [`TextArea::vim_motions`](crate::widgets::TextArea::vim_motions).
29///
30/// The keymap translates matching input keys into canonical Vim command
31/// characters before the TextArea Vim dispatcher sees them. This keeps the
32/// default Vim grammar intact while allowing applications to add aliases such
33/// as mapping `ctrl+n` to `j` or `ctrl+p` to `k` for a particular TextArea.
34#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
35pub struct TextAreaVimKeymap {
36    bindings: Vec<TextAreaVimKeyBinding>,
37}
38
39/// One TextArea Vim key remap entry.
40#[derive(Clone, Debug, PartialEq, Eq, Hash)]
41pub struct TextAreaVimKeyBinding {
42    /// Triggering key bindings. Only single-key bindings are used by the Vim
43    /// dispatcher; chord bindings are ignored here and remain available to the
44    /// app-level keymap/interceptor layers.
45    pub bindings: KeyBindings,
46    /// Canonical Vim command character to dispatch when a binding matches.
47    pub command: char,
48}
49
50impl TextAreaVimKeymap {
51    /// Create an empty TextArea Vim keymap.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Add a remap from one or more key bindings to a canonical Vim command
57    /// character.
58    pub fn bind(mut self, bindings: KeyBindings, command: char) -> Self {
59        self.bindings
60            .push(TextAreaVimKeyBinding { bindings, command });
61        self
62    }
63
64    pub(crate) fn translate_key(&self, key: KeyEvent) -> KeyEvent {
65        let Some(binding) = self.bindings.iter().find(|binding| {
66            binding
67                .bindings
68                .iter()
69                .any(|candidate| !candidate.is_chord() && candidate.matches_sequence(&[key]))
70        }) else {
71            return key;
72        };
73
74        KeyEvent {
75            code: KeyCode::Char(binding.command),
76            mods: KeyMods::default(),
77        }
78    }
79}
80
81/// Current-line highlighting mode for [`TextArea::vim_config`](crate::widgets::TextArea::vim_config).
82#[non_exhaustive]
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
84pub enum TextAreaVimCurrentLineHighlight {
85    /// Do not render a Vim current-line highlight.
86    #[default]
87    Off,
88    /// Highlight only the text content area for every wrapped row of the cursor's
89    /// logical line.
90    Content,
91    /// Highlight the full inner row, including line numbers or custom gutter.
92    Full,
93}
94
95/// Rendering configuration for TextArea Vim affordances.
96///
97/// This keeps Vim-only visual options grouped separately from the plain TextArea
98/// builder API. It controls the built-in search bar/match feedback and optional
99/// current-line highlighting; Vim command semantics are still enabled with
100/// [`TextArea::vim_motions`](crate::widgets::TextArea::vim_motions).
101#[derive(Clone, Debug, PartialEq, Eq, Hash)]
102pub struct TextAreaVimConfig {
103    /// Style for the Vim search/status bar. `Inherit` resolves to the widget
104    /// chrome style, `Extend` patches the chrome style, and `Replace` uses the
105    /// supplied style directly.
106    pub search_bar_style: StyleSlot,
107    /// Style overlay for the Vim search bar prefix icons.
108    /// Unset fields fall through to [`Self::search_bar_style`].
109    pub search_bar_prefix_style: StyleSlot,
110    /// Style overlay for Vim search count labels like `[2/5]`.
111    /// Unset fields fall through to [`Self::search_bar_style`].
112    pub search_bar_count_style: StyleSlot,
113    /// Style patched over visible matches while Vim search feedback is shown.
114    /// `Inherit` resolves against the theme text-selection role.
115    pub search_match_style: StyleSlot,
116    /// Style patched over the current Vim search match. While a search prompt is
117    /// pending, this is the match `Enter` would jump to; after `Enter`, `n` and
118    /// `N` update it. `Inherit` resolves against the theme text-selection role so
119    /// the default uses a background highlight.
120    pub current_search_match_style: StyleSlot,
121    /// Optional Vim current-line highlighting.
122    pub current_line_highlight: TextAreaVimCurrentLineHighlight,
123    /// Style for current-line highlighting. `Inherit` resolves against the theme
124    /// item-hover role so the default follows the active theme.
125    pub current_line_style: StyleSlot,
126    /// Style for the current line number/custom gutter row. Unset fields fall
127    /// through to the current-line style so full-row highlights stay continuous.
128    pub current_line_number_style: StyleSlot,
129}
130
131impl Default for TextAreaVimConfig {
132    fn default() -> Self {
133        Self {
134            search_bar_style: StyleSlot::Extend(Style::new().reverse()),
135            search_bar_prefix_style: StyleSlot::Inherit,
136            search_bar_count_style: StyleSlot::Inherit,
137            search_match_style: StyleSlot::Replace(Style::new().underline().bold()),
138            current_search_match_style: StyleSlot::Inherit,
139            current_line_highlight: TextAreaVimCurrentLineHighlight::Off,
140            current_line_style: StyleSlot::Inherit,
141            current_line_number_style: StyleSlot::Inherit,
142        }
143    }
144}
145
146impl TextAreaVimConfig {
147    /// Create a Vim rendering config with default search feedback and no
148    /// current-line highlight.
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// Replace the Vim search/status bar style.
154    pub fn search_bar_style(mut self, style: Style) -> Self {
155        self.search_bar_style = StyleSlot::Replace(style);
156        self
157    }
158
159    /// Extend the TextArea chrome style for the Vim search/status bar.
160    pub fn extend_search_bar_style(mut self, style: Style) -> Self {
161        self.search_bar_style = StyleSlot::Extend(style);
162        self
163    }
164
165    /// Set the Vim search/status bar style slot directly for composite forwarding.
166    pub fn search_bar_style_slot(mut self, slot: StyleSlot) -> Self {
167        self.search_bar_style = slot;
168        self
169    }
170
171    /// Overlay style for the Vim search bar prefix icons.
172    pub fn search_bar_prefix_style(mut self, style: Style) -> Self {
173        self.search_bar_prefix_style = StyleSlot::Replace(style);
174        self
175    }
176
177    /// Extend the Vim search bar style for the prefix icons.
178    pub fn extend_search_bar_prefix_style(mut self, style: Style) -> Self {
179        self.search_bar_prefix_style = StyleSlot::Extend(style);
180        self
181    }
182
183    /// Set the Vim search bar prefix style slot directly for composite forwarding.
184    pub fn search_bar_prefix_style_slot(mut self, slot: StyleSlot) -> Self {
185        self.search_bar_prefix_style = slot;
186        self
187    }
188
189    /// Overlay style for Vim search count labels like `[2/5]`.
190    pub fn search_bar_count_style(mut self, style: Style) -> Self {
191        self.search_bar_count_style = StyleSlot::Replace(style);
192        self
193    }
194
195    /// Extend the Vim search bar style for count labels like `[2/5]`.
196    pub fn extend_search_bar_count_style(mut self, style: Style) -> Self {
197        self.search_bar_count_style = StyleSlot::Extend(style);
198        self
199    }
200
201    /// Set the Vim search bar count style slot directly for composite forwarding.
202    pub fn search_bar_count_style_slot(mut self, slot: StyleSlot) -> Self {
203        self.search_bar_count_style = slot;
204        self
205    }
206
207    /// Replace the visible Vim search-match style.
208    pub fn search_match_style(mut self, style: Style) -> Self {
209        self.search_match_style = StyleSlot::Replace(style);
210        self
211    }
212
213    /// Extend the theme text-selection role for visible Vim search matches.
214    pub fn extend_search_match_style(mut self, style: Style) -> Self {
215        self.search_match_style = StyleSlot::Extend(style);
216        self
217    }
218
219    /// Set the search-match style slot directly for composite forwarding.
220    pub fn search_match_style_slot(mut self, slot: StyleSlot) -> Self {
221        self.search_match_style = slot;
222        self
223    }
224
225    /// Replace the current Vim search-match style.
226    pub fn current_search_match_style(mut self, style: Style) -> Self {
227        self.current_search_match_style = StyleSlot::Replace(style);
228        self
229    }
230
231    /// Extend the theme text-selection role for the current Vim search match.
232    pub fn extend_current_search_match_style(mut self, style: Style) -> Self {
233        self.current_search_match_style = StyleSlot::Extend(style);
234        self
235    }
236
237    /// Set the current-search-match style slot directly for composite forwarding.
238    pub fn current_search_match_style_slot(mut self, slot: StyleSlot) -> Self {
239        self.current_search_match_style = slot;
240        self
241    }
242
243    /// Configure Vim current-line highlighting.
244    pub fn current_line_highlight(mut self, mode: TextAreaVimCurrentLineHighlight) -> Self {
245        self.current_line_highlight = mode;
246        self
247    }
248
249    /// Convenience toggle for full-row Vim current-line highlighting.
250    pub fn highlight_current_line(mut self, enabled: bool) -> Self {
251        self.current_line_highlight = if enabled {
252            TextAreaVimCurrentLineHighlight::Full
253        } else {
254            TextAreaVimCurrentLineHighlight::Off
255        };
256        self
257    }
258
259    /// Replace the Vim current-line style.
260    pub fn current_line_style(mut self, style: Style) -> Self {
261        self.current_line_style = StyleSlot::Replace(style);
262        self
263    }
264
265    /// Extend the theme item-hover role for Vim current-line highlighting.
266    pub fn extend_current_line_style(mut self, style: Style) -> Self {
267        self.current_line_style = StyleSlot::Extend(style);
268        self
269    }
270
271    /// Set the Vim current-line style slot directly for composite forwarding.
272    pub fn current_line_style_slot(mut self, slot: StyleSlot) -> Self {
273        self.current_line_style = slot;
274        self
275    }
276
277    /// Replace the style overlay used for the current line number/custom gutter row.
278    pub fn current_line_number_style(mut self, style: Style) -> Self {
279        self.current_line_number_style = StyleSlot::Replace(style);
280        self
281    }
282
283    /// Extend the current-line style for the current line number/custom gutter row.
284    pub fn extend_current_line_number_style(mut self, style: Style) -> Self {
285        self.current_line_number_style = StyleSlot::Extend(style);
286        self
287    }
288
289    /// Set the current-line-number style slot directly for composite forwarding.
290    pub fn current_line_number_style_slot(mut self, slot: StyleSlot) -> Self {
291        self.current_line_number_style = slot;
292        self
293    }
294}
295
296#[derive(Clone, Debug, PartialEq, Eq, Hash)]
297pub(crate) struct TextAreaVimSearchFeedback {
298    pub query: Arc<str>,
299    pub cursor: usize,
300    pub forward: bool,
301    pub pending: bool,
302    pub target_range: Option<(usize, usize)>,
303    pub current_match_index: Option<usize>,
304    pub match_count: usize,
305}