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