Skip to main content

vtcode_vim/
engine.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2
3use crate::text::{
4    next_char_boundary, vim_current_line_bounds, vim_current_line_full_range, vim_end_word,
5    vim_find_char, vim_is_linewise_range, vim_line_end, vim_line_first_non_ws, vim_line_start,
6    vim_motion_range, vim_next_word_start, vim_prev_word_start, vim_text_object_range,
7};
8use crate::types::{
9    ChangeTarget, ClipboardKind, FindState, InsertCapture, InsertKind, InsertRepeat, Motion,
10    Operator, PendingState, RepeatableCommand, TextObjectSpec, VimMode, VimState,
11};
12
13const INDENT: &str = "    ";
14
15/// Minimal text-editor surface required by the Vim engine.
16pub trait Editor {
17    fn content(&self) -> &str;
18    fn cursor(&self) -> usize;
19    fn set_cursor(&mut self, pos: usize);
20    fn move_left(&mut self);
21    fn move_right(&mut self);
22    fn delete_char_forward(&mut self);
23    fn insert_text(&mut self, text: &str);
24    fn replace(&mut self, content: String, cursor: usize);
25}
26
27/// Result of routing a single key through the Vim engine.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct HandleKeyOutcome {
30    pub handled: bool,
31    pub clear_selection: bool,
32}
33
34/// Apply a single key event to the Vim state and editor surface.
35#[must_use]
36pub fn handle_key<E: Editor>(
37    state: &mut VimState,
38    editor: &mut E,
39    clipboard: &mut String,
40    key: &KeyEvent,
41) -> HandleKeyOutcome {
42    if !state.enabled() {
43        return HandleKeyOutcome::default();
44    }
45
46    if key
47        .modifiers
48        .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER)
49    {
50        return HandleKeyOutcome::default();
51    }
52
53    let mut ctx = VimContext {
54        state,
55        editor,
56        clipboard,
57    };
58    ctx.handle_key(key)
59}
60
61struct VimContext<'a, E> {
62    state: &'a mut VimState,
63    editor: &'a mut E,
64    clipboard: &'a mut String,
65}
66
67impl<E: Editor> VimContext<'_, E> {
68    fn handle_key(&mut self, key: &KeyEvent) -> HandleKeyOutcome {
69        match self.state.mode() {
70            VimMode::Insert => self.handle_insert_key(key),
71            VimMode::Normal => self.handle_normal_key(key),
72        }
73    }
74
75    fn handle_insert_key(&mut self, key: &KeyEvent) -> HandleKeyOutcome {
76        if matches!(key.code, KeyCode::Esc) {
77            self.finish_insert_capture();
78            self.state.set_mode(VimMode::Normal);
79            self.state.pending = None;
80            return HandleKeyOutcome {
81                handled: true,
82                clear_selection: true,
83            };
84        }
85        HandleKeyOutcome::default()
86    }
87
88    fn handle_normal_key(&mut self, key: &KeyEvent) -> HandleKeyOutcome {
89        let handled = match key.code {
90            KeyCode::Esc => {
91                self.state.pending = None;
92                return HandleKeyOutcome {
93                    handled: true,
94                    clear_selection: true,
95                };
96            }
97            KeyCode::Char(ch) => {
98                if let Some(pending) = self.state.pending.take() {
99                    self.handle_pending(pending, ch)
100                } else {
101                    self.handle_normal_char(ch)
102                }
103            }
104            KeyCode::Left => {
105                self.editor.move_left();
106                self.state.preferred_column = None;
107                true
108            }
109            KeyCode::Right => {
110                self.editor.move_right();
111                self.state.preferred_column = None;
112                true
113            }
114            KeyCode::Up => {
115                self.move_vertical(false);
116                true
117            }
118            KeyCode::Down => {
119                self.move_vertical(true);
120                true
121            }
122            _ => false,
123        };
124
125        HandleKeyOutcome {
126            handled,
127            clear_selection: false,
128        }
129    }
130
131    fn handle_pending(&mut self, pending: PendingState, ch: char) -> bool {
132        match pending {
133            PendingState::Operator(operator) => self.handle_operator(operator, ch),
134            PendingState::TextObject(operator, around) => {
135                self.handle_text_object(operator, around, ch)
136            }
137            PendingState::Find { till, forward } => self.handle_find(forward, till, ch),
138            PendingState::GoToLine => {
139                if ch == 'g' {
140                    self.move_to_line(true);
141                    true
142                } else {
143                    false
144                }
145            }
146        }
147    }
148
149    fn handle_normal_char(&mut self, ch: char) -> bool {
150        match ch {
151            'h' => {
152                self.editor.move_left();
153                self.state.preferred_column = None;
154                true
155            }
156            'l' => {
157                self.editor.move_right();
158                self.state.preferred_column = None;
159                true
160            }
161            'j' => {
162                self.move_vertical(true);
163                true
164            }
165            'k' => {
166                self.move_vertical(false);
167                true
168            }
169            'w' => {
170                self.move_motion(Motion::WordForward);
171                true
172            }
173            'e' => {
174                self.move_motion(Motion::EndWord);
175                true
176            }
177            'b' => {
178                self.move_motion(Motion::WordBackward);
179                true
180            }
181            '0' => {
182                self.editor
183                    .set_cursor(vim_line_start(self.editor.content(), self.editor.cursor()));
184                self.state.preferred_column = None;
185                true
186            }
187            '^' => {
188                self.editor.set_cursor(vim_line_first_non_ws(
189                    self.editor.content(),
190                    self.editor.cursor(),
191                ));
192                self.state.preferred_column = None;
193                true
194            }
195            '$' => {
196                self.editor
197                    .set_cursor(vim_line_end(self.editor.content(), self.editor.cursor()));
198                self.state.preferred_column = None;
199                true
200            }
201            'g' => {
202                self.state.pending = Some(PendingState::GoToLine);
203                true
204            }
205            'G' => {
206                self.move_to_line(false);
207                true
208            }
209            'f' => {
210                self.state.pending = Some(PendingState::Find {
211                    till: false,
212                    forward: true,
213                });
214                true
215            }
216            'F' => {
217                self.state.pending = Some(PendingState::Find {
218                    till: false,
219                    forward: false,
220                });
221                true
222            }
223            't' => {
224                self.state.pending = Some(PendingState::Find {
225                    till: true,
226                    forward: true,
227                });
228                true
229            }
230            'T' => {
231                self.state.pending = Some(PendingState::Find {
232                    till: true,
233                    forward: false,
234                });
235                true
236            }
237            ';' => self.repeat_find(false),
238            ',' => self.repeat_find(true),
239            'x' => {
240                if self.editor.cursor() < self.editor.content().len() {
241                    self.editor.delete_char_forward();
242                    self.state.last_change = Some(RepeatableCommand::DeleteChar);
243                }
244                true
245            }
246            'd' => {
247                self.state.pending = Some(PendingState::Operator(Operator::Delete));
248                true
249            }
250            'c' => {
251                self.state.pending = Some(PendingState::Operator(Operator::Change));
252                true
253            }
254            'y' => {
255                self.state.pending = Some(PendingState::Operator(Operator::Yank));
256                true
257            }
258            '>' => {
259                self.state.pending = Some(PendingState::Operator(Operator::Indent));
260                true
261            }
262            '<' => {
263                self.state.pending = Some(PendingState::Operator(Operator::Outdent));
264                true
265            }
266            'D' => {
267                self.delete_to_line_end();
268                self.state.last_change = Some(RepeatableCommand::DeleteToLineEnd);
269                true
270            }
271            'C' => {
272                self.change_to_line_end();
273                true
274            }
275            'Y' => {
276                self.yank_current_line();
277                true
278            }
279            'p' => {
280                self.paste(true);
281                self.state.last_change = Some(RepeatableCommand::PasteAfter);
282                true
283            }
284            'P' => {
285                self.paste(false);
286                self.state.last_change = Some(RepeatableCommand::PasteBefore);
287                true
288            }
289            'J' => {
290                self.join_lines();
291                self.state.last_change = Some(RepeatableCommand::JoinLines);
292                true
293            }
294            '.' => {
295                self.repeat_last_change();
296                true
297            }
298            'i' => {
299                self.start_insert(InsertKind::Insert);
300                true
301            }
302            'I' => {
303                let start = vim_line_first_non_ws(self.editor.content(), self.editor.cursor());
304                self.editor.set_cursor(start);
305                self.start_insert(InsertKind::InsertStart);
306                true
307            }
308            'a' => {
309                if self.editor.cursor() < self.editor.content().len() {
310                    self.editor.move_right();
311                }
312                self.start_insert(InsertKind::Append);
313                true
314            }
315            'A' => {
316                let end = vim_line_end(self.editor.content(), self.editor.cursor());
317                self.editor.set_cursor(end);
318                self.start_insert(InsertKind::AppendEnd);
319                true
320            }
321            'o' => {
322                self.open_line(true);
323                self.start_insert(InsertKind::OpenBelow);
324                true
325            }
326            'O' => {
327                self.open_line(false);
328                self.start_insert(InsertKind::OpenAbove);
329                true
330            }
331            _ => false,
332        }
333    }
334
335    fn handle_operator(&mut self, operator: Operator, ch: char) -> bool {
336        match (operator, ch) {
337            (Operator::Delete, 'd') | (Operator::Change, 'c') | (Operator::Yank, 'y') => {
338                self.apply_line_operator(operator);
339                true
340            }
341            (Operator::Indent, '>') | (Operator::Outdent, '<') => {
342                self.apply_line_operator(operator);
343                true
344            }
345            (_, 'w') => self.apply_motion_operator(operator, Motion::WordForward),
346            (_, 'e') => self.apply_motion_operator(operator, Motion::EndWord),
347            (_, 'b') => self.apply_motion_operator(operator, Motion::WordBackward),
348            (_, 'i') => {
349                self.state.pending = Some(PendingState::TextObject(operator, false));
350                true
351            }
352            (_, 'a') => {
353                self.state.pending = Some(PendingState::TextObject(operator, true));
354                true
355            }
356            _ => false,
357        }
358    }
359
360    fn handle_text_object(&mut self, operator: Operator, around: bool, ch: char) -> bool {
361        let object = match ch {
362            'w' => TextObjectSpec::Word { around, big: false },
363            'W' => TextObjectSpec::Word { around, big: true },
364            '"' => TextObjectSpec::Delimited {
365                around,
366                open: '"',
367                close: '"',
368            },
369            '\'' => TextObjectSpec::Delimited {
370                around,
371                open: '\'',
372                close: '\'',
373            },
374            '(' => TextObjectSpec::Delimited {
375                around,
376                open: '(',
377                close: ')',
378            },
379            '[' => TextObjectSpec::Delimited {
380                around,
381                open: '[',
382                close: ']',
383            },
384            '{' => TextObjectSpec::Delimited {
385                around,
386                open: '{',
387                close: '}',
388            },
389            _ => return false,
390        };
391
392        let handled = self.apply_text_object_operator(operator, object);
393        if handled {
394            self.state.last_change = match operator {
395                Operator::Delete | Operator::Indent | Operator::Outdent => {
396                    Some(RepeatableCommand::OperateTextObject { operator, object })
397                }
398                Operator::Change | Operator::Yank => None,
399            };
400        }
401        handled
402    }
403
404    fn handle_find(&mut self, forward: bool, till: bool, ch: char) -> bool {
405        if let Some(pos) = vim_find_char(
406            self.editor.content(),
407            self.editor.cursor(),
408            ch,
409            forward,
410            till,
411        ) {
412            self.editor.set_cursor(pos);
413            self.state.last_find = Some(FindState { ch, till, forward });
414            self.state.preferred_column = None;
415        }
416        true
417    }
418
419    fn repeat_find(&mut self, reverse: bool) -> bool {
420        let Some(find) = self.state.last_find else {
421            return true;
422        };
423        let forward = if reverse { !find.forward } else { find.forward };
424        self.handle_find(forward, find.till, find.ch)
425    }
426
427    fn start_insert(&mut self, kind: InsertKind) {
428        self.state.set_mode(VimMode::Insert);
429        self.state.pending = None;
430        self.state.insert_capture = Some(InsertCapture {
431            repeat: InsertRepeat::Insert(kind),
432            start: self.editor.cursor(),
433        });
434    }
435
436    fn finish_insert_capture(&mut self) {
437        let Some(capture) = self.state.insert_capture.take() else {
438            return;
439        };
440        let cursor = self.editor.cursor();
441        if cursor >= capture.start {
442            let inserted = self.editor.content()[capture.start..cursor].to_string();
443            self.state.last_change = match capture.repeat {
444                InsertRepeat::Insert(_) if inserted.is_empty() => None,
445                InsertRepeat::Insert(kind) => Some(RepeatableCommand::InsertText {
446                    kind,
447                    text: inserted,
448                }),
449                InsertRepeat::Change(target) => Some(RepeatableCommand::Change {
450                    target,
451                    text: inserted,
452                }),
453            };
454        }
455    }
456
457    fn begin_change(&mut self, start: usize, end: usize, target: ChangeTarget) {
458        self.capture_range(start, end);
459        self.replace_range(start, end, "");
460        self.state.set_mode(VimMode::Insert);
461        self.state.insert_capture = Some(InsertCapture {
462            repeat: InsertRepeat::Change(target),
463            start,
464        });
465    }
466
467    fn start_change(&mut self, target: ChangeTarget) -> bool {
468        match target {
469            ChangeTarget::Motion(motion) => {
470                let Some((start, end)) =
471                    vim_motion_range(self.editor.content(), self.editor.cursor(), motion)
472                else {
473                    return true;
474                };
475                self.begin_change(start, end, target);
476                true
477            }
478            ChangeTarget::TextObject(object) => {
479                let Some((start, end)) =
480                    vim_text_object_range(self.editor.content(), self.editor.cursor(), object)
481                else {
482                    return true;
483                };
484                self.begin_change(start, end, target);
485                true
486            }
487            ChangeTarget::Line => {
488                let (start, end) =
489                    vim_current_line_bounds(self.editor.content(), self.editor.cursor());
490                self.begin_change(start, end, target);
491                true
492            }
493            ChangeTarget::LineEnd => {
494                let start = self.editor.cursor();
495                let end = vim_line_end(self.editor.content(), self.editor.cursor());
496                self.begin_change(start, end, target);
497                true
498            }
499        }
500    }
501
502    fn move_motion(&mut self, motion: Motion) {
503        let next = match motion {
504            Motion::WordForward => vim_next_word_start(self.editor.content(), self.editor.cursor()),
505            Motion::EndWord => vim_end_word(self.editor.content(), self.editor.cursor()),
506            Motion::WordBackward => {
507                vim_prev_word_start(self.editor.content(), self.editor.cursor())
508            }
509        };
510        self.editor.set_cursor(next);
511        self.state.preferred_column = None;
512    }
513
514    fn move_vertical(&mut self, down: bool) {
515        let content = self.editor.content();
516        let (line_start, line_end) = vim_current_line_bounds(content, self.editor.cursor());
517        let column = self
518            .state
519            .preferred_column
520            .unwrap_or_else(|| self.editor.cursor().saturating_sub(line_start));
521        let target = if down {
522            if line_end >= content.len() {
523                self.editor.cursor()
524            } else {
525                let next_start = line_end + 1;
526                let next_end = content[next_start..]
527                    .find('\n')
528                    .map(|idx| next_start + idx)
529                    .unwrap_or(content.len());
530                (next_start + column).min(next_end)
531            }
532        } else if line_start == 0 {
533            self.editor.cursor()
534        } else {
535            let prev_end = line_start - 1;
536            let prev_start = content[..prev_end]
537                .rfind('\n')
538                .map(|idx| idx + 1)
539                .unwrap_or(0);
540            (prev_start + column).min(prev_end)
541        };
542        self.editor.set_cursor(target);
543        self.state.preferred_column = Some(column);
544    }
545
546    fn move_to_line(&mut self, first: bool) {
547        let content = self.editor.content();
548        let (current_start, _) = vim_current_line_bounds(content, self.editor.cursor());
549        let column = self
550            .state
551            .preferred_column
552            .unwrap_or_else(|| self.editor.cursor().saturating_sub(current_start));
553        let target = if first {
554            column.min(content.find('\n').unwrap_or(content.len()))
555        } else {
556            let last_start = content.rfind('\n').map(|idx| idx + 1).unwrap_or(0);
557            let last_end = content[last_start..]
558                .find('\n')
559                .map(|idx| last_start + idx)
560                .unwrap_or(content.len());
561            (last_start + column).min(last_end)
562        };
563        self.editor.set_cursor(target);
564        self.state.preferred_column = Some(column);
565    }
566
567    fn apply_motion_operator(&mut self, operator: Operator, motion: Motion) -> bool {
568        if operator == Operator::Change {
569            return self.start_change(ChangeTarget::Motion(motion));
570        }
571
572        let Some((start, end)) =
573            vim_motion_range(self.editor.content(), self.editor.cursor(), motion)
574        else {
575            return true;
576        };
577        self.apply_range_operator(operator, start, end);
578        if operator != Operator::Yank {
579            self.state.last_change = Some(RepeatableCommand::OperateMotion { operator, motion });
580        }
581        true
582    }
583
584    fn apply_text_object_operator(&mut self, operator: Operator, object: TextObjectSpec) -> bool {
585        if operator == Operator::Change {
586            return self.start_change(ChangeTarget::TextObject(object));
587        }
588
589        let Some((start, end)) =
590            vim_text_object_range(self.editor.content(), self.editor.cursor(), object)
591        else {
592            return true;
593        };
594        self.apply_range_operator(operator, start, end);
595        true
596    }
597
598    fn apply_line_operator(&mut self, operator: Operator) {
599        if operator == Operator::Change {
600            let _ = self.start_change(ChangeTarget::Line);
601            return;
602        }
603
604        let (start, end) = vim_current_line_full_range(self.editor.content(), self.editor.cursor());
605        self.apply_range_operator(operator, start, end);
606        if operator != Operator::Yank {
607            self.state.last_change = Some(RepeatableCommand::OperateLine { operator });
608        }
609    }
610
611    fn apply_range_operator(&mut self, operator: Operator, start: usize, end: usize) {
612        if start > end || end > self.editor.content().len() {
613            return;
614        }
615
616        match operator {
617            Operator::Yank => self.capture_range(start, end),
618            Operator::Delete => {
619                self.capture_range(start, end);
620                self.replace_range(start, end, "");
621            }
622            Operator::Indent => self.indent_range(start, end, true),
623            Operator::Outdent => self.indent_range(start, end, false),
624            Operator::Change => {}
625        }
626    }
627
628    fn indent_range(&mut self, start: usize, end: usize, indent: bool) {
629        let content = self.editor.content().to_string();
630        let mut out = String::with_capacity(content.len() + INDENT.len());
631        let mut line_start = 0;
632        for segment in content.split_inclusive('\n') {
633            let line_end = line_start + segment.len();
634            if line_end > start && line_start <= end {
635                if indent {
636                    out.push_str(INDENT);
637                    out.push_str(segment);
638                } else if let Some(stripped) = segment.strip_prefix(INDENT) {
639                    out.push_str(stripped);
640                } else {
641                    out.push_str(segment.trim_start_matches(' '));
642                }
643            } else {
644                out.push_str(segment);
645            }
646            line_start = line_end;
647        }
648        if !content.ends_with('\n') && line_start < content.len() {
649            let tail = &content[line_start..];
650            if line_start <= end && content.len() > start {
651                if indent {
652                    out.push_str(INDENT);
653                    out.push_str(tail);
654                } else if let Some(stripped) = tail.strip_prefix(INDENT) {
655                    out.push_str(stripped);
656                } else {
657                    out.push_str(tail.trim_start_matches(' '));
658                }
659            }
660        }
661        self.editor.replace(out, start.min(content.len()));
662    }
663
664    fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
665        let mut content = self.editor.content().to_string();
666        content.replace_range(start..end, replacement);
667        self.editor.replace(content, start + replacement.len());
668    }
669
670    fn capture_range(&mut self, start: usize, end: usize) {
671        *self.clipboard = self.editor.content()[start..end].to_string();
672        self.state.clipboard_kind = if vim_is_linewise_range(self.editor.content(), start, end) {
673            ClipboardKind::LineWise
674        } else {
675            ClipboardKind::CharWise
676        };
677    }
678
679    fn delete_to_line_end(&mut self) {
680        let end = vim_line_end(self.editor.content(), self.editor.cursor());
681        self.capture_range(self.editor.cursor(), end);
682        self.replace_range(self.editor.cursor(), end, "");
683    }
684
685    fn change_to_line_end(&mut self) {
686        let _ = self.start_change(ChangeTarget::LineEnd);
687    }
688
689    fn yank_current_line(&mut self) {
690        let (start, end) = vim_current_line_full_range(self.editor.content(), self.editor.cursor());
691        self.capture_range(start, end);
692    }
693
694    fn open_line(&mut self, below: bool) {
695        let insert_at = if below {
696            let end = vim_line_end(self.editor.content(), self.editor.cursor());
697            if end < self.editor.content().len() {
698                end + 1
699            } else {
700                end
701            }
702        } else {
703            vim_line_start(self.editor.content(), self.editor.cursor())
704        };
705        let mut content = self.editor.content().to_string();
706        content.insert(insert_at, '\n');
707        self.editor.replace(content, insert_at + 1);
708    }
709
710    fn paste(&mut self, after: bool) {
711        if self.clipboard.is_empty() {
712            return;
713        }
714        match self.state.clipboard_kind {
715            ClipboardKind::CharWise => {
716                let mut content = self.editor.content().to_string();
717                let insert_at = if after && self.editor.cursor() < content.len() {
718                    next_char_boundary(&content, self.editor.cursor())
719                } else {
720                    self.editor.cursor()
721                };
722                content.insert_str(insert_at, self.clipboard);
723                self.editor
724                    .replace(content, insert_at + self.clipboard.len());
725            }
726            ClipboardKind::LineWise => {
727                let mut content = self.editor.content().to_string();
728                let (line_start, line_end) =
729                    vim_current_line_bounds(&content, self.editor.cursor());
730                let insert_at = if after {
731                    if line_end < content.len() {
732                        line_end + 1
733                    } else {
734                        line_end
735                    }
736                } else {
737                    line_start
738                };
739                content.insert_str(insert_at, self.clipboard);
740                let cursor = (insert_at + self.clipboard.len()).min(content.len());
741                self.editor.replace(content, cursor);
742            }
743        }
744    }
745
746    fn join_lines(&mut self) {
747        let content = self.editor.content().to_string();
748        let (_, line_end) = vim_current_line_bounds(&content, self.editor.cursor());
749        if line_end >= content.len() {
750            return;
751        }
752        let next_start = line_end + 1;
753        let next_non_ws = content[next_start..]
754            .char_indices()
755            .find_map(|(idx, ch)| (!ch.is_whitespace()).then_some(next_start + idx))
756            .unwrap_or(next_start);
757        let mut joined = content;
758        joined.replace_range(line_end..next_non_ws, " ");
759        self.editor.replace(joined, line_end + 1);
760    }
761
762    fn repeat_last_change(&mut self) {
763        let Some(command) = self.state.last_change.clone() else {
764            return;
765        };
766        match command {
767            RepeatableCommand::DeleteChar => {
768                if self.editor.cursor() < self.editor.content().len() {
769                    self.editor.delete_char_forward();
770                }
771            }
772            RepeatableCommand::PasteAfter => self.paste(true),
773            RepeatableCommand::PasteBefore => self.paste(false),
774            RepeatableCommand::JoinLines => self.join_lines(),
775            RepeatableCommand::InsertText { kind, text } => self.repeat_insert(kind, &text),
776            RepeatableCommand::OperateMotion { operator, motion } => {
777                let _ = self.apply_motion_operator(operator, motion);
778            }
779            RepeatableCommand::OperateTextObject { operator, object } => {
780                let _ = self.apply_text_object_operator(operator, object);
781            }
782            RepeatableCommand::OperateLine { operator } => self.apply_line_operator(operator),
783            RepeatableCommand::DeleteToLineEnd => self.delete_to_line_end(),
784            RepeatableCommand::Change { target, text } => self.repeat_change(target, &text),
785        }
786    }
787
788    fn repeat_insert(&mut self, kind: InsertKind, text: &str) {
789        match kind {
790            InsertKind::Insert => {}
791            InsertKind::InsertStart => {
792                let start = vim_line_first_non_ws(self.editor.content(), self.editor.cursor());
793                self.editor.set_cursor(start);
794            }
795            InsertKind::Append => {
796                if self.editor.cursor() < self.editor.content().len() {
797                    self.editor.move_right();
798                }
799            }
800            InsertKind::AppendEnd => {
801                let end = vim_line_end(self.editor.content(), self.editor.cursor());
802                self.editor.set_cursor(end);
803            }
804            InsertKind::OpenBelow => self.open_line(true),
805            InsertKind::OpenAbove => self.open_line(false),
806        }
807        self.editor.insert_text(text);
808        self.state.set_mode(VimMode::Normal);
809    }
810
811    fn repeat_change(&mut self, target: ChangeTarget, text: &str) {
812        if !self.start_change(target) {
813            return;
814        }
815        self.editor.insert_text(text);
816        self.finish_insert_capture();
817        self.state.set_mode(VimMode::Normal);
818    }
819}