Skip to main content

tui_canvas/canvas/actions/
dispatch.rs

1// src/canvas/actions/dispatch.rs
2//! Provides the typed dispatcher that maps CanvasAction → TextFormState method calls.
3
4use super::types::{ActionResult, CanvasAction};
5use crate::DataProvider;
6use crate::editor::EditorCore;
7use std::fmt::Display;
8
9impl<D: DataProvider> EditorCore<D> {
10    fn into_action_result<T, E: Display>(result: Result<T, E>) -> ActionResult {
11        match result {
12            Ok(_) => ActionResult::Success,
13            Err(err) => ActionResult::Error(err.to_string()),
14        }
15    }
16
17    /// Execute a CanvasAction on this editor instance.
18    pub fn execute(&mut self, action: CanvasAction) -> ActionResult {
19        use CanvasAction::*;
20        match action {
21            // Mode switching
22            EnterEditMode => {
23                self.enter_edit_mode();
24                ActionResult::Success
25            }
26            EnterEditModeAfter => {
27                self.enter_append_mode();
28                ActionResult::Success
29            }
30            ExitEditMode => Self::into_action_result(self.exit_edit_mode()),
31            EnterHighlightMode => {
32                self.enter_highlight_mode();
33                ActionResult::Success
34            }
35            EnterHighlightModeLinewise => {
36                self.enter_highlight_line_mode();
37                ActionResult::Success
38            }
39            ExitHighlightMode => {
40                self.exit_highlight_mode();
41                ActionResult::Success
42            }
43
44            // Movement
45            MoveLeft => {
46                if self.is_highlight_mode() {
47                    self.move_left_with_selection();
48                    ActionResult::Success
49                } else {
50                    Self::into_action_result(self.move_left())
51                }
52            }
53            MoveRight => {
54                if self.is_highlight_mode() {
55                    self.move_right_with_selection();
56                    ActionResult::Success
57                } else {
58                    Self::into_action_result(self.move_right())
59                }
60            }
61            MoveUp => {
62                if self.is_highlight_mode() {
63                    self.move_up_with_selection();
64                } else {
65                    self.move_up();
66                }
67                ActionResult::Success
68            }
69            MoveDown => {
70                if self.is_highlight_mode() {
71                    self.move_down_with_selection();
72                } else {
73                    self.move_down();
74                }
75                ActionResult::Success
76            }
77            MoveWordNext => {
78                if self.is_highlight_mode() {
79                    self.move_word_next_with_selection();
80                } else {
81                    self.move_word_next();
82                }
83                ActionResult::Success
84            }
85            MoveWordPrev => {
86                if self.is_highlight_mode() {
87                    self.move_word_prev_with_selection();
88                } else {
89                    self.move_word_prev();
90                }
91                ActionResult::Success
92            }
93            MoveWordEnd => {
94                if self.is_highlight_mode() {
95                    self.move_word_end_with_selection();
96                } else {
97                    self.move_word_end();
98                }
99                ActionResult::Success
100            }
101            MoveWordEndPrev => {
102                if self.is_highlight_mode() {
103                    self.move_word_end_prev_with_selection();
104                } else {
105                    self.move_word_end_prev();
106                }
107                ActionResult::Success
108            }
109            MoveBigWordNext => {
110                if self.is_highlight_mode() {
111                    self.move_big_word_next_with_selection();
112                } else {
113                    self.move_big_word_next();
114                }
115                ActionResult::Success
116            }
117            MoveBigWordPrev => {
118                if self.is_highlight_mode() {
119                    self.move_big_word_prev_with_selection();
120                } else {
121                    self.move_big_word_prev();
122                }
123                ActionResult::Success
124            }
125            MoveBigWordEnd => {
126                if self.is_highlight_mode() {
127                    self.move_big_word_end_with_selection();
128                } else {
129                    self.move_big_word_end();
130                }
131                ActionResult::Success
132            }
133            MoveBigWordEndPrev => {
134                if self.is_highlight_mode() {
135                    self.move_big_word_end_prev_with_selection();
136                } else {
137                    self.move_big_word_end_prev();
138                }
139                ActionResult::Success
140            }
141            MoveFirstLine => Self::into_action_result(self.move_first_line()),
142            MoveLastLine => Self::into_action_result(self.move_last_line()),
143            MoveLineStart => {
144                if self.is_highlight_mode() {
145                    self.move_line_start_with_selection();
146                } else {
147                    self.move_line_start();
148                }
149                ActionResult::Success
150            }
151            MoveLineEnd => {
152                if self.is_highlight_mode() {
153                    self.move_line_end_with_selection();
154                } else {
155                    self.move_line_end();
156                }
157                ActionResult::Success
158            }
159            NextField => {
160                self.next_field();
161                ActionResult::Success
162            }
163            PrevField => {
164                self.prev_field();
165                ActionResult::Success
166            }
167
168            // Editing
169            DeleteBackward => Self::into_action_result(self.delete_backward()),
170            DeleteForward => Self::into_action_result(self.delete_forward()),
171            Undo => {
172                self.undo();
173                ActionResult::Success
174            }
175            Redo => {
176                self.redo();
177                ActionResult::Success
178            }
179            OpenLineBelow => Self::into_action_result(self.open_line_below()),
180            OpenLineAbove => Self::into_action_result(self.open_line_above()),
181
182            // Suggestions
183            #[cfg(feature = "suggestions")]
184            TriggerSuggestions => {
185                let _ = self.trigger_suggestions().map(|(idx, query)| {
186                    let items = self.data_provider.fetch_suggestions_sync(idx, &query);
187                    if items.is_empty() {
188                        self.dismiss_suggestions();
189                    } else {
190                        self.apply_suggestions(items);
191                    }
192                });
193                ActionResult::Success
194            }
195            #[cfg(feature = "suggestions")]
196            SuggestionUp => {
197                self.suggestions_prev();
198                ActionResult::Success
199            }
200            #[cfg(feature = "suggestions")]
201            SuggestionDown => {
202                self.suggestions_next();
203                ActionResult::Success
204            }
205            #[cfg(feature = "suggestions")]
206            SelectSuggestion => {
207                let _ = self.apply_suggestion();
208                ActionResult::Success
209            }
210            #[cfg(feature = "suggestions")]
211            ExitSuggestions => {
212                self.dismiss_suggestions();
213                ActionResult::Success
214            }
215            #[cfg(not(feature = "suggestions"))]
216            TriggerSuggestions | SuggestionUp | SuggestionDown | SelectSuggestion
217            | ExitSuggestions => ActionResult::Message("suggestions feature is disabled".into()),
218
219            // Any actions that require arguments / not handled directly
220            InsertChar(c) => Self::into_action_result(self.insert_char(c)),
221
222            // Fallback: custom or unhandled
223            Custom(name) => ActionResult::Message(format!("Unhandled custom action: {name}")),
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::canvas::state::SelectionState;
232
233    #[derive(Clone)]
234    struct TestProvider {
235        fields: Vec<(&'static str, String)>,
236    }
237
238    impl TestProvider {
239        fn new(values: &[&'static str]) -> Self {
240            Self {
241                fields: values
242                    .iter()
243                    .enumerate()
244                    .map(|(i, value)| {
245                        let name = match i {
246                            0 => "a",
247                            1 => "b",
248                            _ => "c",
249                        };
250                        (name, (*value).to_string())
251                    })
252                    .collect(),
253            }
254        }
255    }
256
257    impl DataProvider for TestProvider {
258        fn field_count(&self) -> usize {
259            self.fields.len()
260        }
261
262        fn field_name(&self, index: usize) -> &str {
263            self.fields[index].0
264        }
265
266        fn field_value(&self, index: usize) -> &str {
267            &self.fields[index].1
268        }
269
270        fn set_field_value(&mut self, index: usize, value: String) {
271            self.fields[index].1 = value;
272        }
273    }
274
275    #[test]
276    fn dispatch_extends_visual_selection_without_reanchoring() {
277        let mut editor = EditorCore::new(TestProvider::new(&["alpha", "beta"]));
278
279        assert!(
280            editor
281                .execute(CanvasAction::EnterHighlightMode)
282                .is_success()
283        );
284        assert!(editor.execute(CanvasAction::MoveRight).is_success());
285        assert_eq!(editor.current_field(), 0);
286        assert_eq!(editor.cursor_position(), 1);
287        assert!(matches!(
288            editor.selection_state(),
289            SelectionState::Characterwise { anchor: (0, 0) }
290        ));
291
292        assert!(editor.execute(CanvasAction::MoveDown).is_success());
293        assert_eq!(editor.current_field(), 1);
294        assert!(matches!(
295            editor.selection_state(),
296            SelectionState::Characterwise { anchor: (0, 0) }
297        ));
298    }
299}