Skip to main content

tui_canvas/editor/
movement.rs

1// src/editor/movement.rs
2
3use crate::DataProvider;
4use crate::canvas::actions::movement::line::{line_end_position, line_start_position};
5use crate::canvas::actions::movement::word::{
6    find_last_big_word_start_in_field, find_last_word_start_in_field,
7};
8use crate::canvas::modes::AppMode;
9use crate::editor::EditorCore;
10
11impl<D: DataProvider> EditorCore<D> {
12    /// Move cursor left within current field (mask-aware)
13    pub fn move_left(&mut self) -> anyhow::Result<()> {
14        self.break_undo_coalescing();
15
16        #[cfg(feature = "validation")]
17        let mut moved = false;
18        #[cfg(not(feature = "validation"))]
19        let moved = false;
20
21        #[cfg(feature = "validation")]
22        {
23            let field_index = self.ui_state.current_field;
24            if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
25                if let Some(mask) = &cfg.display_mask {
26                    let display_pos = mask.raw_pos_to_display_pos(self.ui_state.cursor_pos);
27                    if let Some(prev_input) = mask.prev_input_position(display_pos) {
28                        let raw_pos = mask.display_pos_to_raw_pos(prev_input);
29                        let max_pos = self.current_text().chars().count();
30                        self.set_cursor_raw(raw_pos.min(max_pos));
31                        moved = true;
32                    } else {
33                        self.set_cursor_raw(0);
34                        moved = true;
35                    }
36                }
37            }
38        }
39
40        if !moved && self.ui_state.cursor_pos > 0 {
41            self.set_cursor_raw(self.ui_state.cursor_pos - 1);
42        }
43        Ok(())
44    }
45
46    /// Move cursor right within current field (mask-aware)
47    pub fn move_right(&mut self) -> anyhow::Result<()> {
48        self.break_undo_coalescing();
49
50        #[cfg(feature = "validation")]
51        let mut moved = false;
52        #[cfg(not(feature = "validation"))]
53        let moved = false;
54
55        #[cfg(feature = "validation")]
56        {
57            let field_index = self.ui_state.current_field;
58            if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
59                if let Some(mask) = &cfg.display_mask {
60                    let display_pos = mask.raw_pos_to_display_pos(self.ui_state.cursor_pos);
61                    let next_display_pos = mask.next_input_position(display_pos);
62                    let next_pos = mask.display_pos_to_raw_pos(next_display_pos);
63                    let max_pos = self.current_text().chars().count();
64                    self.set_cursor_raw(next_pos.min(max_pos));
65                    moved = true;
66                }
67            }
68        }
69
70        if !moved {
71            let max_pos = self.current_text().chars().count();
72            if self.ui_state.cursor_pos < max_pos {
73                self.set_cursor_raw(self.ui_state.cursor_pos + 1);
74            }
75        }
76        Ok(())
77    }
78
79    /// Move to start of current field (vim 0)
80    pub fn move_line_start(&mut self) {
81        let new_pos = line_start_position();
82        self.set_cursor_raw(new_pos);
83    }
84
85    /// Move to end of current field (vim $)
86    pub fn move_line_end(&mut self) {
87        let current_text = self.current_text();
88        let is_edit_mode = self.ui_state.current_mode == AppMode::Ins;
89
90        let new_pos = line_end_position(current_text, is_edit_mode);
91        self.set_cursor_raw(new_pos);
92    }
93
94    /// Set cursor to exact position (for f/F/t/T etc.)
95    pub fn set_cursor_position(&mut self, position: usize) {
96        let current_text = self.current_text();
97        let char_len = current_text.chars().count();
98        self.set_cursor_for_mode(position, char_len);
99    }
100}
101
102impl<D: DataProvider> EditorCore<D> {
103    fn move_up_to_previous_field_and_set_last<F>(&mut self, mut position_for_field: F) -> bool
104    where
105        F: FnMut(&str) -> usize,
106    {
107        let current_field = self.ui_state.current_field;
108        if !self.move_up() || self.ui_state.current_field == current_field {
109            return false;
110        }
111
112        let new_text = self.current_text();
113        if !new_text.is_empty() {
114            let pos = position_for_field(new_text);
115            self.set_cursor_raw(pos);
116        }
117        true
118    }
119
120    fn move_down_to_next_field_and_set<F>(
121        &mut self,
122        set_zero_when_empty: bool,
123        mut position_for_field: F,
124    ) -> bool
125    where
126        F: FnMut(&str) -> usize,
127    {
128        if !self.move_down() {
129            return false;
130        }
131
132        let new_text = self.current_text();
133        if new_text.is_empty() {
134            if set_zero_when_empty {
135                self.set_cursor_raw(0);
136            }
137        } else {
138            let pos = position_for_field(new_text);
139            let char_len = new_text.chars().count();
140            self.set_cursor_for_mode(pos, char_len);
141        }
142        true
143    }
144
145    /// Move to start of next word (vim w) - can cross field boundaries
146    pub fn move_word_next(&mut self) {
147        use crate::canvas::actions::movement::word::find_next_word_start;
148        let current_text = self.current_text();
149
150        if current_text.is_empty() {
151            self.move_down_to_next_field_and_set(false, |new_text| {
152                if new_text.chars().next().is_some_and(|c| !c.is_whitespace()) {
153                    0
154                } else {
155                    find_next_word_start(new_text, 0)
156                }
157            });
158            return;
159        }
160
161        let current_pos = self.ui_state.cursor_pos;
162        let new_pos = find_next_word_start(current_text, current_pos);
163
164        if new_pos >= current_text.chars().count() {
165            self.move_down_to_next_field_and_set(true, |new_text| {
166                if new_text.chars().next().is_some_and(|c| !c.is_whitespace()) {
167                    0
168                } else {
169                    find_next_word_start(new_text, 0)
170                }
171            });
172        } else {
173            let char_len = current_text.chars().count();
174            self.set_cursor_for_mode(new_pos, char_len);
175        }
176    }
177
178    /// Move to start of previous word (vim b) - can cross field boundaries
179    pub fn move_word_prev(&mut self) {
180        use crate::canvas::actions::movement::word::find_prev_word_start;
181        let current_text = self.current_text();
182
183        if current_text.is_empty() {
184            self.move_up_to_previous_field_and_set_last(find_last_word_start_in_field);
185            return;
186        }
187
188        let current_pos = self.ui_state.cursor_pos;
189
190        if current_pos == 0 {
191            self.move_up_to_previous_field_and_set_last(find_last_word_start_in_field);
192            return;
193        }
194
195        let new_pos = find_prev_word_start(current_text, current_pos);
196
197        if new_pos < current_pos {
198            self.set_cursor_raw(new_pos);
199        } else {
200            self.move_up_to_previous_field_and_set_last(find_last_word_start_in_field);
201        }
202    }
203
204    /// Move to end of current/next word (vim e) - can cross field boundaries
205    pub fn move_word_end(&mut self) {
206        use crate::canvas::actions::movement::word::find_word_end;
207        let current_text = self.current_text();
208        let char_len = current_text.chars().count();
209        let current_pos = self.ui_state.cursor_pos;
210
211        if current_text.is_empty() {
212            if self.move_down() {
213                self.set_cursor_raw(0);
214            }
215            return;
216        }
217
218        let mut target_pos = find_word_end(current_text, current_pos);
219
220        if target_pos <= current_pos && current_pos + 1 < char_len {
221            target_pos = find_word_end(current_text, current_pos + 1);
222        }
223
224        if target_pos > current_pos {
225            self.set_cursor_for_mode(target_pos, char_len);
226        } else {
227            if self.move_down() {
228                self.set_cursor_raw(0);
229
230                let next_text = self.current_text();
231                if !next_text.is_empty() {
232                    let first_word_end = find_word_end(next_text, 0);
233                    let next_char_len = next_text.chars().count();
234                    self.set_cursor_for_mode(first_word_end, next_char_len);
235                }
236            }
237        }
238    }
239
240    /// Move to end of previous word (vim ge) - can cross field boundaries
241    pub fn move_word_end_prev(&mut self) {
242        use crate::canvas::actions::movement::word::{
243            find_last_word_end_in_field, find_prev_word_end,
244        };
245        let current_text = self.current_text();
246
247        if current_text.is_empty() {
248            self.move_up_to_previous_field_and_set_last(find_last_word_end_in_field);
249            return;
250        }
251
252        let current_pos = self.ui_state.cursor_pos;
253
254        if current_pos == 0 {
255            self.move_up_to_previous_field_and_set_last(find_last_word_end_in_field);
256            return;
257        }
258
259        let new_pos = find_prev_word_end(current_text, current_pos);
260
261        if new_pos == current_pos {
262            self.move_up_to_previous_field_and_set_last(find_last_word_end_in_field);
263        } else {
264            let char_len = current_text.chars().count();
265            self.set_cursor_for_mode(new_pos, char_len);
266        }
267    }
268
269    /// Move to start of next big_word (vim W) - can cross field boundaries
270    pub fn move_big_word_next(&mut self) {
271        use crate::canvas::actions::movement::word::find_next_big_word_start;
272        let current_text = self.current_text();
273
274        if current_text.is_empty() {
275            self.move_down_to_next_field_and_set(false, |new_text| {
276                if new_text.chars().next().is_some_and(|c| !c.is_whitespace()) {
277                    0
278                } else {
279                    find_next_big_word_start(new_text, 0)
280                }
281            });
282            return;
283        }
284
285        let current_pos = self.ui_state.cursor_pos;
286        let new_pos = find_next_big_word_start(current_text, current_pos);
287
288        if new_pos >= current_text.chars().count() {
289            self.move_down_to_next_field_and_set(true, |new_text| {
290                if new_text.chars().next().is_some_and(|c| !c.is_whitespace()) {
291                    0
292                } else {
293                    find_next_big_word_start(new_text, 0)
294                }
295            });
296        } else {
297            let char_len = current_text.chars().count();
298            self.set_cursor_for_mode(new_pos, char_len);
299        }
300    }
301
302    /// Move to start of previous big_word (vim B) - can cross field boundaries
303    pub fn move_big_word_prev(&mut self) {
304        use crate::canvas::actions::movement::word::find_prev_big_word_start;
305        let current_text = self.current_text();
306
307        if current_text.is_empty() {
308            self.move_up_to_previous_field_and_set_last(find_last_big_word_start_in_field);
309            return;
310        }
311
312        let current_pos = self.ui_state.cursor_pos;
313
314        if current_pos == 0 {
315            self.move_up_to_previous_field_and_set_last(find_last_big_word_start_in_field);
316            return;
317        }
318
319        let new_pos = find_prev_big_word_start(current_text, current_pos);
320
321        if new_pos < current_pos {
322            self.set_cursor_raw(new_pos);
323        } else {
324            self.move_up_to_previous_field_and_set_last(find_last_big_word_start_in_field);
325        }
326    }
327
328    /// Move to end of current/next big_word (vim E) - can cross field boundaries
329    pub fn move_big_word_end(&mut self) {
330        use crate::canvas::actions::movement::word::find_big_word_end;
331        let current_text = self.current_text();
332
333        if current_text.is_empty() {
334            self.move_down_to_next_field_and_set(false, |new_text| find_big_word_end(new_text, 0));
335            return;
336        }
337
338        let current_pos = self.ui_state.cursor_pos;
339        let char_len = current_text.chars().count();
340        let new_pos = find_big_word_end(current_text, current_pos);
341
342        if new_pos == current_pos && current_pos + 1 < char_len {
343            let next_pos = find_big_word_end(current_text, current_pos + 1);
344            if next_pos < char_len {
345                self.set_cursor_for_mode(next_pos, char_len);
346                return;
347            }
348        }
349
350        if new_pos >= char_len.saturating_sub(1) {
351            self.move_down_to_next_field_and_set(false, |new_text| find_big_word_end(new_text, 0));
352        } else {
353            self.set_cursor_for_mode(new_pos, char_len);
354        }
355    }
356
357    /// Move to end of previous big_word (vim gE) - can cross field boundaries
358    pub fn move_big_word_end_prev(&mut self) {
359        use crate::canvas::actions::movement::word::{find_big_word_end, find_prev_big_word_end};
360
361        let current_text = self.current_text();
362
363        if current_text.is_empty() {
364            self.move_up_to_previous_field_and_set_last(|new_text| find_big_word_end(new_text, 0));
365            return;
366        }
367
368        let current_pos = self.ui_state.cursor_pos;
369        let new_pos = find_prev_big_word_end(current_text, current_pos);
370
371        if new_pos == current_pos {
372            self.move_up_to_previous_field_and_set_last(|new_text| find_big_word_end(new_text, 0));
373        } else {
374            let char_len = current_text.chars().count();
375            self.set_cursor_for_mode(new_pos, char_len);
376        }
377    }
378}