Skip to main content

tui_canvas/editor/
core.rs

1// src/editor/core.rs
2#[cfg(feature = "cursor-style")]
3use crate::cursor::CursorManager;
4
5use crate::DataProvider;
6#[cfg(feature = "suggestions")]
7use crate::SuggestionItem;
8use crate::canvas::modes::AppMode;
9use crate::canvas::state::EditorState;
10#[cfg(feature = "keybindings")]
11use crate::editor::behavior::{EditorBehaviorState, KeybindingParadigm};
12#[cfg(feature = "keybindings")]
13use crate::keybindings::BuiltinCanvasKeybindingPreset;
14use derivative::Derivative;
15
16#[cfg(feature = "keybindings")]
17use crate::keybindings::{CanvasKeyBindings, KeySequenceTracker};
18
19#[derive(Derivative)]
20#[derivative(Debug, Default)]
21pub struct EditorCore<D: DataProvider> {
22    pub(crate) ui_state: EditorState,
23    pub(crate) data_provider: D,
24    #[cfg(feature = "suggestions")]
25    pub(crate) suggestions: Vec<SuggestionItem>,
26
27    #[cfg(feature = "validation")]
28    #[derivative(Debug = "ignore")]
29    #[derivative(Default(value = "None"))]
30    pub(crate) external_validation_callback: Option<
31        Box<dyn FnMut(usize, &str) -> crate::validation::ExternalValidationState + Send + Sync>,
32    >,
33    #[cfg(feature = "keybindings")]
34    #[derivative(Default(value = "None"))]
35    pub(crate) keybindings: Option<CanvasKeyBindings>,
36
37    #[cfg(feature = "keybindings")]
38    #[derivative(Default(value = "KeySequenceTracker::new(400)"))]
39    pub(crate) seq_tracker: KeySequenceTracker,
40
41    #[cfg(feature = "keybindings")]
42    pub(crate) behavior_state: EditorBehaviorState,
43
44    pub(crate) undo_stack: Vec<crate::editor::features::history::EditSnapshot>,
45    pub(crate) redo_stack: Vec<crate::editor::features::history::EditSnapshot>,
46    #[derivative(Default(value = "crate::editor::features::history::DEFAULT_HISTORY_LIMIT"))]
47    pub(crate) history_limit: usize,
48    pub(crate) history_last_kind: Option<crate::editor::features::history::EditKind>,
49    #[derivative(Default(value = "true"))]
50    pub(crate) history_enabled: bool,
51}
52
53impl<D: DataProvider> EditorCore<D> {
54    pub(crate) fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
55        s.char_indices()
56            .nth(char_idx)
57            .map(|(byte_idx, _)| byte_idx)
58            .unwrap_or_else(|| s.len())
59    }
60
61    #[allow(dead_code)]
62    pub(crate) fn byte_to_char_index(s: &str, byte_idx: usize) -> usize {
63        s[..byte_idx].chars().count()
64    }
65
66    /// Whether a multi-key command is mid-flight in the shared editor state: a
67    /// partially-matched key sequence (e.g. `g` of `gg`), a pending count
68    /// (`2…`), or an operator awaiting its motion (vim `d`/`c`/`y`). A host can
69    /// use this to keep routing subsequent keys to the editor instead of letting
70    /// an outer keymap claim them. (Literal-char captures like `f`/`r` live on
71    /// the concrete widget state and are folded in there.)
72    #[cfg(feature = "keybindings")]
73    pub(crate) fn is_sequence_pending(&self) -> bool {
74        !self.seq_tracker.sequence().is_empty()
75            || self.behavior_state.vim().has_count()
76            || self.behavior_state.vim().has_pending_operator()
77    }
78
79    pub fn new(data_provider: D) -> Self {
80        let editor = Self {
81            ui_state: EditorState::new(),
82            data_provider,
83            #[cfg(feature = "suggestions")]
84            suggestions: Vec::new(),
85            #[cfg(feature = "validation")]
86            external_validation_callback: None,
87            #[cfg(feature = "keybindings")]
88            keybindings: None,
89            #[cfg(feature = "keybindings")]
90            seq_tracker: KeySequenceTracker::new(400),
91            #[cfg(feature = "keybindings")]
92            behavior_state: EditorBehaviorState::default(),
93            undo_stack: Vec::new(),
94            redo_stack: Vec::new(),
95            history_limit: crate::editor::features::history::DEFAULT_HISTORY_LIMIT,
96            history_last_kind: None,
97            history_enabled: true,
98        };
99
100        #[cfg(feature = "validation")]
101        {
102            let mut editor = editor;
103            editor.initialize_validation();
104
105            #[cfg(feature = "cursor-style")]
106            {
107                let _ = CursorManager::update_for_mode(editor.ui_state.current_mode);
108            }
109            editor
110        }
111        #[cfg(not(feature = "validation"))]
112        {
113            #[cfg(feature = "cursor-style")]
114            {
115                let _ = CursorManager::update_for_mode(editor.ui_state.current_mode);
116            }
117            editor
118        }
119    }
120
121    /// Set the keybindings for this editor instance.
122    #[cfg(feature = "keybindings")]
123    pub fn set_keybindings(&mut self, keybindings: CanvasKeyBindings) {
124        if let Some(paradigm) = keybindings.paradigm {
125            self.behavior_state.set_paradigm(paradigm);
126            self.apply_after_mode_change_for_paradigm();
127        }
128        self.keybindings = Some(keybindings);
129    }
130
131    /// Install a built-in keybinding preset and its editing paradigm.
132    #[cfg(feature = "keybindings")]
133    pub fn set_keybinding_preset(&mut self, preset: BuiltinCanvasKeybindingPreset) {
134        self.set_keybindings(CanvasKeyBindings::from_builtin_preset(preset));
135    }
136
137    #[cfg(feature = "keybindings")]
138    pub(crate) fn keybinding_paradigm(&self) -> KeybindingParadigm {
139        self.behavior_state.paradigm()
140    }
141
142    /// Check if this editor has keybindings configured.
143    #[cfg(feature = "keybindings")]
144    pub fn has_keybindings(&self) -> bool {
145        self.keybindings.is_some()
146    }
147
148    /// Set the timeout for multi-key sequences (in milliseconds)
149    #[cfg(feature = "keybindings")]
150    pub fn set_key_sequence_timeout_ms(&mut self, timeout_ms: u64) {
151        self.seq_tracker = KeySequenceTracker::new(timeout_ms);
152    }
153
154    pub fn current_text(&self) -> &str {
155        let field_index = self.ui_state.current_field;
156        if field_index < self.data_provider.field_count() {
157            self.data_provider.field_value(field_index)
158        } else {
159            ""
160        }
161    }
162
163    pub(crate) fn clamp_current_field_to_count(&mut self, field_count: usize) -> Option<usize> {
164        if field_count == 0 {
165            self.ui_state.current_field = 0;
166            self.set_cursor_raw(0);
167            return None;
168        }
169
170        let field_index = self.ui_state.current_field.min(field_count - 1);
171        if field_index != self.ui_state.current_field {
172            self.ui_state.current_field = field_index;
173            let len = self.current_text().chars().count();
174            let cursor = self.cursor_position().min(len);
175            self.set_cursor_raw(cursor);
176        }
177
178        Some(field_index)
179    }
180
181    pub(crate) fn set_cursor_raw(&mut self, pos: usize) {
182        self.ui_state.set_cursor(pos, pos, true);
183        #[cfg(feature = "keybindings")]
184        if self.keybinding_paradigm() == KeybindingParadigm::Helix
185            && self.ui_state.current_mode == AppMode::Nor
186        {
187            self.collapse_selection_to_cursor();
188        }
189    }
190
191    pub(crate) fn set_cursor_for_mode(&mut self, pos: usize, max_len: usize) {
192        self.ui_state
193            .set_cursor(pos, max_len, self.ui_state.current_mode == AppMode::Ins);
194        #[cfg(feature = "keybindings")]
195        if self.keybinding_paradigm() == KeybindingParadigm::Helix
196            && self.ui_state.current_mode == AppMode::Nor
197        {
198            self.collapse_selection_to_cursor();
199        }
200    }
201
202    pub fn current_field(&self) -> usize {
203        self.ui_state.current_field()
204    }
205    pub fn cursor_position(&self) -> usize {
206        self.ui_state.cursor_position()
207    }
208    pub fn mode(&self) -> AppMode {
209        self.ui_state.mode()
210    }
211    #[cfg(feature = "suggestions")]
212    pub fn is_suggestions_active(&self) -> bool {
213        self.ui_state.is_suggestions_active()
214    }
215    pub fn ui_state(&self) -> &EditorState {
216        &self.ui_state
217    }
218    pub fn data_provider(&self) -> &D {
219        &self.data_provider
220    }
221    pub fn data_provider_mut(&mut self) -> &mut D {
222        &mut self.data_provider
223    }
224    #[cfg(feature = "suggestions")]
225    pub fn suggestions(&self) -> &[SuggestionItem] {
226        &self.suggestions
227    }
228
229    #[cfg(feature = "validation")]
230    pub fn validation_state(&self) -> &crate::validation::ValidationState {
231        self.ui_state.validation_state()
232    }
233
234    #[cfg(feature = "cursor-style")]
235    pub fn cleanup_cursor(&self) -> std::io::Result<()> {
236        CursorManager::reset()
237    }
238    #[cfg(not(feature = "cursor-style"))]
239    pub fn cleanup_cursor(&self) -> std::io::Result<()> {
240        Ok(())
241    }
242}
243
244impl<D: DataProvider> Drop for EditorCore<D> {
245    fn drop(&mut self) {
246        let _ = self.cleanup_cursor();
247    }
248}