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
15pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct HandleKeyOutcome {
30 pub handled: bool,
31 pub clear_selection: bool,
32}
33
34#[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}