Skip to main content

tui_canvas/textform/
state.rs

1#[cfg(feature = "crossterm")]
2use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
3#[cfg(feature = "cursor-style")]
4use std::io;
5
6use std::ops::{Deref, DerefMut};
7
8#[cfg(feature = "cursor-style")]
9use crate::CursorManager;
10use crate::canvas::actions::{ActionResult, CanvasAction};
11#[cfg(feature = "keybindings")]
12use crate::canvas::state::SelectionState;
13#[cfg(feature = "gui")]
14use crate::gui_utils::{display_cols_up_to, display_width};
15use crate::{DataProvider, editor::EditorCore};
16#[cfg(feature = "gui")]
17use ratatui::{layout::Rect, widgets::Block};
18
19#[cfg(feature = "keybindings")]
20use crate::{
21    editor::{
22        behavior::{KeybindingParadigm, VimOperator, VimPendingOperator, YankRegister},
23        paradigm::helix_word::HelixWordTarget,
24        product::{KeybindingProduct, handle_product_key_event},
25    },
26    integration::focus_handoff::{BoundaryExit, key_outcome_for_vertical_navigation},
27    keybindings::{CanvasKeyAction, CanvasKeyBindings, KeyEventOutcome},
28};
29
30#[cfg(feature = "keybindings")]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum TextFormActionPolicy {
33    SharedCore,
34    ProductHandled,
35    StructuralNoOp,
36    Unsupported,
37}
38
39#[cfg(feature = "keybindings")]
40fn textform_action_policy(action: &CanvasKeyAction) -> TextFormActionPolicy {
41    use CanvasKeyAction::*;
42
43    match action {
44        MoveLeft
45        | MoveRight
46        | MoveUp
47        | MoveDown
48        | MoveLineStart
49        | MoveLineEnd
50        | MoveHalfPageUp
51        | MoveHalfPageDown
52        | MoveFirstLine
53        | MoveLastLine
54        | MoveWordNext
55        | MoveWordPrev
56        | MoveWordEnd
57        | MoveWordEndPrev
58        | MoveBigWordNext
59        | MoveBigWordPrev
60        | MoveBigWordEnd
61        | MoveBigWordEndPrev
62        | DeleteCharBackward
63        | DeleteCharForward
64        | Undo
65        | Redo
66        | OpenSuggestions
67        | ApplySuggestion
68        | ExitSuggestions
69        | SuggestionDown
70        | SuggestionUp
71        | EnterEditModeBefore
72        | EnterEditModeAfter
73        | ExitEditMode
74        | EnterHighlightMode
75        | EnterHighlightModeLinewise
76        | ExitHighlightMode => TextFormActionPolicy::SharedCore,
77
78        NextField
79        | PrevField
80        | OpenLineBelow
81        | OpenLineAbove
82        | EnterEditModeLineStart
83        | EnterEditModeLineEnd
84        | DeleteLine
85        | DeleteToLineEnd
86        | ChangeLine
87        | ChangeToLineEnd
88        | OperatorDelete
89        | OperatorChange
90        | OperatorYank
91        | YankLine
92        | CopyLine
93        | CutLine
94        | PasteAfter
95        | PasteBefore
96        | DeleteSelection
97        | DeleteSelectionNoYank
98        | ChangeSelection
99        | ChangeSelectionNoYank
100        | YankSelection
101        | CollapseSelection
102        | ExtendLineBelow
103        | ExtendToLineBounds => TextFormActionPolicy::ProductHandled,
104
105        JoinLineBelow | MoveLineUp | MoveLineDown | DuplicateLineUp | DuplicateLineDown => {
106            TextFormActionPolicy::StructuralNoOp
107        }
108
109        EnterDecider
110        | Exit
111        | SearchNext
112        | SearchPrev
113        | SelectAll
114        | FlipSelections
115        | SwitchCase
116        | SwitchToLowercase
117        | SwitchToUppercase
118        | TrimSelections
119        | GotoFirstNonWhitespace
120        | MovePageUp
121        | MovePageDown
122        | SearchSelection
123        | EnsureSelectionForward
124        | MatchBrackets
125        | IndentSelection
126        | UnindentSelection
127        | IncrementNumber
128        | DecrementNumber
129        | FindNextChar
130        | FindPrevChar
131        | TillNextChar
132        | TillPrevChar
133        | ReplaceChar
134        | RepeatLastFind
135        | RepeatLastFindReverse
136        | SurroundAdd
137        | SurroundDelete
138        | SurroundReplace
139        | DeleteWordBackward
140        | DeleteToLineStart
141        | DeleteWordForward
142        | ClearSearch
143        | SelectLeft
144        | SelectRight
145        | SelectUp
146        | SelectDown
147        | SelectWordPrev
148        | SelectWordNext
149        | SelectLineStart
150        | SelectLineEnd
151        | SelectDocStart
152        | SelectDocEnd
153        | Unknown(_) => TextFormActionPolicy::Unsupported,
154    }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum TextFormEventOutcome {
159    Ignored,
160    Handled,
161    Submitted,
162}
163
164#[derive(Debug)]
165pub struct TextFormState<D: DataProvider> {
166    pub(crate) core: EditorCore<D>,
167    fixed_field_count: usize,
168}
169
170impl<D: DataProvider + Default> Default for TextFormState<D> {
171    fn default() -> Self {
172        Self::new(D::default())
173    }
174}
175
176impl<D: DataProvider> TextFormState<D> {
177    pub fn new(data_provider: D) -> Self {
178        let fixed_field_count = data_provider.field_count();
179        Self {
180            // A form is a fixed-row buffer: its dispatch calls the `fixed`
181            // row operations (clear/overwrite slots) rather than the `dynamic`
182            // ones a text area uses.
183            core: EditorCore::new(data_provider),
184            fixed_field_count,
185        }
186    }
187
188    pub fn with_provider(data_provider: D) -> Self {
189        Self::new(data_provider)
190    }
191
192    pub fn core(&self) -> &EditorCore<D> {
193        &self.core
194    }
195
196    pub fn fixed_field_count(&self) -> usize {
197        self.fixed_field_count
198    }
199
200    fn sync_fixed_rows(&mut self) {
201        let actual = self.core.data_provider().field_count();
202        self.fixed_field_count = actual;
203        self.core
204            .clamp_current_field_to_count(self.fixed_field_count);
205    }
206
207    fn with_fixed_rows<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
208        self.sync_fixed_rows();
209        let result = f(self);
210        self.sync_fixed_rows();
211        result
212    }
213
214    #[cfg(feature = "crossterm")]
215    pub fn handle_event(&mut self, event: Event) -> TextFormEventOutcome {
216        self.with_fixed_rows(|this| match event {
217            Event::Key(key) => this.input(key),
218            Event::Paste(text) => this.paste(&text),
219            _ => TextFormEventOutcome::Ignored,
220        })
221    }
222
223    pub fn paste(&mut self, text: &str) -> TextFormEventOutcome {
224        self.with_fixed_rows(|this| {
225            let filtered: String = text
226                .chars()
227                .filter(|&ch| ch != '\n' && ch != '\r')
228                .collect();
229
230            if filtered.is_empty() {
231                return TextFormEventOutcome::Ignored;
232            }
233
234            this.core.enter_edit_mode();
235            let _ = this.core.insert_text(&filtered);
236            TextFormEventOutcome::Handled
237        })
238    }
239
240    #[cfg(feature = "crossterm")]
241    pub fn input(&mut self, key: KeyEvent) -> TextFormEventOutcome {
242        self.with_fixed_rows(|this| {
243            if key.kind != KeyEventKind::Press {
244                return TextFormEventOutcome::Ignored;
245            }
246
247            match (key.code, key.modifiers) {
248                (KeyCode::Enter, _) => this.enter_next_field_or_submit(),
249                (KeyCode::Tab, _) => {
250                    let _ = this.core.next_field();
251                    TextFormEventOutcome::Handled
252                }
253                (KeyCode::BackTab, _) => {
254                    let _ = this.core.prev_field();
255                    TextFormEventOutcome::Handled
256                }
257                (KeyCode::Backspace, _) => {
258                    let _ = this.core.delete_backward();
259                    TextFormEventOutcome::Handled
260                }
261                (KeyCode::Delete, _) => {
262                    let _ = this.core.delete_forward();
263                    TextFormEventOutcome::Handled
264                }
265                (KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => {
266                    this.core.move_word_prev();
267                    TextFormEventOutcome::Handled
268                }
269                (KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => {
270                    this.core.move_word_next();
271                    TextFormEventOutcome::Handled
272                }
273                (KeyCode::Left, _) => {
274                    let _ = this.core.move_left();
275                    TextFormEventOutcome::Handled
276                }
277                (KeyCode::Right, _) => {
278                    let _ = this.core.move_right();
279                    TextFormEventOutcome::Handled
280                }
281                (KeyCode::Up, _) => {
282                    let _ = this.core.move_up();
283                    TextFormEventOutcome::Handled
284                }
285                (KeyCode::Down, _) => {
286                    let _ = this.core.move_down();
287                    TextFormEventOutcome::Handled
288                }
289                (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
290                    this.core.move_line_start();
291                    TextFormEventOutcome::Handled
292                }
293                (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
294                    this.core.move_line_end();
295                    TextFormEventOutcome::Handled
296                }
297                (KeyCode::Char('b'), KeyModifiers::ALT) => {
298                    this.core.move_word_prev();
299                    TextFormEventOutcome::Handled
300                }
301                (KeyCode::Char('f'), KeyModifiers::ALT) => {
302                    this.core.move_word_next();
303                    TextFormEventOutcome::Handled
304                }
305                (KeyCode::Char('e'), KeyModifiers::ALT) => {
306                    this.core.move_word_end();
307                    TextFormEventOutcome::Handled
308                }
309                (KeyCode::Esc, _) => {
310                    if this.core.mode() == crate::canvas::modes::AppMode::Ins {
311                        let _ = this.core.exit_edit_mode();
312                        TextFormEventOutcome::Handled
313                    } else {
314                        TextFormEventOutcome::Ignored
315                    }
316                }
317                (KeyCode::Char(c), m)
318                    if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) =>
319                {
320                    this.core.enter_edit_mode();
321                    let _ = this.core.insert_char(c);
322                    TextFormEventOutcome::Handled
323                }
324                _ => TextFormEventOutcome::Ignored,
325            }
326        })
327    }
328
329    #[cfg(feature = "keybindings")]
330    pub fn handle_key_event(&mut self, evt: KeyEvent) -> KeyEventOutcome {
331        self.with_fixed_rows(|this| handle_product_key_event(this, evt))
332    }
333
334    /// Whether a multi-key command (key sequence, count, or pending operator) is
335    /// in flight, so a host should keep routing keys here rather than letting an
336    /// outer keymap claim the next stroke.
337    #[cfg(feature = "keybindings")]
338    pub fn is_sequence_pending(&self) -> bool {
339        self.core.is_sequence_pending()
340    }
341
342    #[cfg(feature = "cursor-style")]
343    pub fn update_cursor_style(&self) -> io::Result<()> {
344        CursorManager::update_for_mode(self.core.mode())
345    }
346
347    #[cfg(not(feature = "cursor-style"))]
348    pub fn update_cursor_style(&self) -> std::io::Result<()> {
349        Ok(())
350    }
351
352    #[cfg(feature = "gui")]
353    pub fn cursor(&self, area: Rect, block: Option<&Block<'_>>) -> (u16, u16) {
354        let inner = if let Some(block) = block {
355            block.inner(area)
356        } else {
357            area
358        };
359        let provider = self.core.data_provider();
360        let label_width = (0..provider.field_count())
361            .map(|index| display_width(provider.field_name(index)))
362            .max()
363            .unwrap_or(0)
364            .saturating_add(1);
365        let row = self.core.current_field() as u16;
366        let current_text = self.core.current_text();
367        let cursor_cols = display_cols_up_to(current_text, self.core.display_cursor_position());
368
369        (
370            inner
371                .x
372                .saturating_add(label_width)
373                .saturating_add(cursor_cols),
374            inner.y.saturating_add(row),
375        )
376    }
377
378    #[cfg(feature = "keybindings")]
379    pub fn use_keybinding_preset(
380        &mut self,
381        preset: crate::keybindings::BuiltinCanvasKeybindingPreset,
382    ) {
383        self.core.set_keybinding_preset(preset);
384    }
385
386    #[cfg(feature = "keybindings")]
387    pub fn set_keybindings(&mut self, keybindings: CanvasKeyBindings) {
388        self.core.set_keybindings(keybindings);
389    }
390
391    pub fn clear_current_field(&mut self) {
392        self.with_fixed_rows(|this| this.core.set_current_field_value(String::new()))
393    }
394
395    pub fn clear_field(&mut self, field_index: usize) {
396        self.with_fixed_rows(|this| {
397            if field_index < this.fixed_field_count {
398                this.core.set_field_value(field_index, String::new());
399            }
400        })
401    }
402
403    pub fn clear_current_and_following_fields(&mut self, count: usize) {
404        self.with_fixed_rows(|this| {
405            if this.fixed_field_count == 0 {
406                return;
407            }
408
409            let start = this.core.current_field();
410            let end = start
411                .saturating_add(count.max(1))
412                .saturating_sub(1)
413                .min(this.fixed_field_count - 1);
414            this.clear_field_range(start, end);
415        })
416    }
417
418    #[cfg(feature = "keybindings")]
419    fn selected_fixed_field_values(&self, start: usize, end: usize) -> Vec<String> {
420        if self.fixed_field_count == 0 || start > end {
421            return Vec::new();
422        }
423
424        let end = end.min(self.fixed_field_count - 1);
425        (start..=end)
426            .map(|field_index| {
427                self.core
428                    .data_provider()
429                    .field_value(field_index)
430                    .to_string()
431            })
432            .collect()
433    }
434
435    #[cfg(feature = "keybindings")]
436    fn yank_current_and_following_fields(&mut self, count: usize) {
437        if self.fixed_field_count == 0 {
438            return;
439        }
440
441        let start = self.core.current_field();
442        let end = start
443            .saturating_add(count.max(1))
444            .saturating_sub(1)
445            .min(self.fixed_field_count - 1);
446        let lines = self.selected_fixed_field_values(start, end);
447        if !lines.is_empty() {
448            self.core.behavior_state.yank_mut().set_line_register(lines);
449        }
450    }
451
452    #[cfg(feature = "keybindings")]
453    fn cut_current_and_following_fields(&mut self, count: usize) {
454        self.yank_current_and_following_fields(count);
455        self.clear_current_and_following_fields(count);
456    }
457
458    fn clear_field_range(&mut self, start: usize, end: usize) {
459        if self.fixed_field_count == 0 {
460            return;
461        }
462
463        let start = start.min(self.fixed_field_count - 1);
464        let end = end.min(self.fixed_field_count - 1);
465        if start > end {
466            return;
467        }
468
469        for field_index in start..=end {
470            self.core.set_field_value(field_index, String::new());
471        }
472    }
473
474    #[cfg(feature = "keybindings")]
475    fn field_char_len(&self, field_index: usize) -> usize {
476        self.core
477            .data_provider()
478            .field_value(field_index)
479            .chars()
480            .count()
481    }
482
483    #[cfg(feature = "keybindings")]
484    fn delete_selection_helix(&mut self, yank: bool, count: usize) {
485        for _ in 0..count.max(1) {
486            if !self.core.delete_selection_once_fixed(yank) {
487                break;
488            }
489        }
490        self.core.finish_helix_selection_edit();
491    }
492
493    #[cfg(feature = "keybindings")]
494    fn change_selection_helix(&mut self, yank: bool, count: usize) {
495        for _ in 0..count.max(1) {
496            if !self.core.delete_selection_once_fixed(yank) {
497                break;
498            }
499        }
500        self.core.enter_edit_mode();
501    }
502
503    #[cfg(feature = "keybindings")]
504    fn character_paste_position_helix(&self, after: bool) -> (usize, usize) {
505        match self.core.selection_state() {
506            SelectionState::Characterwise { anchor } => {
507                let cursor = (self.core.current_field(), self.core.cursor_position());
508                let start = (*anchor).min(cursor);
509                let end = (*anchor).max(cursor);
510                if after {
511                    (end.0, end.1.saturating_add(1))
512                } else {
513                    start
514                }
515            }
516            _ => {
517                let field = self.core.current_field();
518                let len = self.field_char_len(field);
519                let cursor = self.core.cursor_position().min(len);
520                if after {
521                    (field, cursor.saturating_add(1).min(len))
522                } else {
523                    (field, cursor)
524                }
525            }
526        }
527    }
528
529    #[cfg(feature = "keybindings")]
530    fn paste_register_helix(&mut self, after: bool, count: usize) {
531        let Some(register) = self.core.behavior_state.yank().register().cloned() else {
532            return;
533        };
534
535        match register {
536            YankRegister::Lines(lines) => {
537                self.core.paste_lines_fixed(after, count, lines);
538                self.core.ensure_helix_primary_selection();
539            }
540            YankRegister::Text(lines) => {
541                let text = crate::editor::rows::repeated_text(&lines, count);
542                if text.is_empty() || self.fixed_field_count == 0 {
543                    return;
544                }
545                let (field, col) = self.character_paste_position_helix(after);
546                if field >= self.fixed_field_count {
547                    return;
548                }
549                let (target_field, target_col) = self.core.insert_text_fixed(field, col, &text);
550                let _ = self.core.transition_to_field(target_field);
551                self.core.set_cursor_position(target_col);
552                self.core.ensure_helix_primary_selection();
553            }
554        }
555    }
556
557    pub fn change_current_field(&mut self) {
558        self.with_fixed_rows(|this| {
559            this.core.set_current_field_value(String::new());
560            this.core.enter_edit_mode();
561        })
562    }
563
564    pub fn delete_to_field_end(&mut self) {
565        self.with_fixed_rows(|this| {
566            let cursor = this.core.cursor_position();
567            let kept: String = this.core.current_text().chars().take(cursor).collect();
568            this.core.set_current_field_value(kept);
569            this.core.set_cursor_position(cursor);
570        })
571    }
572
573    pub fn execute(&mut self, action: CanvasAction) -> ActionResult {
574        self.with_fixed_rows(|this| match action {
575            CanvasAction::OpenLineBelow => {
576                let _ = this.core.open_line_below();
577                ActionResult::Success
578            }
579            CanvasAction::OpenLineAbove => {
580                let _ = this.core.open_line_above();
581                ActionResult::Success
582            }
583            other => this.core.execute(other),
584        })
585    }
586
587    pub fn undo(&mut self) -> bool {
588        self.with_fixed_rows(|this| this.core.undo())
589    }
590
591    pub fn redo(&mut self) -> bool {
592        self.with_fixed_rows(|this| this.core.redo())
593    }
594
595    #[cfg(feature = "crossterm")]
596    fn enter_next_field_or_submit(&mut self) -> TextFormEventOutcome {
597        let last = self.core.data_provider().field_count().saturating_sub(1);
598        if self.core.current_field() >= last {
599            TextFormEventOutcome::Submitted
600        } else {
601            let _ = self.core.next_field();
602            TextFormEventOutcome::Handled
603        }
604    }
605
606    #[cfg(feature = "keybindings")]
607    fn move_next_field_count(&mut self, count: usize) {
608        for _ in 0..count {
609            let _ = self.core.next_field();
610        }
611    }
612
613    #[cfg(feature = "keybindings")]
614    fn move_prev_field_count(&mut self, count: usize) {
615        for _ in 0..count {
616            let _ = self.core.prev_field();
617        }
618    }
619
620    #[cfg(feature = "keybindings")]
621    fn execute_canvas_key_action(
622        &mut self,
623        action: &CanvasKeyAction,
624        count: usize,
625    ) -> KeyEventOutcome {
626        let Some(canvas_action) = action.to_canvas_action() else {
627            return KeyEventOutcome::NotMatched;
628        };
629
630        let boundary = match action {
631            CanvasKeyAction::MoveUp | CanvasKeyAction::PrevField => Some(BoundaryExit::Top),
632            CanvasKeyAction::MoveDown | CanvasKeyAction::NextField => Some(BoundaryExit::Bottom),
633            _ => None,
634        };
635        let before_field = self.core.current_field();
636
637        let mut result = ActionResult::Success;
638        for _ in 0..count {
639            result = self.execute(canvas_action.clone());
640        }
641
642        if let Some(boundary) = boundary {
643            let moved = self.core.current_field() != before_field;
644            return key_outcome_for_vertical_navigation(moved, boundary);
645        }
646
647        match result {
648            ActionResult::Success => KeyEventOutcome::Consumed(None),
649            ActionResult::Message(msg) | ActionResult::Error(msg) => {
650                KeyEventOutcome::Consumed(Some(msg))
651            }
652        }
653    }
654
655    #[cfg(feature = "keybindings")]
656    fn begin_operator_vim(&mut self, operator: VimOperator, count: usize) {
657        let anchor = (self.core.current_field(), self.core.cursor_position());
658        self.core
659            .behavior_state
660            .vim_mut()
661            .set_pending_operator(VimPendingOperator {
662                operator,
663                count: count.max(1),
664                anchor,
665            });
666    }
667
668    #[cfg(feature = "keybindings")]
669    fn apply_operator_motion_vim(
670        &mut self,
671        action: &CanvasKeyAction,
672        motion_count: usize,
673    ) -> KeyEventOutcome {
674        let Some(pending) = self.core.behavior_state.vim().pending_operator() else {
675            return self.execute_canvas_key_action(action, motion_count);
676        };
677
678        self.core.behavior_state.vim_mut().clear_pending_operator();
679        let total = pending.count.saturating_mul(motion_count.max(1)).max(1);
680
681        if matches!(
682            action,
683            CanvasKeyAction::OperatorDelete
684                | CanvasKeyAction::OperatorChange
685                | CanvasKeyAction::OperatorYank
686        ) {
687            let start = pending.anchor.0;
688            let end = start.saturating_add(total).saturating_sub(1);
689            match pending.operator {
690                VimOperator::Delete => self.clear_field_range(start, end),
691                VimOperator::Change => {
692                    self.clear_field_range(start, end);
693                    self.core.enter_edit_mode();
694                }
695                VimOperator::Yank => {
696                    let lines = self.selected_fixed_field_values(start, end);
697                    if !lines.is_empty() {
698                        self.core.behavior_state.yank_mut().set_line_register(lines);
699                    }
700                }
701            }
702            return KeyEventOutcome::Consumed(None);
703        }
704
705        let linewise_target = match action {
706            CanvasKeyAction::MoveUp => Some(pending.anchor.0.saturating_sub(total)),
707            CanvasKeyAction::MoveDown => Some(
708                pending
709                    .anchor
710                    .0
711                    .saturating_add(total)
712                    .min(self.fixed_field_count.saturating_sub(1)),
713            ),
714            CanvasKeyAction::MoveFirstLine => Some(0),
715            CanvasKeyAction::MoveLastLine => Some(self.fixed_field_count.saturating_sub(1)),
716            _ => None,
717        };
718
719        if let Some(target) = linewise_target {
720            let start = pending.anchor.0.min(target);
721            let end = pending.anchor.0.max(target);
722            match pending.operator {
723                VimOperator::Delete => self.clear_field_range(start, end),
724                VimOperator::Change => {
725                    self.clear_field_range(start, end);
726                    self.core.enter_edit_mode();
727                }
728                VimOperator::Yank => {
729                    let lines = self.selected_fixed_field_values(start, end);
730                    if !lines.is_empty() {
731                        self.core.behavior_state.yank_mut().set_line_register(lines);
732                    }
733                }
734            }
735            return KeyEventOutcome::Consumed(None);
736        }
737
738        match pending.operator {
739            VimOperator::Delete => {
740                self.delete_to_field_end();
741                KeyEventOutcome::Consumed(None)
742            }
743            VimOperator::Change => {
744                self.delete_to_field_end();
745                self.core.enter_edit_mode();
746                KeyEventOutcome::Consumed(None)
747            }
748            VimOperator::Yank => {
749                let field = self.core.current_field();
750                let line = self.core.data_provider().field_value(field).to_string();
751                self.core
752                    .behavior_state
753                    .yank_mut()
754                    .set_text_register(vec![line]);
755                KeyEventOutcome::Consumed(None)
756            }
757        }
758    }
759}
760
761impl<D: DataProvider> Deref for TextFormState<D> {
762    type Target = EditorCore<D>;
763
764    fn deref(&self) -> &Self::Target {
765        &self.core
766    }
767}
768
769impl<D: DataProvider> DerefMut for TextFormState<D> {
770    fn deref_mut(&mut self) -> &mut Self::Target {
771        self.sync_fixed_rows();
772        &mut self.core
773    }
774}
775
776#[cfg(feature = "keybindings")]
777impl<D: DataProvider> KeybindingProduct for TextFormState<D> {
778    type Provider = D;
779
780    fn core(&self) -> &EditorCore<Self::Provider> {
781        &self.core
782    }
783
784    fn core_mut(&mut self) -> &mut EditorCore<Self::Provider> {
785        &mut self.core
786    }
787
788    fn handle_insert_enter(&mut self) -> KeyEventOutcome {
789        self.move_next_field_count(1);
790        KeyEventOutcome::Consumed(None)
791    }
792
793    fn handle_insert_tab(&mut self) -> KeyEventOutcome {
794        self.move_next_field_count(1);
795        KeyEventOutcome::Consumed(None)
796    }
797
798    fn handle_plain_insert_char(&mut self, ch: char) -> KeyEventOutcome {
799        self.core.enter_edit_mode();
800        if self.core.insert_char(ch).is_ok() {
801            KeyEventOutcome::Consumed(None)
802        } else {
803            KeyEventOutcome::NotMatched
804        }
805    }
806
807    fn dispatch_product_key_action(
808        &mut self,
809        action: &CanvasKeyAction,
810        count: usize,
811    ) -> KeyEventOutcome {
812        let policy = textform_action_policy(action);
813
814        if self.core.behavior_state.vim().has_pending_operator() {
815            return self.apply_operator_motion_vim(action, count);
816        }
817
818        if self.core.keybinding_paradigm() == KeybindingParadigm::Helix {
819            match action {
820                CanvasKeyAction::MoveWordNext => {
821                    self.core
822                        .select_word_motion_helix(count, HelixWordTarget::NextWordStart);
823                    return KeyEventOutcome::Consumed(None);
824                }
825                CanvasKeyAction::MoveWordPrev => {
826                    self.core
827                        .select_word_motion_helix(count, HelixWordTarget::PrevWordStart);
828                    return KeyEventOutcome::Consumed(None);
829                }
830                CanvasKeyAction::MoveWordEnd => {
831                    self.core
832                        .select_word_motion_helix(count, HelixWordTarget::NextWordEnd);
833                    return KeyEventOutcome::Consumed(None);
834                }
835                CanvasKeyAction::MoveWordEndPrev => {
836                    self.core
837                        .select_word_motion_helix(count, HelixWordTarget::PrevWordEnd);
838                    return KeyEventOutcome::Consumed(None);
839                }
840                CanvasKeyAction::MoveBigWordNext => {
841                    self.core
842                        .select_word_motion_helix(count, HelixWordTarget::NextLongWordStart);
843                    return KeyEventOutcome::Consumed(None);
844                }
845                CanvasKeyAction::MoveBigWordPrev => {
846                    self.core
847                        .select_word_motion_helix(count, HelixWordTarget::PrevLongWordStart);
848                    return KeyEventOutcome::Consumed(None);
849                }
850                CanvasKeyAction::MoveBigWordEnd => {
851                    self.core
852                        .select_word_motion_helix(count, HelixWordTarget::NextLongWordEnd);
853                    return KeyEventOutcome::Consumed(None);
854                }
855                CanvasKeyAction::MoveBigWordEndPrev => {
856                    self.core
857                        .select_word_motion_helix(count, HelixWordTarget::PrevLongWordEnd);
858                    return KeyEventOutcome::Consumed(None);
859                }
860                _ => {}
861            }
862        }
863
864        match action {
865            CanvasKeyAction::OperatorDelete => {
866                self.begin_operator_vim(VimOperator::Delete, count);
867                KeyEventOutcome::Consumed(None)
868            }
869            CanvasKeyAction::OperatorChange => {
870                self.begin_operator_vim(VimOperator::Change, count);
871                KeyEventOutcome::Consumed(None)
872            }
873            CanvasKeyAction::OperatorYank => {
874                self.begin_operator_vim(VimOperator::Yank, count);
875                KeyEventOutcome::Consumed(None)
876            }
877            CanvasKeyAction::NextField => {
878                self.move_next_field_count(count);
879                KeyEventOutcome::Consumed(None)
880            }
881            CanvasKeyAction::PrevField => {
882                self.move_prev_field_count(count);
883                KeyEventOutcome::Consumed(None)
884            }
885            CanvasKeyAction::OpenLineBelow => {
886                self.move_next_field_count(count);
887                self.core.enter_edit_mode();
888                KeyEventOutcome::Consumed(None)
889            }
890            CanvasKeyAction::OpenLineAbove => {
891                self.move_prev_field_count(count);
892                self.core.enter_edit_mode();
893                KeyEventOutcome::Consumed(None)
894            }
895            CanvasKeyAction::EnterEditModeLineStart => {
896                self.core.move_line_start();
897                self.core.enter_edit_mode();
898                KeyEventOutcome::Consumed(None)
899            }
900            CanvasKeyAction::EnterEditModeLineEnd => {
901                self.core.move_line_end();
902                self.core.enter_edit_mode();
903                KeyEventOutcome::Consumed(None)
904            }
905            CanvasKeyAction::DeleteLine => {
906                self.clear_current_and_following_fields(count);
907                KeyEventOutcome::Consumed(None)
908            }
909            CanvasKeyAction::DeleteToLineEnd => {
910                self.delete_to_field_end();
911                KeyEventOutcome::Consumed(None)
912            }
913            CanvasKeyAction::ChangeLine => {
914                self.change_current_field();
915                KeyEventOutcome::Consumed(None)
916            }
917            CanvasKeyAction::ChangeToLineEnd => {
918                self.delete_to_field_end();
919                self.core.enter_edit_mode();
920                KeyEventOutcome::Consumed(None)
921            }
922            CanvasKeyAction::YankLine | CanvasKeyAction::CopyLine => {
923                self.yank_current_and_following_fields(count);
924                KeyEventOutcome::Consumed(None)
925            }
926            CanvasKeyAction::CutLine => {
927                self.cut_current_and_following_fields(count);
928                KeyEventOutcome::Consumed(None)
929            }
930            CanvasKeyAction::ExtendLineBelow => {
931                for _ in 0..count {
932                    self.core.extend_line_below_helix();
933                }
934                KeyEventOutcome::Consumed(None)
935            }
936            CanvasKeyAction::ExtendToLineBounds => {
937                for _ in 0..count {
938                    self.core.extend_to_line_bounds_helix();
939                }
940                KeyEventOutcome::Consumed(None)
941            }
942            CanvasKeyAction::CollapseSelection => {
943                self.core.collapse_selection_to_cursor();
944                KeyEventOutcome::Consumed(None)
945            }
946            CanvasKeyAction::DeleteSelection | CanvasKeyAction::DeleteSelectionNoYank => {
947                self.delete_selection_helix(
948                    matches!(action, CanvasKeyAction::DeleteSelection),
949                    count,
950                );
951                KeyEventOutcome::Consumed(None)
952            }
953            CanvasKeyAction::ChangeSelection | CanvasKeyAction::ChangeSelectionNoYank => {
954                self.change_selection_helix(
955                    matches!(action, CanvasKeyAction::ChangeSelection),
956                    count,
957                );
958                KeyEventOutcome::Consumed(None)
959            }
960            CanvasKeyAction::YankSelection => {
961                if self.core.keybinding_paradigm() == KeybindingParadigm::Vim {
962                    // Vim visual `y`: yank the selection, then drop back to
963                    // normal mode (leaving no selection behind).
964                    for _ in 0..count {
965                        self.core.yank_selection_core();
966                    }
967                    self.core.exit_highlight_mode_vim();
968                } else {
969                    // Helix keeps the primary selection alive after yank.
970                    for _ in 0..count {
971                        self.core.yank_primary_selection_helix();
972                    }
973                }
974                KeyEventOutcome::Consumed(None)
975            }
976            CanvasKeyAction::JoinLineBelow
977            | CanvasKeyAction::MoveLineUp
978            | CanvasKeyAction::MoveLineDown
979            | CanvasKeyAction::DuplicateLineUp
980            | CanvasKeyAction::DuplicateLineDown => KeyEventOutcome::Consumed(None),
981            CanvasKeyAction::PasteAfter => {
982                self.paste_register_helix(true, count);
983                KeyEventOutcome::Consumed(None)
984            }
985            CanvasKeyAction::PasteBefore => {
986                self.paste_register_helix(false, count);
987                KeyEventOutcome::Consumed(None)
988            }
989            _ => match policy {
990                TextFormActionPolicy::SharedCore => self.execute_canvas_key_action(action, count),
991                TextFormActionPolicy::StructuralNoOp => KeyEventOutcome::Consumed(None),
992                TextFormActionPolicy::Unsupported => KeyEventOutcome::NotMatched,
993                TextFormActionPolicy::ProductHandled => KeyEventOutcome::Consumed(Some(format!(
994                    "Unhandled textform action: {}",
995                    action.as_str()
996                ))),
997            },
998        }
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    #[cfg(feature = "crossterm")]
1005    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1006
1007    #[cfg(feature = "crossterm")]
1008    use super::TextFormEventOutcome;
1009    use super::TextFormState;
1010    use crate::DataProvider;
1011
1012    #[derive(Default)]
1013    struct TestProvider {
1014        fields: [String; 2],
1015    }
1016
1017    impl DataProvider for TestProvider {
1018        fn field_count(&self) -> usize {
1019            2
1020        }
1021
1022        fn field_name(&self, index: usize) -> &str {
1023            match index {
1024                0 => "first",
1025                1 => "second",
1026                _ => "",
1027            }
1028        }
1029
1030        fn field_value(&self, index: usize) -> &str {
1031            self.fields.get(index).map(String::as_str).unwrap_or("")
1032        }
1033
1034        fn set_field_value(&mut self, index: usize, value: String) {
1035            if let Some(field) = self.fields.get_mut(index) {
1036                *field = value;
1037            }
1038        }
1039    }
1040
1041    #[derive(Default)]
1042    struct VecProvider {
1043        fields: Vec<String>,
1044    }
1045
1046    impl DataProvider for VecProvider {
1047        fn field_count(&self) -> usize {
1048            self.fields.len()
1049        }
1050
1051        fn field_name(&self, index: usize) -> &str {
1052            match index {
1053                0 => "first",
1054                1 => "second",
1055                _ => "",
1056            }
1057        }
1058
1059        fn field_value(&self, index: usize) -> &str {
1060            self.fields.get(index).map(String::as_str).unwrap_or("")
1061        }
1062
1063        fn set_field_value(&mut self, index: usize, value: String) {
1064            if let Some(field) = self.fields.get_mut(index) {
1065                *field = value;
1066            }
1067        }
1068    }
1069
1070    #[derive(Default)]
1071    struct StrictVecProvider {
1072        fields: Vec<String>,
1073    }
1074
1075    impl DataProvider for StrictVecProvider {
1076        fn field_count(&self) -> usize {
1077            self.fields.len()
1078        }
1079
1080        fn field_name(&self, index: usize) -> &str {
1081            match index {
1082                0 => "first",
1083                1 => "second",
1084                _ => "",
1085            }
1086        }
1087
1088        fn field_value(&self, index: usize) -> &str {
1089            &self.fields[index]
1090        }
1091
1092        fn set_field_value(&mut self, index: usize, value: String) {
1093            self.fields[index] = value;
1094        }
1095    }
1096
1097    #[cfg(feature = "keybindings")]
1098    #[derive(Clone)]
1099    struct FixedPolicyProvider {
1100        names: [&'static str; 3],
1101        fields: [String; 3],
1102    }
1103
1104    #[cfg(feature = "keybindings")]
1105    impl FixedPolicyProvider {
1106        fn new() -> Self {
1107            Self {
1108                names: ["first", "second", "third"],
1109                fields: ["alpha beta".into(), "gamma delta".into(), "epsilon".into()],
1110            }
1111        }
1112
1113        fn names(&self) -> Vec<String> {
1114            self.names.iter().map(|name| (*name).to_string()).collect()
1115        }
1116    }
1117
1118    #[cfg(feature = "keybindings")]
1119    impl DataProvider for FixedPolicyProvider {
1120        fn field_count(&self) -> usize {
1121            self.fields.len()
1122        }
1123
1124        fn field_name(&self, index: usize) -> &str {
1125            self.names.get(index).copied().unwrap_or("")
1126        }
1127
1128        fn field_value(&self, index: usize) -> &str {
1129            self.fields.get(index).map(String::as_str).unwrap_or("")
1130        }
1131
1132        fn set_field_value(&mut self, index: usize, value: String) {
1133            if let Some(field) = self.fields.get_mut(index) {
1134                *field = value;
1135            }
1136        }
1137    }
1138
1139    #[cfg(feature = "keybindings")]
1140    fn all_known_key_actions() -> Vec<crate::keybindings::CanvasKeyAction> {
1141        use crate::keybindings::CanvasKeyAction::*;
1142
1143        vec![
1144            MoveLeft,
1145            MoveRight,
1146            MoveUp,
1147            MoveDown,
1148            NextField,
1149            PrevField,
1150            MoveLineStart,
1151            MoveLineEnd,
1152            MoveHalfPageUp,
1153            MoveHalfPageDown,
1154            MoveFirstLine,
1155            MoveLastLine,
1156            MoveWordNext,
1157            MoveWordPrev,
1158            MoveWordEnd,
1159            MoveWordEndPrev,
1160            MoveBigWordNext,
1161            MoveBigWordPrev,
1162            MoveBigWordEnd,
1163            MoveBigWordEndPrev,
1164            DeleteCharBackward,
1165            DeleteCharForward,
1166            Undo,
1167            Redo,
1168            OpenLineBelow,
1169            OpenLineAbove,
1170            EnterEditModeLineStart,
1171            EnterEditModeLineEnd,
1172            DeleteLine,
1173            DeleteToLineEnd,
1174            ChangeLine,
1175            ChangeToLineEnd,
1176            OperatorDelete,
1177            OperatorChange,
1178            OperatorYank,
1179            JoinLineBelow,
1180            YankLine,
1181            PasteAfter,
1182            PasteBefore,
1183            OpenSuggestions,
1184            ApplySuggestion,
1185            EnterDecider,
1186            SuggestionDown,
1187            SuggestionUp,
1188            EnterEditModeBefore,
1189            EnterEditModeAfter,
1190            Exit,
1191            ExitEditMode,
1192            EnterHighlightMode,
1193            EnterHighlightModeLinewise,
1194            ExitHighlightMode,
1195            DeleteSelection,
1196            DeleteSelectionNoYank,
1197            ChangeSelection,
1198            ChangeSelectionNoYank,
1199            YankSelection,
1200            CollapseSelection,
1201            ExtendLineBelow,
1202            ExtendToLineBounds,
1203            SearchNext,
1204            SearchPrev,
1205            SelectAll,
1206            FlipSelections,
1207            SwitchCase,
1208            SwitchToLowercase,
1209            SwitchToUppercase,
1210            TrimSelections,
1211            GotoFirstNonWhitespace,
1212            MovePageUp,
1213            MovePageDown,
1214            SearchSelection,
1215            EnsureSelectionForward,
1216            MatchBrackets,
1217            IndentSelection,
1218            UnindentSelection,
1219            IncrementNumber,
1220            DecrementNumber,
1221            FindNextChar,
1222            FindPrevChar,
1223            TillNextChar,
1224            TillPrevChar,
1225            ReplaceChar,
1226            RepeatLastFind,
1227            RepeatLastFindReverse,
1228            SurroundAdd,
1229            SurroundDelete,
1230            SurroundReplace,
1231            DeleteWordBackward,
1232            DeleteToLineStart,
1233            DeleteWordForward,
1234            ClearSearch,
1235            MoveLineUp,
1236            MoveLineDown,
1237            DuplicateLineUp,
1238            DuplicateLineDown,
1239            CopyLine,
1240            CutLine,
1241            SelectLeft,
1242            SelectRight,
1243            SelectUp,
1244            SelectDown,
1245            SelectWordPrev,
1246            SelectWordNext,
1247            SelectLineStart,
1248            SelectLineEnd,
1249            SelectDocStart,
1250            SelectDocEnd,
1251        ]
1252    }
1253
1254    #[cfg(feature = "crossterm")]
1255    #[test]
1256    fn enter_moves_between_fields_then_submits() {
1257        let mut form = TextFormState::new(TestProvider::default());
1258
1259        let first = form.input(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
1260        assert_eq!(first, TextFormEventOutcome::Handled);
1261        assert_eq!(form.current_field(), 1);
1262
1263        let second = form.input(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
1264        assert_eq!(second, TextFormEventOutcome::Submitted);
1265        assert_eq!(form.current_field(), 1);
1266    }
1267
1268    #[cfg(feature = "keybindings")]
1269    #[test]
1270    fn every_known_key_action_preserves_textform_fixed_slots() {
1271        use crate::editor::product::KeybindingProduct;
1272        use crate::keybindings::{BuiltinCanvasKeybindingPreset, KeyEventOutcome};
1273
1274        for action in all_known_key_actions() {
1275            let provider = FixedPolicyProvider::new();
1276            let expected_names = provider.names();
1277            let mut form = TextFormState::new(provider);
1278            form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1279            form.core
1280                .behavior_state
1281                .yank_mut()
1282                .set_text_register(vec!["paste".to_string()]);
1283
1284            let outcome = form.dispatch_product_key_action(&action, 1);
1285            assert!(
1286                !matches!(
1287                    outcome,
1288                    KeyEventOutcome::Consumed(Some(ref msg))
1289                        if msg.starts_with("Unhandled textform action:")
1290                ),
1291                "action is classified as product-handled but not implemented: {}",
1292                action.as_str()
1293            );
1294
1295            assert_eq!(
1296                form.data_provider().field_count(),
1297                3,
1298                "action changed field count: {}",
1299                action.as_str()
1300            );
1301            assert_eq!(
1302                form.fixed_field_count(),
1303                3,
1304                "action changed fixed count: {}",
1305                action.as_str()
1306            );
1307            let names: Vec<String> = (0..form.data_provider().field_count())
1308                .map(|index| form.data_provider().field_name(index).to_string())
1309                .collect();
1310            assert_eq!(
1311                names,
1312                expected_names,
1313                "action changed field identity/order: {}",
1314                action.as_str()
1315            );
1316        }
1317    }
1318
1319    #[cfg(feature = "keybindings")]
1320    #[test]
1321    fn structural_noop_actions_leave_fixed_field_values_unchanged() {
1322        use crate::editor::product::KeybindingProduct;
1323        use crate::keybindings::CanvasKeyAction;
1324
1325        let actions = [
1326            CanvasKeyAction::JoinLineBelow,
1327            CanvasKeyAction::MoveLineUp,
1328            CanvasKeyAction::MoveLineDown,
1329            CanvasKeyAction::DuplicateLineUp,
1330            CanvasKeyAction::DuplicateLineDown,
1331        ];
1332
1333        for action in actions {
1334            let provider = FixedPolicyProvider::new();
1335            let expected = provider.capture_content();
1336            let mut form = TextFormState::new(provider);
1337
1338            let _ = form.dispatch_product_key_action(&action, 1);
1339
1340            assert_eq!(
1341                form.data_provider().capture_content(),
1342                expected,
1343                "structural no-op changed fixed field values: {}",
1344                action.as_str()
1345            );
1346        }
1347    }
1348
1349    #[test]
1350    fn delete_line_clears_current_field_without_removing_it() {
1351        let mut form = TextFormState::new(TestProvider {
1352            fields: ["abc".to_string(), "def".to_string()],
1353        });
1354        form.clear_current_field();
1355
1356        assert_eq!(form.data_provider().field_count(), 2);
1357        assert_eq!(form.current_text(), "");
1358    }
1359
1360    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1361    #[test]
1362    fn keybinding_enter_moves_to_next_field_without_splitting_rows() {
1363        use crate::keybindings::KeyEventOutcome;
1364
1365        let mut form = TextFormState::new(TestProvider {
1366            fields: ["abc".to_string(), "def".to_string()],
1367        });
1368        form.enter_edit_mode();
1369
1370        let outcome = form.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
1371
1372        assert_eq!(outcome, KeyEventOutcome::Consumed(None));
1373        assert_eq!(form.data_provider().field_count(), 2);
1374        assert_eq!(form.current_field(), 1);
1375        assert_eq!(form.data_provider().field_value(0), "abc");
1376        assert_eq!(form.current_text(), "def");
1377    }
1378
1379    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1380    #[test]
1381    fn vim_esc_exits_select_mode() {
1382        use crate::canvas::modes::AppMode;
1383
1384        let mut form = TextFormState::new(TestProvider {
1385            fields: ["row1".to_string(), "row2".to_string()],
1386        });
1387        form.set_keybindings(crate::keybindings::CanvasKeyBindings::vim_defaults());
1388
1389        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
1390        assert_eq!(form.mode(), AppMode::Sel);
1391
1392        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1393        assert_eq!(form.mode(), AppMode::Nor);
1394    }
1395
1396    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1397    #[test]
1398    fn helix_v_up_delete_collapses_selection() {
1399        use crate::canvas::state::SelectionState;
1400        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1401
1402        let mut form = TextFormState::new(TestProvider {
1403            fields: ["row1".to_string(), "row2".to_string()],
1404        });
1405        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1406
1407        // Onto the second field, enter select, extend UP, delete.
1408        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE));
1409        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
1410        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE));
1411        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1412
1413        // No stale multi-field selection should remain after the delete.
1414        match form.selection_state() {
1415            SelectionState::None => {}
1416            SelectionState::Characterwise { anchor } => {
1417                assert_eq!(*anchor, (form.current_field(), form.cursor_position()));
1418            }
1419            other => panic!("expected collapsed selection, got {other:?}"),
1420        }
1421    }
1422
1423    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1424    #[test]
1425    fn helix_esc_exits_select_but_keeps_selection_in_normal_mode() {
1426        use crate::canvas::modes::AppMode;
1427        use crate::canvas::state::SelectionState;
1428        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1429
1430        let mut form = TextFormState::new(TestProvider {
1431            fields: ["row1".to_string(), "row2".to_string()],
1432        });
1433        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1434
1435        // `v` enters select (highlight) mode; Esc leaves it back to Normal.
1436        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
1437        assert_eq!(form.mode(), AppMode::Sel, "v should enter select");
1438        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1439        assert_eq!(form.mode(), AppMode::Nor, "esc from v-select");
1440
1441        // `x` leaves a linewise selection but stays in Normal mode. Esc must NOT
1442        // collapse it — in Helix the selection persists; `;` collapses it.
1443        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1444        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1445        assert!(
1446            matches!(form.selection_state(), SelectionState::Linewise { .. }),
1447            "esc in Nor must keep the x-selection, got {:?}",
1448            form.selection_state()
1449        );
1450    }
1451
1452    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1453    #[test]
1454    fn vim_dd_clears_current_field_without_shifting_following_fields() {
1455        let mut form = TextFormState::new(TestProvider {
1456            fields: ["row1".to_string(), "row2".to_string()],
1457        });
1458        form.set_keybindings(crate::keybindings::CanvasKeyBindings::vim_defaults());
1459
1460        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1461        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1462
1463        assert_eq!(form.fixed_field_count(), 2);
1464        assert_eq!(form.data_provider().field_count(), 2);
1465        assert_eq!(form.data_provider().field_value(0), "");
1466        assert_eq!(form.data_provider().field_value(1), "row2");
1467        assert_eq!(form.current_field(), 0);
1468    }
1469
1470    #[cfg(feature = "keybindings")]
1471    #[test]
1472    fn cut_line_clears_fixed_slot_without_shifting_later_fields() {
1473        use crate::editor::product::KeybindingProduct;
1474        use crate::keybindings::CanvasKeyAction;
1475
1476        let mut form = TextFormState::new(VecProvider {
1477            fields: vec!["row1".to_string(), "row2".to_string(), "row3".to_string()],
1478        });
1479        let _ = form.transition_to_field(1);
1480
1481        let _ = form.dispatch_product_key_action(&CanvasKeyAction::CutLine, 1);
1482
1483        assert_eq!(form.fixed_field_count(), 3);
1484        assert_eq!(
1485            form.data_provider().capture_content(),
1486            vec!["row1", "", "row3"]
1487        );
1488        assert_eq!(form.current_field(), 1);
1489    }
1490
1491    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1492    #[test]
1493    fn vim_counted_dd_clears_fixed_slots_without_shifting_later_fields() {
1494        let mut form = TextFormState::new(VecProvider {
1495            fields: vec!["row1".to_string(), "row2".to_string(), "row3".to_string()],
1496        });
1497        form.set_keybindings(crate::keybindings::CanvasKeyBindings::vim_defaults());
1498
1499        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
1500        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1501        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1502
1503        assert_eq!(form.fixed_field_count(), 3);
1504        assert_eq!(form.data_provider().capture_content(), vec!["", "", "row3"]);
1505        assert_eq!(form.current_field(), 0);
1506    }
1507
1508    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1509    #[test]
1510    fn vim_visual_yank_copies_selection_and_exits_to_normal() {
1511        use super::YankRegister;
1512        use crate::canvas::modes::AppMode;
1513
1514        let mut form = TextFormState::new(VecProvider {
1515            fields: vec!["hello world".to_string(), "second".to_string()],
1516        });
1517        form.set_keybindings(crate::keybindings::CanvasKeyBindings::vim_defaults());
1518
1519        // Enter visual mode and extend the selection over "hel".
1520        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
1521        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE));
1522        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE));
1523        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
1524
1525        // The selection (not the whole field) lands in the register...
1526        assert_eq!(
1527            form.core.behavior_state.yank().register().cloned(),
1528            Some(YankRegister::Text(vec!["hel".to_string()]))
1529        );
1530        // ...and visual mode is left behind.
1531        assert_eq!(form.mode(), AppMode::Nor);
1532    }
1533
1534    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1535    #[test]
1536    fn vim_join_line_below_does_not_merge_or_remove_fixed_fields() {
1537        let mut form = TextFormState::new(TestProvider {
1538            fields: ["row1".to_string(), "row2".to_string()],
1539        });
1540        form.set_keybindings(crate::keybindings::CanvasKeyBindings::vim_defaults());
1541
1542        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('J'), KeyModifiers::SHIFT));
1543
1544        assert_eq!(form.fixed_field_count(), 2);
1545        assert_eq!(form.data_provider().field_value(0), "row1");
1546        assert_eq!(form.data_provider().field_value(1), "row2");
1547        assert_eq!(form.current_field(), 0);
1548    }
1549
1550    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1551    #[test]
1552    fn helix_x_then_d_clears_selected_fixed_slot_without_shifting_following_fields() {
1553        let mut form = TextFormState::new(TestProvider {
1554            fields: ["row1".to_string(), "row2".to_string()],
1555        });
1556        form.use_keybinding_preset(crate::keybindings::BuiltinCanvasKeybindingPreset::Helix);
1557
1558        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1559        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1560
1561        assert_eq!(form.fixed_field_count(), 2);
1562        assert_eq!(form.data_provider().field_count(), 2);
1563        assert_eq!(form.data_provider().field_value(0), "");
1564        assert_eq!(form.data_provider().field_value(1), "row2");
1565        assert_eq!(form.current_field(), 0);
1566    }
1567
1568    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1569    #[test]
1570    fn helix_extended_line_delete_clears_fixed_slots_without_shifting_later_fields() {
1571        let mut form = TextFormState::new(VecProvider {
1572            fields: vec!["row1".to_string(), "row2".to_string(), "row3".to_string()],
1573        });
1574        form.use_keybinding_preset(crate::keybindings::BuiltinCanvasKeybindingPreset::Helix);
1575
1576        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1577        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1578        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1579
1580        assert_eq!(form.fixed_field_count(), 3);
1581        assert_eq!(form.data_provider().capture_content(), vec!["", "", "row3"]);
1582        assert_eq!(form.current_field(), 0);
1583    }
1584
1585    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1586    #[test]
1587    fn helix_word_motion_sets_characterwise_selection_for_highlight() {
1588        use crate::canvas::state::SelectionState;
1589        use crate::keybindings::{BuiltinCanvasKeybindingPreset, KeyEventOutcome};
1590
1591        let mut form = TextFormState::new(TestProvider {
1592            fields: ["one two three".to_string(), "row2".to_string()],
1593        });
1594        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1595
1596        let outcome = form.handle_key_event(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
1597
1598        assert!(matches!(outcome, KeyEventOutcome::Consumed(None)));
1599        assert_eq!(form.cursor_position(), 3);
1600        assert!(matches!(
1601            form.selection_state(),
1602            SelectionState::Characterwise { anchor: (0, 0) }
1603        ));
1604        assert_eq!(form.data_provider().field_value(1), "row2");
1605    }
1606
1607    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1608    #[test]
1609    fn helix_word_then_delete_removes_selection_without_clearing_field() {
1610        use crate::keybindings::{BuiltinCanvasKeybindingPreset, KeyEventOutcome};
1611
1612        let mut form = TextFormState::new(TestProvider {
1613            fields: ["one two three".to_string(), "row2".to_string()],
1614        });
1615        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1616
1617        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
1618        let outcome = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1619
1620        assert!(matches!(outcome, KeyEventOutcome::Consumed(None)));
1621        assert_eq!(form.fixed_field_count(), 2);
1622        assert_eq!(form.data_provider().field_value(0), "two three");
1623        assert_eq!(form.data_provider().field_value(1), "row2");
1624        assert_eq!(form.current_field(), 0);
1625        assert_eq!(form.cursor_position(), 0);
1626    }
1627
1628    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1629    #[test]
1630    fn helix_cross_field_character_delete_does_not_merge_fixed_fields() {
1631        use crate::canvas::state::SelectionState;
1632        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1633
1634        let mut form = TextFormState::new(TestProvider {
1635            fields: ["abc".to_string(), "def".to_string()],
1636        });
1637        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1638        let _ = form.transition_to_field(1);
1639        form.set_cursor_position(1);
1640        form.core.ui_state.selection = SelectionState::Characterwise { anchor: (0, 1) };
1641
1642        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1643
1644        assert_eq!(form.fixed_field_count(), 2);
1645        assert_eq!(form.data_provider().field_value(0), "a");
1646        assert_eq!(form.data_provider().field_value(1), "f");
1647        assert_eq!(form.current_field(), 0);
1648        assert_eq!(form.cursor_position(), 0);
1649    }
1650
1651    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1652    #[test]
1653    fn helix_characterwise_yank_then_paste_inserts_inside_field() {
1654        use crate::keybindings::{BuiltinCanvasKeybindingPreset, KeyEventOutcome};
1655
1656        let mut form = TextFormState::new(TestProvider {
1657            fields: ["one two".to_string(), "row2".to_string()],
1658        });
1659        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1660
1661        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
1662        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
1663        let outcome = form.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));
1664
1665        assert!(matches!(outcome, KeyEventOutcome::Consumed(None)));
1666        assert_eq!(form.fixed_field_count(), 2);
1667        assert_eq!(form.data_provider().field_value(0), "one one two");
1668        assert_eq!(form.data_provider().field_value(1), "row2");
1669    }
1670
1671    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1672    #[test]
1673    fn helix_linewise_yank_then_paste_writes_fixed_slots_without_shifting() {
1674        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1675
1676        let mut form = TextFormState::new(VecProvider {
1677            fields: vec!["row1".to_string(), "row2".to_string(), "row3".to_string()],
1678        });
1679        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1680
1681        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1682        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
1683        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));
1684
1685        assert_eq!(form.fixed_field_count(), 3);
1686        assert_eq!(
1687            form.data_provider().capture_content(),
1688            vec!["row1", "row1", "row3"]
1689        );
1690    }
1691
1692    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1693    #[test]
1694    fn helix_extend_line_stays_in_normal_mode_like_textarea() {
1695        use crate::canvas::modes::AppMode;
1696        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1697
1698        let mut form = TextFormState::new(TestProvider {
1699            fields: ["row1".to_string(), "row2".to_string()],
1700        });
1701        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1702
1703        // Helix `x` selects the whole line but stays in normal mode (there is no
1704        // separate visual mode for it) — exactly like the text area.
1705        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1706        assert_eq!(form.mode(), AppMode::Nor);
1707    }
1708
1709    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1710    #[test]
1711    fn helix_delete_after_extend_returns_to_normal_mode() {
1712        use crate::canvas::modes::AppMode;
1713        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1714
1715        let mut form = TextFormState::new(TestProvider {
1716            fields: ["row1".to_string(), "row2".to_string()],
1717        });
1718        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1719
1720        // `x` then `d`: the slot clears and we end in normal mode (no stuck SEL).
1721        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
1722        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
1723        assert_eq!(form.mode(), AppMode::Nor);
1724        assert_eq!(form.data_provider().field_value(0), "");
1725        assert_eq!(form.data_provider().field_value(1), "row2");
1726    }
1727
1728    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1729    #[test]
1730    fn helix_highlight_yank_returns_to_normal_mode() {
1731        use crate::canvas::modes::AppMode;
1732        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1733
1734        let mut form = TextFormState::new(TestProvider {
1735            fields: ["row1".to_string(), "row2".to_string()],
1736        });
1737        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1738
1739        // `v` enters the genuine highlight (select) mode; `y` yanks and must
1740        // leave it, matching the text area.
1741        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
1742        assert_eq!(form.mode(), AppMode::Sel);
1743
1744        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
1745        assert_eq!(form.mode(), AppMode::Nor);
1746    }
1747
1748    #[cfg(all(feature = "keybindings", feature = "crossterm"))]
1749    #[test]
1750    fn helix_append_on_last_character_inserts_after_it() {
1751        use crate::canvas::modes::AppMode;
1752        use crate::keybindings::BuiltinCanvasKeybindingPreset;
1753
1754        let mut form = TextFormState::new(TestProvider {
1755            fields: ["abc".to_string(), "row2".to_string()],
1756        });
1757        form.use_keybinding_preset(BuiltinCanvasKeybindingPreset::Helix);
1758
1759        // Selection on the last character of the field; `a` must position the
1760        // cursor *after* it, not clamp back onto it.
1761        form.move_line_end();
1762        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
1763        assert_eq!(form.mode(), AppMode::Ins);
1764        assert_eq!(form.cursor_position(), 3);
1765
1766        let _ = form.handle_key_event(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
1767        assert_eq!(form.current_text(), "abcX");
1768    }
1769
1770    #[cfg(feature = "keybindings")]
1771    #[test]
1772    fn multiline_text_register_paste_preserves_suffix_in_fixed_slot() {
1773        use crate::editor::product::KeybindingProduct;
1774        use crate::keybindings::CanvasKeyAction;
1775
1776        let mut form = TextFormState::new(VecProvider {
1777            fields: vec!["aaZZ".to_string(), "row2".to_string(), "row3".to_string()],
1778        });
1779        form.set_cursor_position(2);
1780        form.core
1781            .behavior_state
1782            .yank_mut()
1783            .set_text_register(vec!["X".to_string(), "Y".to_string()]);
1784
1785        let _ = form.dispatch_product_key_action(&CanvasKeyAction::PasteBefore, 1);
1786
1787        assert_eq!(form.fixed_field_count(), 3);
1788        assert_eq!(
1789            form.data_provider().capture_content(),
1790            vec!["aaX", "YZZ", "row3"]
1791        );
1792    }
1793
1794    #[test]
1795    fn guard_resyncs_field_count_changes_before_textform_mutation() {
1796        let mut form = TextFormState::new(VecProvider {
1797            fields: vec!["one".to_string(), "two".to_string()],
1798        });
1799        form.core.data_provider_mut().fields.pop();
1800
1801        let _ = form.paste("x");
1802
1803        assert_eq!(form.fixed_field_count(), 1);
1804        assert_eq!(form.data_provider().capture_content(), vec!["xone"]);
1805    }
1806
1807    #[test]
1808    fn deref_mut_resyncs_field_count_changes_before_core_method() {
1809        let mut form = TextFormState::new(StrictVecProvider {
1810            fields: vec!["one".to_string(), "two".to_string()],
1811        });
1812        let _ = form.transition_to_field(1);
1813        form.core.data_provider_mut().fields.pop();
1814
1815        let moved = form.move_down();
1816
1817        assert!(!moved);
1818        assert_eq!(form.fixed_field_count(), 1);
1819        assert_eq!(form.current_field(), 0);
1820    }
1821
1822    #[cfg(feature = "validation")]
1823    #[test]
1824    fn transition_clamps_stale_previous_field_before_validation() {
1825        let mut form = TextFormState::new(StrictVecProvider {
1826            fields: vec!["one".to_string(), "two".to_string()],
1827        });
1828        let _ = form.transition_to_field(1);
1829        form.core.data_provider_mut().fields.pop();
1830
1831        assert!(form.core.transition_to_field(0).is_ok());
1832        assert_eq!(form.core.current_field(), 0);
1833    }
1834}