tui_canvas/editor/
core.rs1#[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 #[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 #[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 #[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 #[cfg(feature = "keybindings")]
144 pub fn has_keybindings(&self) -> bool {
145 self.keybindings.is_some()
146 }
147
148 #[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}