Skip to main content

tui_canvas/editor/
navigation.rs

1// src/editor/navigation.rs
2use crate::DataProvider;
3use crate::editor::EditorCore;
4
5impl<D: DataProvider> EditorCore<D> {
6    /// Resolves the target field, skipping those computed by scripts rather than human input.
7    #[cfg(feature = "computed")]
8    fn resolved_navigable_field(
9        &self,
10        requested_field: usize,
11        prev_field: usize,
12        field_count: usize,
13    ) -> usize {
14        let target_field = requested_field.min(field_count - 1);
15
16        let Some(computed_state) = &self.ui_state.computed else {
17            return target_field;
18        };
19
20        if !computed_state.is_computed_field(target_field) {
21            return target_field;
22        }
23
24        let search_forward_first = target_field >= prev_field;
25
26        let mut search_forward =
27            || ((target_field + 1)..field_count).find(|&i| !computed_state.is_computed_field(i));
28        let mut search_backward = || {
29            (0..target_field)
30                .rev()
31                .find(|&i| !computed_state.is_computed_field(i))
32        };
33
34        if search_forward_first {
35            search_forward()
36                .or_else(&mut search_backward)
37                .unwrap_or(prev_field)
38        } else {
39            search_backward()
40                .or_else(&mut search_forward)
41                .unwrap_or(prev_field)
42        }
43    }
44
45    pub fn transition_to_field(&mut self, new_field: usize) -> anyhow::Result<()> {
46        // Switching fields ends any in-progress undo-coalescing run.
47        self.break_undo_coalescing();
48
49        let field_count = self.data_provider.field_count();
50        if field_count == 0 {
51            self.clamp_current_field_to_count(field_count);
52            return Ok(());
53        }
54
55        let prev_field = self.clamp_current_field_to_count(field_count).unwrap_or(0);
56
57        #[cfg(feature = "computed")]
58        let target_field = self.resolved_navigable_field(new_field, prev_field, field_count);
59        #[cfg(not(feature = "computed"))]
60        let target_field = new_field.min(field_count - 1);
61
62        if target_field == prev_field {
63            return Ok(());
64        }
65
66        #[cfg(feature = "validation")]
67        self.ui_state.validation.clear_last_switch_block();
68
69        #[cfg(feature = "validation")]
70        {
71            let current_text = self.current_text();
72            if !self
73                .ui_state
74                .validation
75                .allows_field_switch(prev_field, current_text)
76            {
77                if let Some(reason) = self
78                    .ui_state
79                    .validation
80                    .field_switch_block_reason(prev_field, current_text)
81                {
82                    self.ui_state
83                        .validation
84                        .set_last_switch_block(reason.clone());
85                    tracing::debug!("Field switch blocked: {}", reason);
86                    return Err(anyhow::anyhow!("Cannot switch fields: {}", reason));
87                }
88            }
89        }
90
91        #[cfg(feature = "validation")]
92        {
93            let text = self.data_provider.field_value(prev_field).to_string();
94            let _ = self
95                .ui_state
96                .validation
97                .validate_field_content(prev_field, &text);
98
99            if let Some(cfg) = self.ui_state.validation.get_field_config(prev_field) {
100                if cfg.external_validation_enabled && !text.is_empty() {
101                    self.set_external_validation(
102                        prev_field,
103                        crate::validation::ExternalValidationState::Validating,
104                    );
105
106                    if let Some(cb) = self.external_validation_callback.as_mut() {
107                        let final_state = cb(prev_field, &text);
108                        self.set_external_validation(prev_field, final_state);
109                    }
110                }
111            }
112        }
113
114        let ideal_cursor_column = self.ui_state.ideal_cursor_column;
115        self.ui_state.move_to_field(target_field, field_count);
116
117        let current_text = self.current_text();
118        let max_pos = current_text.chars().count();
119        self.set_cursor_for_mode(ideal_cursor_column, max_pos);
120        self.ui_state.ideal_cursor_column = ideal_cursor_column;
121
122        #[cfg(feature = "suggestions")]
123        {
124            self.dismiss_suggestions();
125        }
126
127        Ok(())
128    }
129
130    /// Move to first line (vim gg)
131    pub fn move_first_line(&mut self) -> anyhow::Result<()> {
132        self.transition_to_field(0)
133    }
134
135    /// Move to last line (vim G)
136    pub fn move_last_line(&mut self) -> anyhow::Result<()> {
137        let last_field = self.data_provider.field_count().saturating_sub(1);
138        self.transition_to_field(last_field)
139    }
140
141    /// Move to previous field (vim k / up)
142    /// Returns true if moved, false if already at top
143    pub fn move_up(&mut self) -> bool {
144        if self.ui_state.current_field == 0 {
145            return false;
146        }
147        let before = self.ui_state.current_field;
148        let new_field = self.ui_state.current_field - 1;
149        self.transition_to_field(new_field).is_ok() && self.ui_state.current_field != before
150    }
151
152    /// Move to next field (vim j / down)
153    /// Returns true if moved, false if already at bottom
154    pub fn move_down(&mut self) -> bool {
155        let last = self.data_provider.field_count().saturating_sub(1);
156        if self.ui_state.current_field >= last {
157            return false;
158        }
159        let before = self.ui_state.current_field;
160        let new_field = self.ui_state.current_field + 1;
161        self.transition_to_field(new_field).is_ok() && self.ui_state.current_field != before
162    }
163
164    /// Move to next field cyclic
165    pub fn move_to_next_field(&mut self) -> anyhow::Result<()> {
166        let field_count = self.data_provider.field_count();
167        if field_count == 0 {
168            return Ok(());
169        }
170        let new_field = (self.ui_state.current_field + 1) % field_count;
171        self.transition_to_field(new_field)
172    }
173
174    pub fn prev_field(&mut self) -> bool {
175        self.move_up()
176    }
177
178    pub fn next_field(&mut self) -> bool {
179        self.move_down()
180    }
181}
182
183#[cfg(all(test, feature = "computed"))]
184mod tests {
185    use super::*;
186    use crate::computed::{ComputedContext, ComputedProvider};
187
188    #[derive(Clone, Default)]
189    struct TestProvider {
190        fields: Vec<(&'static str, String)>,
191    }
192
193    impl TestProvider {
194        fn new(names: &[&'static str]) -> Self {
195            Self {
196                fields: names.iter().map(|name| (*name, String::new())).collect(),
197            }
198        }
199    }
200
201    impl DataProvider for TestProvider {
202        fn field_count(&self) -> usize {
203            self.fields.len()
204        }
205
206        fn field_name(&self, index: usize) -> &str {
207            self.fields[index].0
208        }
209
210        fn field_value(&self, index: usize) -> &str {
211            &self.fields[index].1
212        }
213
214        fn set_field_value(&mut self, index: usize, value: String) {
215            self.fields[index].1 = value;
216        }
217    }
218
219    struct TestComputedProvider;
220
221    impl ComputedProvider for TestComputedProvider {
222        fn handles_field(&self, field_index: usize) -> bool {
223            matches!(field_index, 2 | 3 | 4)
224        }
225
226        fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
227            vec![0]
228        }
229
230        fn compute_field(&mut self, _context: ComputedContext) -> String {
231            String::new()
232        }
233    }
234
235    #[test]
236    fn move_down_skips_trailing_computed_fields() {
237        let provider = TestProvider::new(&["a", "b", "c", "d", "e"]);
238        let mut editor = EditorCore::new(provider);
239        editor.register_computed_provider(&TestComputedProvider);
240
241        assert!(editor.transition_to_field(1).is_ok());
242        assert_eq!(editor.current_field(), 1);
243
244        assert!(!editor.move_down());
245        assert_eq!(editor.current_field(), 1);
246    }
247
248    #[test]
249    fn move_last_line_lands_on_last_editable_field() {
250        let provider = TestProvider::new(&["a", "b", "c", "d", "e"]);
251        let mut editor = EditorCore::new(provider);
252        editor.register_computed_provider(&TestComputedProvider);
253
254        assert!(editor.move_last_line().is_ok());
255        assert_eq!(editor.current_field(), 1);
256    }
257}