Skip to main content

iced_code_editor/canvas_editor/
vim.rs

1/// The active editing mode when Vim behavior is enabled.
2#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
3pub enum VimMode {
4    /// Command-oriented navigation and editing mode.
5    #[default]
6    Normal,
7    /// Text insertion mode.
8    Insert,
9    /// Character-wise visual selection mode.
10    Visual,
11    /// Line-wise visual selection mode.
12    VisualLine,
13}
14
15/// A cursor motion recognized by the Vim parser.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub(crate) enum VimMotion {
18    /// Move left one character (`h`).
19    Left,
20    /// Move down one visible line (`j`).
21    Down,
22    /// Move up one visible line (`k`).
23    Up,
24    /// Move right one character (`l`).
25    Right,
26    /// Move to the start of the next word (`w`).
27    WordForward,
28    /// Move to the start of the previous word (`b`).
29    WordBackward,
30    /// Move to the end of the current/next word (`e`).
31    WordEnd,
32    /// Move to column 0 of the current line (`0`).
33    LineStart,
34    /// Move to the first non-blank character of the line (`^`).
35    FirstNonBlank,
36    /// Move to the last character of the line (`$`).
37    LineEnd,
38    /// Move to line `count` (1-based), or line 1 if no count is given (`gg`).
39    DocumentStart,
40    /// Move to line `count` (1-based) if a count is explicitly given,
41    /// otherwise the last line (`G`).
42    DocumentEnd,
43}
44
45/// An operator waiting for, or combined with, a motion.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub(crate) enum VimOperator {
48    /// Delete the targeted range (`d`).
49    Delete,
50    /// Delete the targeted range and enter Insert mode (`c`).
51    Change,
52    /// Yank (copy) the targeted range into the unnamed register (`y`).
53    Yank,
54}
55
56/// The insertion position requested by a Normal-mode command.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum VimInsertPosition {
59    /// Enter Insert mode before the cursor (`i`).
60    BeforeCursor,
61    /// Enter Insert mode after the cursor (`a`).
62    AfterCursor,
63    /// Enter Insert mode at the first non-blank character of the line (`I`).
64    FirstNonBlank,
65    /// Enter Insert mode at the end of the line (`A`).
66    EndOfLine,
67    /// Open a new line below the current one and enter Insert mode (`o`).
68    NewLineBelow,
69    /// Open a new line above the current one and enter Insert mode (`O`).
70    NewLineAbove,
71}
72
73/// The side of the cursor on which a paste should occur.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub(crate) enum VimPastePosition {
76    /// Paste after the cursor (`p`).
77    AfterCursor,
78    /// Paste before the cursor (`P`).
79    BeforeCursor,
80}
81
82/// A complete, buffer-independent intent emitted by [`VimState`].
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub(crate) enum VimAction {
85    /// Switch to a different Vim mode.
86    Mode(VimMode),
87    /// Move the cursor by a motion, repeated `count` times. `explicit_count`
88    /// distinguishes an explicitly typed count (e.g. `1G`) from the default
89    /// of 1 (e.g. bare `G`), since some motions (`G`) behave differently in
90    /// each case.
91    Motion { motion: VimMotion, count: usize, explicit_count: bool },
92    /// Enter Insert mode at the given position, repeating the eventual
93    /// inserted text `count` times on exit.
94    Insert { position: VimInsertPosition, count: usize },
95    /// Apply an operator to the range covered by a motion, repeated `count`
96    /// times. See [`VimAction::Motion`] for the meaning of `explicit_count`.
97    Operator {
98        operator: VimOperator,
99        motion: VimMotion,
100        count: usize,
101        explicit_count: bool,
102    },
103    /// Apply an operator to `count` whole lines (e.g. `dd`, `yy`, `cc`).
104    LineOperator { operator: VimOperator, count: usize },
105    /// Apply an operator to the active Visual/Visual Line selection.
106    VisualOperator(VimOperator),
107    /// Delete `count` characters under and after the cursor (`x`).
108    DeleteCharacters { count: usize },
109    /// Paste the unnamed register `count` times at the given position.
110    Paste { position: VimPastePosition, count: usize },
111    /// Undo the last `count` grouped commands (`u`).
112    Undo { count: usize },
113    /// Redo the last `count` undone commands (`Ctrl+R`).
114    Redo { count: usize },
115    /// Repeat the last `/` search, optionally reversing its direction
116    /// (`n`/`N`).
117    RepeatSearch { reverse: bool },
118    /// The pending `/` or `:` command-line input changed.
119    CommandLineChanged,
120    /// A `/` search was submitted with the given pattern.
121    SubmitSearch(String),
122    /// A `:N` command was submitted, requesting a jump to the given
123    /// 1-based line.
124    SubmitGotoLine(usize),
125    /// A `:w` or `:wq` command was submitted, requesting a host save and
126    /// optionally exiting Vim mode.
127    WriteFile { exit_vim: bool },
128    /// A `:q` command was submitted, requesting Vim mode be turned off.
129    ExitVimMode,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133enum VimCommandLineKind {
134    Search,
135    Command,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139struct VimCommandLine {
140    kind: VimCommandLineKind,
141    input: String,
142}
143
144/// Whether register text represents a character range or complete lines.
145#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
146pub(crate) enum VimRegisterKind {
147    #[default]
148    Characterwise,
149    Linewise,
150}
151
152/// The per-editor unnamed Vim register.
153#[derive(Debug, Default, Clone, PartialEq, Eq)]
154pub(crate) struct VimRegister {
155    pub(crate) text: String,
156    pub(crate) kind: VimRegisterKind,
157}
158
159/// Pure Vim parsing state owned by one editor instance.
160#[derive(Debug, Default)]
161pub(crate) struct VimState {
162    mode: VimMode,
163    count: Option<usize>,
164    g_prefix: bool,
165    pending_operator: Option<VimOperator>,
166    pending_operator_count: usize,
167    visual_anchor: Option<(usize, usize)>,
168    visual_active: Option<(usize, usize)>,
169    command_line: Option<VimCommandLine>,
170    last_search: Option<String>,
171    pub(crate) register: VimRegister,
172}
173
174impl VimState {
175    pub(crate) fn mode(&self) -> VimMode {
176        self.mode
177    }
178
179    pub(crate) fn reset(&mut self) {
180        *self = Self::default();
181    }
182
183    pub(crate) fn enter_clean_normal_mode(&mut self) {
184        self.mode = VimMode::Normal;
185        self.clear_visual();
186        self.command_line = None;
187        self.clear_pending();
188    }
189
190    pub(crate) fn command_line_active(&self) -> bool {
191        self.command_line.is_some()
192    }
193
194    pub(crate) fn last_search(&self) -> Option<&str> {
195        self.last_search.as_deref()
196    }
197
198    pub(crate) fn command_line_text(&self) -> Option<String> {
199        self.command_line.as_ref().map(|command_line| {
200            let prefix = match command_line.kind {
201                VimCommandLineKind::Search => '/',
202                VimCommandLineKind::Command => ':',
203            };
204            format!("{prefix}{}", command_line.input)
205        })
206    }
207
208    pub(crate) fn pending_keys(&self) -> String {
209        let mut pending = String::new();
210        if let Some(operator) = self.pending_operator {
211            if self.pending_operator_count > 1 {
212                pending.push_str(&self.pending_operator_count.to_string());
213            }
214            pending.push(match operator {
215                VimOperator::Delete => 'd',
216                VimOperator::Change => 'c',
217                VimOperator::Yank => 'y',
218            });
219            if let Some(count) = self.count {
220                pending.push_str(&count.to_string());
221            }
222        } else if let Some(count) = self.count {
223            pending.push_str(&count.to_string());
224        }
225        if self.g_prefix {
226            pending.push('g');
227        }
228        pending
229    }
230
231    pub(crate) fn status_line_text(&self) -> (String, String) {
232        if let Some(command_line) = self.command_line_text() {
233            return (command_line, String::new());
234        }
235
236        let mode = match self.mode {
237            VimMode::Normal => "NORMAL",
238            VimMode::Insert => "INSERT",
239            VimMode::Visual => "VISUAL",
240            VimMode::VisualLine => "VISUAL LINE",
241        };
242
243        (format!("-- {mode} --"), self.pending_keys())
244    }
245
246    pub(crate) fn begin_visual(&mut self, position: (usize, usize)) {
247        self.visual_anchor = Some(position);
248        self.visual_active = Some(position);
249    }
250
251    pub(crate) fn visual_positions(
252        &self,
253    ) -> Option<((usize, usize), (usize, usize))> {
254        Some((self.visual_anchor?, self.visual_active?))
255    }
256
257    pub(crate) fn set_visual_active(&mut self, position: (usize, usize)) {
258        self.visual_active = Some(position);
259    }
260
261    pub(crate) fn clear_visual(&mut self) {
262        self.visual_anchor = None;
263        self.visual_active = None;
264    }
265
266    pub(crate) fn set_mode_from_mouse(&mut self, mode: VimMode) {
267        self.mode = mode;
268        self.clear_pending();
269    }
270
271    pub(crate) fn enter_insert_mode(&mut self) {
272        self.mode = VimMode::Insert;
273        self.clear_visual();
274        self.command_line = None;
275        self.clear_pending();
276    }
277
278    pub(crate) fn parse_key(&mut self, key: char) -> Option<VimAction> {
279        if self.command_line.is_some() {
280            return self.parse_command_line_key(key);
281        }
282
283        if self.mode == VimMode::Insert {
284            return if key == '\u{1b}' {
285                Some(self.set_mode(VimMode::Normal))
286            } else {
287                None
288            };
289        }
290
291        if key == '\u{1b}' {
292            return Some(self.set_mode(VimMode::Normal));
293        }
294
295        if key.is_ascii_digit() && (key != '0' || self.count.is_some()) {
296            self.push_count_digit(key);
297            return None;
298        }
299
300        if self.g_prefix {
301            if key == 'g' {
302                self.g_prefix = false;
303                return Some(self.finish_motion(VimMotion::DocumentStart));
304            }
305            self.clear_pending();
306            return None;
307        }
308
309        if key == 'g' {
310            self.g_prefix = true;
311            return None;
312        }
313
314        if let Some(operator) = operator_for_key(key) {
315            return self.parse_operator(operator);
316        }
317
318        if let Some(motion) = motion_for_key(key) {
319            return Some(self.finish_motion(motion));
320        }
321
322        let action = match key {
323            'i' => Some(self.insert(VimInsertPosition::BeforeCursor)),
324            'a' => Some(self.insert(VimInsertPosition::AfterCursor)),
325            'I' => Some(self.insert(VimInsertPosition::FirstNonBlank)),
326            'A' => Some(self.insert(VimInsertPosition::EndOfLine)),
327            'o' => Some(self.insert(VimInsertPosition::NewLineBelow)),
328            'O' => Some(self.insert(VimInsertPosition::NewLineAbove)),
329            'v' => Some(self.set_mode(VimMode::Visual)),
330            'V' => Some(self.set_mode(VimMode::VisualLine)),
331            'x' => {
332                Some(VimAction::DeleteCharacters { count: self.take_count() })
333            }
334            'p' => Some(VimAction::Paste {
335                position: VimPastePosition::AfterCursor,
336                count: self.take_count(),
337            }),
338            'P' => Some(VimAction::Paste {
339                position: VimPastePosition::BeforeCursor,
340                count: self.take_count(),
341            }),
342            'u' => Some(VimAction::Undo { count: self.take_count() }),
343            '\u{12}' => Some(VimAction::Redo { count: self.take_count() }),
344            'n' => Some(VimAction::RepeatSearch { reverse: false }),
345            'N' => Some(VimAction::RepeatSearch { reverse: true }),
346            '/' => Some(self.open_command_line(VimCommandLineKind::Search)),
347            ':' => Some(self.open_command_line(VimCommandLineKind::Command)),
348            _ => None,
349        };
350
351        if action.is_none() {
352            self.clear_pending();
353        }
354        action
355    }
356
357    fn open_command_line(&mut self, kind: VimCommandLineKind) -> VimAction {
358        self.clear_pending();
359        self.command_line = Some(VimCommandLine { kind, input: String::new() });
360        VimAction::CommandLineChanged
361    }
362
363    fn parse_command_line_key(&mut self, key: char) -> Option<VimAction> {
364        match key {
365            '\u{1b}' => {
366                self.command_line = None;
367                Some(VimAction::CommandLineChanged)
368            }
369            '\u{8}' => {
370                if let Some(command_line) = self.command_line.as_mut()
371                    && command_line.input.pop().is_none()
372                {
373                    self.command_line = None;
374                }
375                Some(VimAction::CommandLineChanged)
376            }
377            '\n' | '\r' => {
378                let command_line = self.command_line.take()?;
379                if command_line.input.is_empty() {
380                    return Some(VimAction::CommandLineChanged);
381                }
382                match command_line.kind {
383                    VimCommandLineKind::Search => {
384                        self.last_search = Some(command_line.input.clone());
385                        Some(VimAction::SubmitSearch(command_line.input))
386                    }
387                    VimCommandLineKind::Command => {
388                        match command_line.input.as_str() {
389                            "q" => Some(VimAction::ExitVimMode),
390                            "w" => {
391                                Some(VimAction::WriteFile { exit_vim: false })
392                            }
393                            "wq" => {
394                                Some(VimAction::WriteFile { exit_vim: true })
395                            }
396                            _ => command_line
397                                .input
398                                .parse::<usize>()
399                                .ok()
400                                .filter(|line| *line > 0)
401                                .map(VimAction::SubmitGotoLine)
402                                .or(Some(VimAction::CommandLineChanged)),
403                        }
404                    }
405                }
406            }
407            key if !key.is_control() => {
408                if let Some(command_line) = self.command_line.as_mut() {
409                    command_line.input.push(key);
410                }
411                Some(VimAction::CommandLineChanged)
412            }
413            _ => None,
414        }
415    }
416
417    fn set_mode(&mut self, mode: VimMode) -> VimAction {
418        self.mode = mode;
419        self.clear_pending();
420        VimAction::Mode(mode)
421    }
422
423    fn insert(&mut self, position: VimInsertPosition) -> VimAction {
424        let count = self.take_count();
425        self.mode = VimMode::Insert;
426        self.clear_pending();
427        VimAction::Insert { position, count }
428    }
429
430    fn parse_operator(&mut self, operator: VimOperator) -> Option<VimAction> {
431        if self.mode != VimMode::Normal {
432            self.clear_pending();
433            return Some(VimAction::VisualOperator(operator));
434        }
435
436        if let Some(pending) = self.pending_operator {
437            if pending == operator {
438                let count = self
439                    .pending_operator_count
440                    .saturating_mul(self.take_count());
441                self.clear_pending();
442                return Some(VimAction::LineOperator { operator, count });
443            }
444            self.clear_pending();
445            return None;
446        }
447
448        self.pending_operator_count = self.take_count();
449        self.pending_operator = Some(operator);
450        None
451    }
452
453    fn finish_motion(&mut self, motion: VimMotion) -> VimAction {
454        let motion_count_explicit = self.count.is_some();
455        let motion_count = self.take_count();
456        if let Some(operator) = self.pending_operator {
457            let count =
458                self.pending_operator_count.saturating_mul(motion_count);
459            // The operator's own count slot doesn't track whether it was
460            // explicitly typed, so `1dG` vs `dG` stays ambiguous; this
461            // approximates "explicit" for the combined count.
462            let explicit_count =
463                motion_count_explicit || self.pending_operator_count > 1;
464            self.clear_pending();
465            VimAction::Operator { operator, motion, count, explicit_count }
466        } else {
467            self.g_prefix = false;
468            VimAction::Motion {
469                motion,
470                count: motion_count,
471                explicit_count: motion_count_explicit,
472            }
473        }
474    }
475
476    fn push_count_digit(&mut self, key: char) {
477        let digit = key.to_digit(10).unwrap_or_default() as usize;
478        self.count = Some(
479            self.count
480                .unwrap_or_default()
481                .saturating_mul(10)
482                .saturating_add(digit),
483        );
484    }
485
486    fn take_count(&mut self) -> usize {
487        self.count.take().unwrap_or(1).max(1)
488    }
489
490    fn clear_pending(&mut self) {
491        self.count = None;
492        self.g_prefix = false;
493        self.pending_operator = None;
494        self.pending_operator_count = 1;
495    }
496}
497
498fn motion_for_key(key: char) -> Option<VimMotion> {
499    match key {
500        'h' => Some(VimMotion::Left),
501        'j' => Some(VimMotion::Down),
502        'k' => Some(VimMotion::Up),
503        'l' => Some(VimMotion::Right),
504        'w' => Some(VimMotion::WordForward),
505        'b' => Some(VimMotion::WordBackward),
506        'e' => Some(VimMotion::WordEnd),
507        '0' => Some(VimMotion::LineStart),
508        '^' => Some(VimMotion::FirstNonBlank),
509        '$' => Some(VimMotion::LineEnd),
510        'G' => Some(VimMotion::DocumentEnd),
511        _ => None,
512    }
513}
514
515fn operator_for_key(key: char) -> Option<VimOperator> {
516    match key {
517        'd' => Some(VimOperator::Delete),
518        'c' => Some(VimOperator::Change),
519        'y' => Some(VimOperator::Yank),
520        _ => None,
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::{
527        VimAction, VimMotion, VimOperator, VimRegister, VimRegisterKind,
528        VimState,
529    };
530
531    #[test]
532    fn vim_parser_accumulates_count_and_operator() {
533        let mut state = VimState::default();
534
535        assert_eq!(state.parse_key('2'), None);
536        assert_eq!(state.parse_key('d'), None);
537        assert_eq!(state.parse_key('3'), None);
538        assert_eq!(
539            state.parse_key('w'),
540            Some(VimAction::Operator {
541                operator: VimOperator::Delete,
542                motion: VimMotion::WordForward,
543                count: 6,
544                explicit_count: true,
545            })
546        );
547    }
548
549    #[test]
550    fn vim_parser_zero_is_motion_without_leading_count() {
551        let mut state = VimState::default();
552
553        assert_eq!(
554            state.parse_key('0'),
555            Some(VimAction::Motion {
556                motion: VimMotion::LineStart,
557                count: 1,
558                explicit_count: false,
559            })
560        );
561
562        assert_eq!(state.parse_key('1'), None);
563        assert_eq!(state.parse_key('0'), None);
564        assert_eq!(
565            state.parse_key('l'),
566            Some(VimAction::Motion {
567                motion: VimMotion::Right,
568                count: 10,
569                explicit_count: true,
570            })
571        );
572    }
573
574    #[test]
575    fn vim_parser_distinguishes_explicit_count_for_document_end() {
576        let mut state = VimState::default();
577
578        assert_eq!(
579            state.parse_key('G'),
580            Some(VimAction::Motion {
581                motion: VimMotion::DocumentEnd,
582                count: 1,
583                explicit_count: false,
584            })
585        );
586
587        assert_eq!(state.parse_key('1'), None);
588        assert_eq!(
589            state.parse_key('G'),
590            Some(VimAction::Motion {
591                motion: VimMotion::DocumentEnd,
592                count: 1,
593                explicit_count: true,
594            })
595        );
596    }
597
598    #[test]
599    fn vim_state_resets_typed_unnamed_register() {
600        let mut state = VimState {
601            register: VimRegister {
602                text: "line\n".to_owned(),
603                kind: VimRegisterKind::Linewise,
604            },
605            ..VimState::default()
606        };
607
608        assert_eq!(state.register.text, "line\n");
609        assert_eq!(state.register.kind, VimRegisterKind::Linewise);
610
611        state.reset();
612
613        assert!(state.register.text.is_empty());
614        assert_eq!(state.register.kind, VimRegisterKind::Characterwise);
615    }
616
617    #[test]
618    fn vim_command_line_parser_accepts_edit_submit_and_cancel() {
619        let mut state = VimState::default();
620
621        assert_eq!(state.parse_key('/'), Some(VimAction::CommandLineChanged));
622        assert_eq!(state.command_line_text().as_deref(), Some("/"));
623
624        for key in "foo".chars() {
625            assert_eq!(
626                state.parse_key(key),
627                Some(VimAction::CommandLineChanged)
628            );
629        }
630        assert_eq!(state.command_line_text().as_deref(), Some("/foo"));
631
632        assert_eq!(
633            state.parse_key('\u{8}'),
634            Some(VimAction::CommandLineChanged)
635        );
636        assert_eq!(state.command_line_text().as_deref(), Some("/fo"));
637        assert_eq!(
638            state.parse_key('\n'),
639            Some(VimAction::SubmitSearch("fo".to_owned()))
640        );
641        assert_eq!(state.command_line_text(), None);
642
643        assert_eq!(state.parse_key(':'), Some(VimAction::CommandLineChanged));
644        for key in "12".chars() {
645            let _ = state.parse_key(key);
646        }
647        assert_eq!(state.parse_key('\n'), Some(VimAction::SubmitGotoLine(12)));
648
649        let _ = state.parse_key(':');
650        let _ = state.parse_key('q');
651        assert_eq!(state.parse_key('\n'), Some(VimAction::ExitVimMode));
652
653        let _ = state.parse_key(':');
654        let _ = state.parse_key('w');
655        assert_eq!(
656            state.parse_key('\n'),
657            Some(VimAction::WriteFile { exit_vim: false })
658        );
659
660        let _ = state.parse_key(':');
661        let _ = state.parse_key('w');
662        let _ = state.parse_key('q');
663        assert_eq!(
664            state.parse_key('\n'),
665            Some(VimAction::WriteFile { exit_vim: true })
666        );
667
668        let _ = state.parse_key('/');
669        let _ = state.parse_key('x');
670        assert_eq!(
671            state.parse_key('\u{1b}'),
672            Some(VimAction::CommandLineChanged)
673        );
674        assert_eq!(state.command_line_text(), None);
675    }
676
677    #[test]
678    fn vim_pending_keys_formats_counts_prefixes_and_operators() {
679        let mut state = VimState::default();
680
681        assert_eq!(state.parse_key('5'), None);
682        assert_eq!(state.pending_keys(), "5");
683        assert_eq!(state.parse_key('d'), None);
684        assert_eq!(state.pending_keys(), "5d");
685        assert_eq!(state.parse_key('2'), None);
686        assert_eq!(state.pending_keys(), "5d2");
687
688        state.enter_clean_normal_mode();
689        assert_eq!(state.parse_key('3'), None);
690        assert_eq!(state.parse_key('g'), None);
691        assert_eq!(state.pending_keys(), "3g");
692    }
693
694    #[test]
695    fn vim_status_line_formats_mode_command_and_pending_input() {
696        let mut state = VimState::default();
697        assert_eq!(
698            state.status_line_text(),
699            ("-- NORMAL --".to_owned(), String::new())
700        );
701
702        let _ = state.parse_key('i');
703        assert_eq!(
704            state.status_line_text(),
705            ("-- INSERT --".to_owned(), String::new())
706        );
707        let _ = state.parse_key('\u{1b}');
708
709        let _ = state.parse_key('5');
710        let _ = state.parse_key('d');
711        assert_eq!(
712            state.status_line_text(),
713            ("-- NORMAL --".to_owned(), "5d".to_owned())
714        );
715
716        state.enter_clean_normal_mode();
717        let _ = state.parse_key('/');
718        let _ = state.parse_key('f');
719        let _ = state.parse_key('o');
720        let _ = state.parse_key('o');
721        assert_eq!(
722            state.status_line_text(),
723            ("/foo".to_owned(), String::new())
724        );
725
726        let _ = state.parse_key('\u{1b}');
727        let _ = state.parse_key('v');
728        assert_eq!(
729            state.status_line_text(),
730            ("-- VISUAL --".to_owned(), String::new())
731        );
732
733        let _ = state.parse_key('V');
734        assert_eq!(
735            state.status_line_text(),
736            ("-- VISUAL LINE --".to_owned(), String::new())
737        );
738    }
739}