Skip to main content

tui_canvas/editor/
mode.rs

1// src/editor/mode.rs
2
3#[cfg(feature = "cursor-style")]
4use crate::cursor::CursorManager;
5
6use crate::DataProvider;
7use crate::canvas::modes::AppMode;
8use crate::canvas::state::SelectionState;
9use crate::editor::EditorCore;
10#[cfg(feature = "keybindings")]
11use crate::editor::behavior::KeybindingParadigm;
12
13impl<D: DataProvider> EditorCore<D> {
14    pub(crate) fn set_highlight_mode_selection(&mut self, selection: SelectionState) {
15        self.ui_state.current_mode = AppMode::Sel;
16        self.ui_state.selection = selection;
17
18        #[cfg(feature = "cursor-style")]
19        {
20            let _ = CursorManager::update_for_mode(AppMode::Sel);
21        }
22    }
23
24    /// Change mode
25    pub fn set_mode(&mut self, mode: AppMode) {
26        // A genuine mode change ends any in-progress undo-coalescing run. In
27        // normal mode the mode never actually changes (always Ins), and the
28        // wrappers call `enter_edit_mode` on every keystroke, so we must only
29        // break on a real transition to keep typing coalesced.
30        #[cfg(not(feature = "textmode-normal"))]
31        if self.ui_state.current_mode != mode {
32            self.break_undo_coalescing();
33        }
34
35        // Avoid unused param warning in normalmode
36        #[cfg(feature = "textmode-normal")]
37        let _ = mode;
38
39        // NORMALMODE: force Ins, ignore requested mode
40        #[cfg(feature = "textmode-normal")]
41        {
42            self.ui_state.current_mode = AppMode::Ins;
43            self.ui_state.selection = SelectionState::None;
44
45            #[cfg(feature = "cursor-style")]
46            {
47                let _ = CursorManager::update_for_mode(AppMode::Ins);
48            }
49        }
50
51        // Default (not normal): paradigm-specific modal behavior
52        #[cfg(not(feature = "textmode-normal"))]
53        {
54            #[cfg(feature = "keybindings")]
55            {
56                match self.keybinding_paradigm() {
57                    KeybindingParadigm::Helix => self.set_mode_helix(mode),
58                    KeybindingParadigm::Emacs | KeybindingParadigm::Vscode => {
59                        self.set_mode_emacs(mode)
60                    }
61                    KeybindingParadigm::Vim => self.set_mode_vim(mode),
62                }
63            }
64            #[cfg(not(feature = "keybindings"))]
65            self.set_mode_vim(mode);
66        }
67    }
68
69    /// Exit insert mode to normal mode
70    pub fn exit_edit_mode(&mut self) -> anyhow::Result<()> {
71        #[cfg(feature = "validation")]
72        {
73            let current_text = self.current_text();
74            if !self
75                .ui_state
76                .validation
77                .allows_field_switch(self.ui_state.current_field, current_text)
78            {
79                if let Some(reason) = self
80                    .ui_state
81                    .validation
82                    .field_switch_block_reason(self.ui_state.current_field, current_text)
83                {
84                    self.ui_state
85                        .validation
86                        .set_last_switch_block(reason.clone());
87                    return Err(anyhow::anyhow!("Cannot exit edit mode: {}", reason));
88                }
89            }
90        }
91
92        let current_text = self.current_text();
93        if !current_text.is_empty() {
94            let max_normal_pos = current_text.chars().count().saturating_sub(1);
95            if self.ui_state.cursor_pos > max_normal_pos {
96                self.set_cursor_raw(max_normal_pos);
97            }
98        }
99
100        #[cfg(feature = "validation")]
101        {
102            let field_index = self.ui_state.current_field;
103            if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
104                if cfg.external_validation_enabled {
105                    let text = self.current_text().to_string();
106                    if !text.is_empty() {
107                        self.set_external_validation(
108                            field_index,
109                            crate::validation::ExternalValidationState::Validating,
110                        );
111                        if let Some(cb) = self.external_validation_callback.as_mut() {
112                            let final_state = cb(field_index, &text);
113                            self.set_external_validation(field_index, final_state);
114                        }
115                    }
116                }
117            }
118        }
119
120        // NORMALMODE: stay in Ins (do not switch to Nor)
121        #[cfg(feature = "textmode-normal")]
122        {
123            #[cfg(feature = "suggestions")]
124            {
125                self.dismiss_suggestions();
126            }
127            Ok(())
128        }
129
130        // Default (not normal): original vim behavior
131        #[cfg(not(feature = "textmode-normal"))]
132        {
133            self.set_mode(AppMode::Nor);
134            #[cfg(feature = "suggestions")]
135            {
136                self.dismiss_suggestions();
137            }
138            Ok(())
139        }
140    }
141
142    /// Enter insert mode
143    pub fn enter_edit_mode(&mut self) {
144        #[cfg(feature = "computed")]
145        {
146            if let Some(computed_state) = &self.ui_state.computed {
147                if computed_state.is_computed_field(self.ui_state.current_field) {
148                    return;
149                }
150            }
151        }
152
153        // NORMALMODE: already in Ins, but enforce it
154        #[cfg(feature = "textmode-normal")]
155        {
156            self.ui_state.current_mode = AppMode::Ins;
157            self.ui_state.selection = SelectionState::None;
158            #[cfg(feature = "cursor-style")]
159            {
160                let _ = CursorManager::update_for_mode(AppMode::Ins);
161            }
162        }
163
164        // Default (not normal): paradigm-specific insert entry
165        #[cfg(not(feature = "textmode-normal"))]
166        {
167            #[cfg(feature = "keybindings")]
168            match self.keybinding_paradigm() {
169                KeybindingParadigm::Helix => self.enter_edit_mode_helix(),
170                KeybindingParadigm::Emacs | KeybindingParadigm::Vscode => {
171                    self.enter_edit_mode_emacs()
172                }
173                KeybindingParadigm::Vim => self.enter_edit_mode_vim(),
174            }
175            #[cfg(not(feature = "keybindings"))]
176            self.enter_edit_mode_vim();
177        }
178
179        // Check if suggestions should be shown based on trigger
180        #[cfg(feature = "suggestions")]
181        self.check_suggestion_trigger();
182    }
183
184    // Selection/visual mode
185
186    pub fn enter_highlight_mode(&mut self) {
187        #[cfg(feature = "textmode-normal")]
188        {}
189
190        #[cfg(not(feature = "textmode-normal"))]
191        {
192            #[cfg(feature = "keybindings")]
193            match self.keybinding_paradigm() {
194                KeybindingParadigm::Helix => self.enter_highlight_mode_helix(),
195                KeybindingParadigm::Emacs | KeybindingParadigm::Vscode => {
196                    self.enter_highlight_mode_emacs()
197                }
198                KeybindingParadigm::Vim => self.enter_highlight_mode_vim(),
199            }
200            #[cfg(not(feature = "keybindings"))]
201            self.enter_highlight_mode_vim();
202        }
203    }
204
205    pub fn enter_highlight_line_mode(&mut self) {
206        #[cfg(feature = "textmode-normal")]
207        {}
208
209        #[cfg(not(feature = "textmode-normal"))]
210        {
211            #[cfg(feature = "keybindings")]
212            match self.keybinding_paradigm() {
213                KeybindingParadigm::Helix => self.enter_highlight_line_mode_helix(),
214                KeybindingParadigm::Emacs | KeybindingParadigm::Vscode => {
215                    self.enter_highlight_line_mode_emacs()
216                }
217                KeybindingParadigm::Vim => self.enter_highlight_line_mode_vim(),
218            }
219            #[cfg(not(feature = "keybindings"))]
220            self.enter_highlight_line_mode_vim();
221        }
222    }
223
224    pub fn exit_highlight_mode(&mut self) {
225        #[cfg(feature = "textmode-normal")]
226        {}
227
228        #[cfg(not(feature = "textmode-normal"))]
229        {
230            #[cfg(feature = "keybindings")]
231            match self.keybinding_paradigm() {
232                KeybindingParadigm::Helix => self.exit_highlight_mode_helix(),
233                KeybindingParadigm::Emacs | KeybindingParadigm::Vscode => {
234                    self.exit_highlight_mode_emacs()
235                }
236                KeybindingParadigm::Vim => self.exit_highlight_mode_vim(),
237            }
238            #[cfg(not(feature = "keybindings"))]
239            self.exit_highlight_mode_vim();
240        }
241    }
242
243    pub fn is_highlight_mode(&self) -> bool {
244        #[cfg(feature = "textmode-normal")]
245        {
246            false
247        }
248        #[cfg(not(feature = "textmode-normal"))]
249        {
250            return self.ui_state.current_mode == AppMode::Sel;
251        }
252    }
253
254    pub fn selection_state(&self) -> &SelectionState {
255        &self.ui_state.selection
256    }
257
258    // Visual-mode movements reuse existing movement methods
259    // These keep calling the movement methods; in normalmode selection is never enabled,
260    // so these just move without creating a selection.
261    pub fn move_left_with_selection(&mut self) {
262        let _ = self.move_left();
263    }
264
265    pub fn move_right_with_selection(&mut self) {
266        let _ = self.move_right();
267    }
268
269    pub fn move_up_with_selection(&mut self) {
270        let _ = self.move_up();
271    }
272
273    pub fn move_down_with_selection(&mut self) {
274        let _ = self.move_down();
275    }
276
277    pub fn move_word_next_with_selection(&mut self) {
278        self.move_word_next();
279    }
280
281    pub fn move_word_end_with_selection(&mut self) {
282        self.move_word_end();
283    }
284
285    pub fn move_word_prev_with_selection(&mut self) {
286        self.move_word_prev();
287    }
288
289    pub fn move_word_end_prev_with_selection(&mut self) {
290        self.move_word_end_prev();
291    }
292
293    pub fn move_big_word_next_with_selection(&mut self) {
294        self.move_big_word_next();
295    }
296
297    pub fn move_big_word_end_with_selection(&mut self) {
298        self.move_big_word_end();
299    }
300
301    pub fn move_big_word_prev_with_selection(&mut self) {
302        self.move_big_word_prev();
303    }
304
305    pub fn move_big_word_end_prev_with_selection(&mut self) {
306        self.move_big_word_end_prev();
307    }
308
309    pub fn move_line_start_with_selection(&mut self) {
310        self.move_line_start();
311    }
312
313    pub fn move_line_end_with_selection(&mut self) {
314        self.move_line_end();
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::canvas::modes::AppMode;
322
323    #[derive(Clone)]
324    struct TestProvider {
325        fields: Vec<(&'static str, String)>,
326    }
327
328    impl TestProvider {
329        fn new(values: &[&'static str]) -> Self {
330            Self {
331                fields: values
332                    .iter()
333                    .enumerate()
334                    .map(|(i, value)| {
335                        let name = match i {
336                            0 => "a",
337                            1 => "b",
338                            _ => "c",
339                        };
340                        (name, (*value).to_string())
341                    })
342                    .collect(),
343            }
344        }
345    }
346
347    impl DataProvider for TestProvider {
348        fn field_count(&self) -> usize {
349            self.fields.len()
350        }
351
352        fn field_name(&self, index: usize) -> &str {
353            self.fields[index].0
354        }
355
356        fn field_value(&self, index: usize) -> &str {
357            &self.fields[index].1
358        }
359
360        fn set_field_value(&mut self, index: usize, value: String) {
361            self.fields[index].1 = value;
362        }
363    }
364
365    #[test]
366    fn visual_characterwise_toggles_and_switches_from_linewise() {
367        let mut editor = EditorCore::new(TestProvider::new(&["alpha", "beta"]));
368
369        editor.enter_highlight_mode();
370        assert_eq!(editor.mode(), AppMode::Sel);
371        assert!(matches!(
372            editor.selection_state(),
373            SelectionState::Characterwise { anchor: (0, 0) }
374        ));
375
376        editor.enter_highlight_mode();
377        assert_eq!(editor.mode(), AppMode::Nor);
378        assert!(matches!(editor.selection_state(), SelectionState::None));
379
380        editor.enter_highlight_line_mode();
381        assert!(matches!(
382            editor.selection_state(),
383            SelectionState::Linewise { anchor_field: 0 }
384        ));
385
386        editor.move_down();
387        editor.enter_highlight_mode();
388        assert!(matches!(
389            editor.selection_state(),
390            SelectionState::Characterwise { anchor: (1, 0) }
391        ));
392    }
393
394    #[test]
395    fn visual_linewise_toggles_and_switches_from_characterwise() {
396        let mut editor = EditorCore::new(TestProvider::new(&["alpha", "beta"]));
397
398        editor.enter_highlight_line_mode();
399        assert_eq!(editor.mode(), AppMode::Sel);
400        assert!(matches!(
401            editor.selection_state(),
402            SelectionState::Linewise { anchor_field: 0 }
403        ));
404
405        editor.enter_highlight_line_mode();
406        assert_eq!(editor.mode(), AppMode::Nor);
407        assert!(matches!(editor.selection_state(), SelectionState::None));
408
409        editor.enter_highlight_mode();
410        editor.move_down();
411        editor.enter_highlight_line_mode();
412        assert!(matches!(
413            editor.selection_state(),
414            SelectionState::Linewise { anchor_field: 0 }
415        ));
416    }
417
418    #[test]
419    fn visual_linewise_switch_preserves_selected_line_range() {
420        let mut editor = EditorCore::new(TestProvider::new(&["alpha", "beta", "gamma"]));
421
422        editor.enter_highlight_mode();
423        editor.move_down();
424        editor.enter_highlight_line_mode();
425
426        assert_eq!(editor.current_field(), 1);
427        assert!(matches!(
428            editor.selection_state(),
429            SelectionState::Linewise { anchor_field: 0 }
430        ));
431    }
432}