1use crate::{
2 Vim,
3 motion::{self, Motion},
4 object::{Object, surrounding_markers},
5 state::Mode,
6};
7use editor::{Bias, movement};
8use gpui::{Context, Window};
9use language::BracketPair;
10
11use std::sync::Arc;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum SurroundsType {
15 Motion(Motion),
16 Object(Object, bool),
17 Selection,
18}
19
20impl Vim {
21 pub fn add_surrounds(
22 &mut self,
23 text: Arc<str>,
24 target: SurroundsType,
25 window: &mut Window,
26 cx: &mut Context<Self>,
27 ) {
28 self.stop_recording(cx);
29 let count = Vim::take_count(cx);
30 let forced_motion = Vim::take_forced_motion(cx);
31 let mode = self.mode;
32 self.update_editor(cx, |_, editor, cx| {
33 let text_layout_details = editor.text_layout_details(window);
34 editor.transact(window, cx, |editor, window, cx| {
35 editor.set_clip_at_line_ends(false, cx);
36
37 let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
38 Some(pair) => pair.clone(),
39 None => BracketPair {
40 start: text.to_string(),
41 end: text.to_string(),
42 close: true,
43 surround: true,
44 newline: false,
45 },
46 };
47 let surround = pair.end != surround_alias((*text).as_ref());
48 let display_map = editor.display_snapshot(cx);
49 let display_selections = editor.selections.all_adjusted_display(&display_map);
50 let mut edits = Vec::new();
51 let mut anchors = Vec::new();
52
53 for selection in &display_selections {
54 let range = match &target {
55 SurroundsType::Object(object, around) => {
56 object.range(&display_map, selection.clone(), *around, None)
57 }
58 SurroundsType::Motion(motion) => {
59 motion
60 .range(
61 &display_map,
62 selection.clone(),
63 count,
64 &text_layout_details,
65 forced_motion,
66 )
67 .map(|(mut range, _)| {
68 // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
69 if let Motion::CurrentLine = motion {
70 range.start = motion::first_non_whitespace(
71 &display_map,
72 false,
73 range.start,
74 );
75 range.end = movement::saturating_right(
76 &display_map,
77 motion::last_non_whitespace(&display_map, range.end, 1),
78 );
79 }
80 range
81 })
82 }
83 SurroundsType::Selection => Some(selection.range()),
84 };
85
86 if let Some(range) = range {
87 let start = range.start.to_offset(&display_map, Bias::Right);
88 let end = range.end.to_offset(&display_map, Bias::Left);
89 let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine {
90 (format!("{}\n", pair.start), format!("\n{}", pair.end))
91 } else {
92 let maybe_space = if surround { " " } else { "" };
93 (
94 format!("{}{}", pair.start, maybe_space),
95 format!("{}{}", maybe_space, pair.end),
96 )
97 };
98 let start_anchor = display_map.buffer_snapshot().anchor_before(start);
99
100 edits.push((start..start, start_cursor_str));
101 edits.push((end..end, end_cursor_str));
102 anchors.push(start_anchor..start_anchor);
103 } else {
104 let start_anchor = display_map
105 .buffer_snapshot()
106 .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
107 anchors.push(start_anchor..start_anchor);
108 }
109 }
110
111 editor.edit(edits, cx);
112 editor.set_clip_at_line_ends(true, cx);
113 editor.change_selections(Default::default(), window, cx, |s| {
114 if mode == Mode::VisualBlock {
115 s.select_anchor_ranges(anchors.into_iter().take(1))
116 } else {
117 s.select_anchor_ranges(anchors)
118 }
119 });
120 });
121 });
122 self.switch_mode(Mode::Normal, false, window, cx);
123 }
124
125 pub fn delete_surrounds(
126 &mut self,
127 text: Arc<str>,
128 window: &mut Window,
129 cx: &mut Context<Self>,
130 ) {
131 self.stop_recording(cx);
132
133 // only legitimate surrounds can be removed
134 let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
135 Some(pair) => pair.clone(),
136 None => return,
137 };
138 let pair_object = match pair_to_object(&pair) {
139 Some(pair_object) => pair_object,
140 None => return,
141 };
142 let surround = pair.end != *text;
143
144 self.update_editor(cx, |_, editor, cx| {
145 editor.transact(window, cx, |editor, window, cx| {
146 editor.set_clip_at_line_ends(false, cx);
147
148 let display_map = editor.display_snapshot(cx);
149 let display_selections = editor.selections.all_display(&display_map);
150 let mut edits = Vec::new();
151 let mut anchors = Vec::new();
152
153 for selection in &display_selections {
154 let start = selection.start.to_offset(&display_map, Bias::Left);
155 if let Some(range) =
156 pair_object.range(&display_map, selection.clone(), true, None)
157 {
158 // If the current parenthesis object is single-line,
159 // then we need to filter whether it is the current line or not
160 if !pair_object.is_multiline() {
161 let is_same_row = selection.start.row() == range.start.row()
162 && selection.end.row() == range.end.row();
163 if !is_same_row {
164 anchors.push(start..start);
165 continue;
166 }
167 }
168 // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
169 // hello«ˇ "hello in a word" »again.
170 // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
171 // In order to be able to accurately match and replace in this case, some cumbersome methods are used
172 let mut chars_and_offset = display_map
173 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
174 .peekable();
175 while let Some((ch, offset)) = chars_and_offset.next() {
176 if ch.to_string() == pair.start {
177 let start = offset;
178 let mut end = start + 1;
179 if surround
180 && let Some((next_ch, _)) = chars_and_offset.peek()
181 && next_ch.eq(&' ')
182 {
183 end += 1;
184 }
185 edits.push((start..end, ""));
186 anchors.push(start..start);
187 break;
188 }
189 }
190 let mut reverse_chars_and_offsets = display_map
191 .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
192 .peekable();
193 while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
194 if ch.to_string() == pair.end {
195 let mut start = offset;
196 let end = start + 1;
197 if surround
198 && let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
199 && next_ch.eq(&' ')
200 {
201 start -= 1;
202 }
203 edits.push((start..end, ""));
204 break;
205 }
206 }
207 } else {
208 anchors.push(start..start);
209 }
210 }
211
212 editor.change_selections(Default::default(), window, cx, |s| {
213 s.select_ranges(anchors);
214 });
215 edits.sort_by_key(|(range, _)| range.start);
216 editor.edit(edits, cx);
217 editor.set_clip_at_line_ends(true, cx);
218 });
219 });
220 }
221
222 pub fn change_surrounds(
223 &mut self,
224 text: Arc<str>,
225 target: Object,
226 opening: bool,
227 window: &mut Window,
228 cx: &mut Context<Self>,
229 ) {
230 if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) {
231 self.stop_recording(cx);
232 self.update_editor(cx, |_, editor, cx| {
233 editor.transact(window, cx, |editor, window, cx| {
234 editor.set_clip_at_line_ends(false, cx);
235
236 let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
237 Some(pair) => pair.clone(),
238 None => BracketPair {
239 start: text.to_string(),
240 end: text.to_string(),
241 close: true,
242 surround: true,
243 newline: false,
244 },
245 };
246
247 // A single space should be added if the new surround is a
248 // bracket and not a quote (pair.start != pair.end) and if
249 // the bracket used is the opening bracket.
250 let add_space =
251 !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref()));
252
253 // Space should be preserved if either the surrounding
254 // characters being updated are quotes
255 // (will_replace_pair.start == will_replace_pair.end) or if
256 // the bracket used in the command is not an opening
257 // bracket.
258 let preserve_space =
259 will_replace_pair.start == will_replace_pair.end || !opening;
260
261 let display_map = editor.display_snapshot(cx);
262 let selections = editor.selections.all_adjusted_display(&display_map);
263 let mut edits = Vec::new();
264 let mut anchors = Vec::new();
265
266 for selection in &selections {
267 let start = selection.start.to_offset(&display_map, Bias::Left);
268 if let Some(range) =
269 target.range(&display_map, selection.clone(), true, None)
270 {
271 if !target.is_multiline() {
272 let is_same_row = selection.start.row() == range.start.row()
273 && selection.end.row() == range.end.row();
274 if !is_same_row {
275 anchors.push(start..start);
276 continue;
277 }
278 }
279
280 // Keeps track of the length of the string that is
281 // going to be edited on the start so we can ensure
282 // that the end replacement string does not exceed
283 // this value. Helpful when dealing with newlines.
284 let mut edit_len = 0;
285 let mut chars_and_offset = display_map
286 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
287 .peekable();
288
289 while let Some((ch, offset)) = chars_and_offset.next() {
290 if ch.to_string() == will_replace_pair.start {
291 let mut open_str = pair.start.clone();
292 let start = offset;
293 let mut end = start + 1;
294 while let Some((next_ch, _)) = chars_and_offset.next()
295 && next_ch.to_string() == " "
296 {
297 end += 1;
298
299 if preserve_space {
300 open_str.push(next_ch);
301 }
302 }
303
304 if add_space {
305 open_str.push(' ');
306 };
307
308 edit_len = end - start;
309 edits.push((start..end, open_str));
310 anchors.push(start..start);
311 break;
312 }
313 }
314
315 let mut reverse_chars_and_offsets = display_map
316 .reverse_buffer_chars_at(
317 range.end.to_offset(&display_map, Bias::Left),
318 )
319 .peekable();
320 while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
321 if ch.to_string() == will_replace_pair.end {
322 let mut close_str = String::new();
323 let mut start = offset;
324 let end = start + 1;
325 while let Some((next_ch, _)) = reverse_chars_and_offsets.next()
326 && next_ch.to_string() == " "
327 && close_str.len() < edit_len - 1
328 {
329 start -= 1;
330
331 if preserve_space {
332 close_str.push(next_ch);
333 }
334 }
335
336 if add_space {
337 close_str.push(' ');
338 };
339
340 close_str.push_str(&pair.end);
341 edits.push((start..end, close_str));
342 break;
343 }
344 }
345 } else {
346 anchors.push(start..start);
347 }
348 }
349
350 let stable_anchors = editor
351 .selections
352 .disjoint_anchors_arc()
353 .iter()
354 .map(|selection| {
355 let start = selection.start.bias_left(&display_map.buffer_snapshot());
356 start..start
357 })
358 .collect::<Vec<_>>();
359 edits.sort_by_key(|(range, _)| range.start);
360 editor.edit(edits, cx);
361 editor.set_clip_at_line_ends(true, cx);
362 editor.change_selections(Default::default(), window, cx, |s| {
363 s.select_anchor_ranges(stable_anchors);
364 });
365 });
366 });
367 }
368 }
369
370 /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
371 ///
372 /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
373 /// A pair of brackets is considered valid if it is well-formed and properly closed.
374 ///
375 /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
376 /// If no valid pair of brackets is found for any cursor, the method returns `false`.
377 pub fn check_and_move_to_valid_bracket_pair(
378 &mut self,
379 object: Object,
380 window: &mut Window,
381 cx: &mut Context<Self>,
382 ) -> bool {
383 let mut valid = false;
384 if let Some(pair) = self.object_to_bracket_pair(object, cx) {
385 self.update_editor(cx, |_, editor, cx| {
386 editor.transact(window, cx, |editor, window, cx| {
387 editor.set_clip_at_line_ends(false, cx);
388 let display_map = editor.display_snapshot(cx);
389 let selections = editor.selections.all_adjusted_display(&display_map);
390 let mut anchors = Vec::new();
391
392 for selection in &selections {
393 let start = selection.start.to_offset(&display_map, Bias::Left);
394 if let Some(range) =
395 object.range(&display_map, selection.clone(), true, None)
396 {
397 // If the current parenthesis object is single-line,
398 // then we need to filter whether it is the current line or not
399 if object.is_multiline()
400 || (!object.is_multiline()
401 && selection.start.row() == range.start.row()
402 && selection.end.row() == range.end.row())
403 {
404 valid = true;
405 let chars_and_offset = display_map
406 .buffer_chars_at(
407 range.start.to_offset(&display_map, Bias::Left),
408 )
409 .peekable();
410 for (ch, offset) in chars_and_offset {
411 if ch.to_string() == pair.start {
412 anchors.push(offset..offset);
413 break;
414 }
415 }
416 } else {
417 anchors.push(start..start)
418 }
419 } else {
420 anchors.push(start..start)
421 }
422 }
423 editor.change_selections(Default::default(), window, cx, |s| {
424 s.select_ranges(anchors);
425 });
426 editor.set_clip_at_line_ends(true, cx);
427 });
428 });
429 }
430 valid
431 }
432
433 fn object_to_bracket_pair(
434 &self,
435 object: Object,
436 cx: &mut Context<Self>,
437 ) -> Option<BracketPair> {
438 match object {
439 Object::Quotes => Some(BracketPair {
440 start: "'".to_string(),
441 end: "'".to_string(),
442 close: true,
443 surround: true,
444 newline: false,
445 }),
446 Object::BackQuotes => Some(BracketPair {
447 start: "`".to_string(),
448 end: "`".to_string(),
449 close: true,
450 surround: true,
451 newline: false,
452 }),
453 Object::DoubleQuotes => Some(BracketPair {
454 start: "\"".to_string(),
455 end: "\"".to_string(),
456 close: true,
457 surround: true,
458 newline: false,
459 }),
460 Object::VerticalBars => Some(BracketPair {
461 start: "|".to_string(),
462 end: "|".to_string(),
463 close: true,
464 surround: true,
465 newline: false,
466 }),
467 Object::Parentheses => Some(BracketPair {
468 start: "(".to_string(),
469 end: ")".to_string(),
470 close: true,
471 surround: true,
472 newline: false,
473 }),
474 Object::SquareBrackets => Some(BracketPair {
475 start: "[".to_string(),
476 end: "]".to_string(),
477 close: true,
478 surround: true,
479 newline: false,
480 }),
481 Object::CurlyBrackets { .. } => Some(BracketPair {
482 start: "{".to_string(),
483 end: "}".to_string(),
484 close: true,
485 surround: true,
486 newline: false,
487 }),
488 Object::AngleBrackets => Some(BracketPair {
489 start: "<".to_string(),
490 end: ">".to_string(),
491 close: true,
492 surround: true,
493 newline: false,
494 }),
495 Object::AnyBrackets => {
496 // If we're dealing with `AnyBrackets`, which can map to multiple
497 // bracket pairs, we'll need to first determine which `BracketPair` to
498 // target.
499 // As such, we keep track of the smallest range size, so
500 // that in cases like `({ name: "John" })` if the cursor is
501 // inside the curly brackets, we target the curly brackets
502 // instead of the parentheses.
503 let mut bracket_pair = None;
504 let mut min_range_size = usize::MAX;
505
506 let _ = self.editor.update(cx, |editor, cx| {
507 let display_map = editor.display_snapshot(cx);
508 let selections = editor.selections.all_adjusted_display(&display_map);
509 // Even if there's multiple cursors, we'll simply rely on
510 // the first one to understand what bracket pair to map to.
511 // I believe we could, if worth it, go one step above and
512 // have a `BracketPair` per selection, so that `AnyBracket`
513 // could work in situations where the transformation below
514 // could be done.
515 //
516 // ```
517 // (< name:ˇ'Zed' >)
518 // <[ name:ˇ'DeltaDB' ]>
519 // ```
520 //
521 // After using `csb{`:
522 //
523 // ```
524 // (ˇ{ name:'Zed' })
525 // <ˇ{ name:'DeltaDB' }>
526 // ```
527 if let Some(selection) = selections.first() {
528 let relative_to = selection.head();
529 let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
530 let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
531
532 for &(open, close) in bracket_pairs.iter() {
533 if let Some(range) = surrounding_markers(
534 &display_map,
535 relative_to,
536 true,
537 false,
538 open,
539 close,
540 ) {
541 let start_offset = range.start.to_offset(&display_map, Bias::Left);
542 let end_offset = range.end.to_offset(&display_map, Bias::Right);
543
544 if cursor_offset >= start_offset && cursor_offset <= end_offset {
545 let size = end_offset - start_offset;
546 if size < min_range_size {
547 min_range_size = size;
548 bracket_pair = Some(BracketPair {
549 start: open.to_string(),
550 end: close.to_string(),
551 close: true,
552 surround: true,
553 newline: false,
554 })
555 }
556 }
557 }
558 }
559 }
560 });
561
562 bracket_pair
563 }
564 _ => None,
565 }
566 }
567}
568
569fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
570 pairs
571 .iter()
572 .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
573}
574
575fn surround_alias(ch: &str) -> &str {
576 match ch {
577 "b" => ")",
578 "B" => "}",
579 "a" => ">",
580 "r" => "]",
581 _ => ch,
582 }
583}
584
585fn all_support_surround_pair() -> Vec<BracketPair> {
586 vec![
587 BracketPair {
588 start: "{".into(),
589 end: "}".into(),
590 close: true,
591 surround: true,
592 newline: false,
593 },
594 BracketPair {
595 start: "'".into(),
596 end: "'".into(),
597 close: true,
598 surround: true,
599 newline: false,
600 },
601 BracketPair {
602 start: "`".into(),
603 end: "`".into(),
604 close: true,
605 surround: true,
606 newline: false,
607 },
608 BracketPair {
609 start: "\"".into(),
610 end: "\"".into(),
611 close: true,
612 surround: true,
613 newline: false,
614 },
615 BracketPair {
616 start: "(".into(),
617 end: ")".into(),
618 close: true,
619 surround: true,
620 newline: false,
621 },
622 BracketPair {
623 start: "|".into(),
624 end: "|".into(),
625 close: true,
626 surround: true,
627 newline: false,
628 },
629 BracketPair {
630 start: "[".into(),
631 end: "]".into(),
632 close: true,
633 surround: true,
634 newline: false,
635 },
636 BracketPair {
637 start: "<".into(),
638 end: ">".into(),
639 close: true,
640 surround: true,
641 newline: false,
642 },
643 ]
644}
645
646fn pair_to_object(pair: &BracketPair) -> Option<Object> {
647 match pair.start.as_str() {
648 "'" => Some(Object::Quotes),
649 "`" => Some(Object::BackQuotes),
650 "\"" => Some(Object::DoubleQuotes),
651 "|" => Some(Object::VerticalBars),
652 "(" => Some(Object::Parentheses),
653 "[" => Some(Object::SquareBrackets),
654 "{" => Some(Object::CurlyBrackets),
655 "<" => Some(Object::AngleBrackets),
656 _ => None,
657 }
658}
659
660#[cfg(test)]
661mod test {
662 use gpui::KeyBinding;
663 use indoc::indoc;
664
665 use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
666
667 #[gpui::test]
668 async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
669 let mut cx = VimTestContext::new(cx, true).await;
670
671 // test add surrounds with around
672 cx.set_state(
673 indoc! {"
674 The quˇick brown
675 fox jumps over
676 the lazy dog."},
677 Mode::Normal,
678 );
679 cx.simulate_keystrokes("y s i w {");
680 cx.assert_state(
681 indoc! {"
682 The ˇ{ quick } brown
683 fox jumps over
684 the lazy dog."},
685 Mode::Normal,
686 );
687
688 // test add surrounds not with around
689 cx.set_state(
690 indoc! {"
691 The quˇick brown
692 fox jumps over
693 the lazy dog."},
694 Mode::Normal,
695 );
696 cx.simulate_keystrokes("y s i w }");
697 cx.assert_state(
698 indoc! {"
699 The ˇ{quick} brown
700 fox jumps over
701 the lazy dog."},
702 Mode::Normal,
703 );
704
705 // test add surrounds with motion
706 cx.set_state(
707 indoc! {"
708 The quˇick brown
709 fox jumps over
710 the lazy dog."},
711 Mode::Normal,
712 );
713 cx.simulate_keystrokes("y s $ }");
714 cx.assert_state(
715 indoc! {"
716 The quˇ{ick brown}
717 fox jumps over
718 the lazy dog."},
719 Mode::Normal,
720 );
721
722 // test add surrounds with multi cursor
723 cx.set_state(
724 indoc! {"
725 The quˇick brown
726 fox jumps over
727 the laˇzy dog."},
728 Mode::Normal,
729 );
730 cx.simulate_keystrokes("y s i w '");
731 cx.assert_state(
732 indoc! {"
733 The ˇ'quick' brown
734 fox jumps over
735 the ˇ'lazy' dog."},
736 Mode::Normal,
737 );
738
739 // test multi cursor add surrounds with motion
740 cx.set_state(
741 indoc! {"
742 The quˇick brown
743 fox jumps over
744 the laˇzy dog."},
745 Mode::Normal,
746 );
747 cx.simulate_keystrokes("y s $ '");
748 cx.assert_state(
749 indoc! {"
750 The quˇ'ick brown'
751 fox jumps over
752 the laˇ'zy dog.'"},
753 Mode::Normal,
754 );
755
756 // test multi cursor add surrounds with motion and custom string
757 cx.set_state(
758 indoc! {"
759 The quˇick brown
760 fox jumps over
761 the laˇzy dog."},
762 Mode::Normal,
763 );
764 cx.simulate_keystrokes("y s $ 1");
765 cx.assert_state(
766 indoc! {"
767 The quˇ1ick brown1
768 fox jumps over
769 the laˇ1zy dog.1"},
770 Mode::Normal,
771 );
772
773 // test add surrounds with motion current line
774 cx.set_state(
775 indoc! {"
776 The quˇick brown
777 fox jumps over
778 the lazy dog."},
779 Mode::Normal,
780 );
781 cx.simulate_keystrokes("y s s {");
782 cx.assert_state(
783 indoc! {"
784 ˇ{ The quick brown }
785 fox jumps over
786 the lazy dog."},
787 Mode::Normal,
788 );
789
790 cx.set_state(
791 indoc! {"
792 The quˇick brown•
793 fox jumps over
794 the lazy dog."},
795 Mode::Normal,
796 );
797 cx.simulate_keystrokes("y s s {");
798 cx.assert_state(
799 indoc! {"
800 ˇ{ The quick brown }•
801 fox jumps over
802 the lazy dog."},
803 Mode::Normal,
804 );
805 cx.simulate_keystrokes("2 y s s )");
806 cx.assert_state(
807 indoc! {"
808 ˇ({ The quick brown }•
809 fox jumps over)
810 the lazy dog."},
811 Mode::Normal,
812 );
813
814 // test add surrounds around object
815 cx.set_state(
816 indoc! {"
817 The [quˇick] brown
818 fox jumps over
819 the lazy dog."},
820 Mode::Normal,
821 );
822 cx.simulate_keystrokes("y s a ] )");
823 cx.assert_state(
824 indoc! {"
825 The ˇ([quick]) brown
826 fox jumps over
827 the lazy dog."},
828 Mode::Normal,
829 );
830
831 // test add surrounds inside object
832 cx.set_state(
833 indoc! {"
834 The [quˇick] brown
835 fox jumps over
836 the lazy dog."},
837 Mode::Normal,
838 );
839 cx.simulate_keystrokes("y s i ] )");
840 cx.assert_state(
841 indoc! {"
842 The [ˇ(quick)] brown
843 fox jumps over
844 the lazy dog."},
845 Mode::Normal,
846 );
847 }
848
849 #[gpui::test]
850 async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
851 let mut cx = VimTestContext::new(cx, true).await;
852
853 cx.update(|_, cx| {
854 cx.bind_keys([KeyBinding::new(
855 "shift-s",
856 PushAddSurrounds {},
857 Some("vim_mode == visual"),
858 )])
859 });
860
861 // test add surrounds with around
862 cx.set_state(
863 indoc! {"
864 The quˇick brown
865 fox jumps over
866 the lazy dog."},
867 Mode::Normal,
868 );
869 cx.simulate_keystrokes("v i w shift-s {");
870 cx.assert_state(
871 indoc! {"
872 The ˇ{ quick } brown
873 fox jumps over
874 the lazy dog."},
875 Mode::Normal,
876 );
877
878 // test add surrounds not with around
879 cx.set_state(
880 indoc! {"
881 The quˇick brown
882 fox jumps over
883 the lazy dog."},
884 Mode::Normal,
885 );
886 cx.simulate_keystrokes("v i w shift-s }");
887 cx.assert_state(
888 indoc! {"
889 The ˇ{quick} brown
890 fox jumps over
891 the lazy dog."},
892 Mode::Normal,
893 );
894
895 // test add surrounds with motion
896 cx.set_state(
897 indoc! {"
898 The quˇick brown
899 fox jumps over
900 the lazy dog."},
901 Mode::Normal,
902 );
903 cx.simulate_keystrokes("v e shift-s }");
904 cx.assert_state(
905 indoc! {"
906 The quˇ{ick} brown
907 fox jumps over
908 the lazy dog."},
909 Mode::Normal,
910 );
911
912 // test add surrounds with multi cursor
913 cx.set_state(
914 indoc! {"
915 The quˇick brown
916 fox jumps over
917 the laˇzy dog."},
918 Mode::Normal,
919 );
920 cx.simulate_keystrokes("v i w shift-s '");
921 cx.assert_state(
922 indoc! {"
923 The ˇ'quick' brown
924 fox jumps over
925 the ˇ'lazy' dog."},
926 Mode::Normal,
927 );
928
929 // test add surrounds with visual block
930 cx.set_state(
931 indoc! {"
932 The quˇick brown
933 fox jumps over
934 the lazy dog."},
935 Mode::Normal,
936 );
937 cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
938 cx.assert_state(
939 indoc! {"
940 The ˇ'quick' brown
941 fox 'jumps' over
942 the 'lazy 'dog."},
943 Mode::Normal,
944 );
945
946 // test add surrounds with visual line
947 cx.set_state(
948 indoc! {"
949 The quˇick brown
950 fox jumps over
951 the lazy dog."},
952 Mode::Normal,
953 );
954 cx.simulate_keystrokes("j shift-v shift-s '");
955 cx.assert_state(
956 indoc! {"
957 The quick brown
958 ˇ'
959 fox jumps over
960 '
961 the lazy dog."},
962 Mode::Normal,
963 );
964 }
965
966 #[gpui::test]
967 async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
968 let mut cx = VimTestContext::new(cx, true).await;
969
970 // test delete surround
971 cx.set_state(
972 indoc! {"
973 The {quˇick} brown
974 fox jumps over
975 the lazy dog."},
976 Mode::Normal,
977 );
978 cx.simulate_keystrokes("d s {");
979 cx.assert_state(
980 indoc! {"
981 The ˇquick brown
982 fox jumps over
983 the lazy dog."},
984 Mode::Normal,
985 );
986
987 // test delete not exist surrounds
988 cx.set_state(
989 indoc! {"
990 The {quˇick} brown
991 fox jumps over
992 the lazy dog."},
993 Mode::Normal,
994 );
995 cx.simulate_keystrokes("d s [");
996 cx.assert_state(
997 indoc! {"
998 The {quˇick} brown
999 fox jumps over
1000 the lazy dog."},
1001 Mode::Normal,
1002 );
1003
1004 // test delete surround forward exist, in the surrounds plugin of other editors,
1005 // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
1006 cx.set_state(
1007 indoc! {"
1008 The {quick} brˇown
1009 fox jumps over
1010 the lazy dog."},
1011 Mode::Normal,
1012 );
1013 cx.simulate_keystrokes("d s {");
1014 cx.assert_state(
1015 indoc! {"
1016 The {quick} brˇown
1017 fox jumps over
1018 the lazy dog."},
1019 Mode::Normal,
1020 );
1021
1022 // test cursor delete inner surrounds
1023 cx.set_state(
1024 indoc! {"
1025 The { quick brown
1026 fox jumˇps over }
1027 the lazy dog."},
1028 Mode::Normal,
1029 );
1030 cx.simulate_keystrokes("d s {");
1031 cx.assert_state(
1032 indoc! {"
1033 The ˇquick brown
1034 fox jumps over
1035 the lazy dog."},
1036 Mode::Normal,
1037 );
1038
1039 // test multi cursor delete surrounds
1040 cx.set_state(
1041 indoc! {"
1042 The [quˇick] brown
1043 fox jumps over
1044 the [laˇzy] dog."},
1045 Mode::Normal,
1046 );
1047 cx.simulate_keystrokes("d s ]");
1048 cx.assert_state(
1049 indoc! {"
1050 The ˇquick brown
1051 fox jumps over
1052 the ˇlazy dog."},
1053 Mode::Normal,
1054 );
1055
1056 // test multi cursor delete surrounds with around
1057 cx.set_state(
1058 indoc! {"
1059 Tˇhe [ quick ] brown
1060 fox jumps over
1061 the [laˇzy] dog."},
1062 Mode::Normal,
1063 );
1064 cx.simulate_keystrokes("d s [");
1065 cx.assert_state(
1066 indoc! {"
1067 The ˇquick brown
1068 fox jumps over
1069 the ˇlazy dog."},
1070 Mode::Normal,
1071 );
1072
1073 cx.set_state(
1074 indoc! {"
1075 Tˇhe [ quick ] brown
1076 fox jumps over
1077 the [laˇzy ] dog."},
1078 Mode::Normal,
1079 );
1080 cx.simulate_keystrokes("d s [");
1081 cx.assert_state(
1082 indoc! {"
1083 The ˇquick brown
1084 fox jumps over
1085 the ˇlazy dog."},
1086 Mode::Normal,
1087 );
1088
1089 // test multi cursor delete different surrounds
1090 // the pair corresponding to the two cursors is the same,
1091 // so they are combined into one cursor
1092 cx.set_state(
1093 indoc! {"
1094 The [quˇick] brown
1095 fox jumps over
1096 the {laˇzy} dog."},
1097 Mode::Normal,
1098 );
1099 cx.simulate_keystrokes("d s {");
1100 cx.assert_state(
1101 indoc! {"
1102 The [quick] brown
1103 fox jumps over
1104 the ˇlazy dog."},
1105 Mode::Normal,
1106 );
1107
1108 // test delete surround with multi cursor and nest surrounds
1109 cx.set_state(
1110 indoc! {"
1111 fn test_surround() {
1112 ifˇ 2 > 1 {
1113 ˇprintln!(\"it is fine\");
1114 };
1115 }"},
1116 Mode::Normal,
1117 );
1118 cx.simulate_keystrokes("d s }");
1119 cx.assert_state(
1120 indoc! {"
1121 fn test_surround() ˇ
1122 if 2 > 1 ˇ
1123 println!(\"it is fine\");
1124 ;
1125 "},
1126 Mode::Normal,
1127 );
1128 }
1129
1130 #[gpui::test]
1131 async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1132 let mut cx = VimTestContext::new(cx, true).await;
1133
1134 cx.set_state(
1135 indoc! {"
1136 The {quˇick} brown
1137 fox jumps over
1138 the lazy dog."},
1139 Mode::Normal,
1140 );
1141 cx.simulate_keystrokes("c s { [");
1142 cx.assert_state(
1143 indoc! {"
1144 The ˇ[ quick ] brown
1145 fox jumps over
1146 the lazy dog."},
1147 Mode::Normal,
1148 );
1149
1150 // test multi cursor change surrounds
1151 cx.set_state(
1152 indoc! {"
1153 The {quˇick} brown
1154 fox jumps over
1155 the {laˇzy} dog."},
1156 Mode::Normal,
1157 );
1158 cx.simulate_keystrokes("c s { [");
1159 cx.assert_state(
1160 indoc! {"
1161 The ˇ[ quick ] brown
1162 fox jumps over
1163 the ˇ[ lazy ] dog."},
1164 Mode::Normal,
1165 );
1166
1167 // test multi cursor delete different surrounds with after cursor
1168 cx.set_state(
1169 indoc! {"
1170 Thˇe {quick} brown
1171 fox jumps over
1172 the {laˇzy} dog."},
1173 Mode::Normal,
1174 );
1175 cx.simulate_keystrokes("c s { [");
1176 cx.assert_state(
1177 indoc! {"
1178 The ˇ[ quick ] brown
1179 fox jumps over
1180 the ˇ[ lazy ] dog."},
1181 Mode::Normal,
1182 );
1183
1184 // test multi cursor change surrount with not around
1185 cx.set_state(
1186 indoc! {"
1187 Thˇe { quick } brown
1188 fox jumps over
1189 the {laˇzy} dog."},
1190 Mode::Normal,
1191 );
1192 cx.simulate_keystrokes("c s { ]");
1193 cx.assert_state(
1194 indoc! {"
1195 The ˇ[quick] brown
1196 fox jumps over
1197 the ˇ[lazy] dog."},
1198 Mode::Normal,
1199 );
1200
1201 // test multi cursor change with not exist surround
1202 cx.set_state(
1203 indoc! {"
1204 The {quˇick} brown
1205 fox jumps over
1206 the [laˇzy] dog."},
1207 Mode::Normal,
1208 );
1209 cx.simulate_keystrokes("c s [ '");
1210 cx.assert_state(
1211 indoc! {"
1212 The {quick} brown
1213 fox jumps over
1214 the ˇ'lazy' dog."},
1215 Mode::Normal,
1216 );
1217
1218 // test change nesting surrounds
1219 cx.set_state(
1220 indoc! {"
1221 fn test_surround() {
1222 ifˇ 2 > 1 {
1223 ˇprintln!(\"it is fine\");
1224 }
1225 };"},
1226 Mode::Normal,
1227 );
1228 cx.simulate_keystrokes("c s } ]");
1229 cx.assert_state(
1230 indoc! {"
1231 fn test_surround() ˇ[
1232 if 2 > 1 ˇ[
1233 println!(\"it is fine\");
1234 ]
1235 ];"},
1236 Mode::Normal,
1237 );
1238
1239 // Currently, the same test case but using the closing bracket `]`
1240 // actually removes a whitespace before the closing bracket, something
1241 // that might need to be fixed?
1242 cx.set_state(
1243 indoc! {"
1244 fn test_surround() {
1245 ifˇ 2 > 1 {
1246 ˇprintln!(\"it is fine\");
1247 }
1248 };"},
1249 Mode::Normal,
1250 );
1251 cx.simulate_keystrokes("c s { ]");
1252 cx.assert_state(
1253 indoc! {"
1254 fn test_surround() ˇ[
1255 if 2 > 1 ˇ[
1256 println!(\"it is fine\");
1257 ]
1258 ];"},
1259 Mode::Normal,
1260 );
1261
1262 // test change quotes.
1263 cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
1264 cx.simulate_keystrokes("c s ' \"");
1265 cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
1266
1267 // test multi cursor change quotes
1268 cx.set_state(
1269 indoc! {"
1270 ' ˇstr '
1271 some example text here
1272 ˇ' str '
1273 "},
1274 Mode::Normal,
1275 );
1276 cx.simulate_keystrokes("c s ' \"");
1277 cx.assert_state(
1278 indoc! {"
1279 ˇ\" str \"
1280 some example text here
1281 ˇ\" str \"
1282 "},
1283 Mode::Normal,
1284 );
1285
1286 // test quote to bracket spacing.
1287 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1288 cx.simulate_keystrokes("c s ' {");
1289 cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1290
1291 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1292 cx.simulate_keystrokes("c s ' }");
1293 cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1294 }
1295
1296 #[gpui::test]
1297 async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1298 let mut cx = VimTestContext::new(cx, true).await;
1299
1300 // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1301 // action.
1302 cx.update(|_, cx| {
1303 cx.bind_keys([KeyBinding::new(
1304 "b",
1305 AnyBrackets,
1306 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1307 )]);
1308 });
1309
1310 cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1311 cx.simulate_keystrokes("c s b [");
1312 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1313
1314 cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1315 cx.simulate_keystrokes("c s b {");
1316 cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1317
1318 cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1319 cx.simulate_keystrokes("c s b [");
1320 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1321
1322 cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1323 cx.simulate_keystrokes("c s b [");
1324 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1325
1326 cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1327 cx.simulate_keystrokes("c s b }");
1328 cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1329
1330 cx.set_state(
1331 indoc! {"
1332 (< name: ˇ'Zed' >)
1333 (< nˇame: 'DeltaDB' >)
1334 "},
1335 Mode::Normal,
1336 );
1337 cx.simulate_keystrokes("c s b {");
1338 cx.set_state(
1339 indoc! {"
1340 (ˇ{ name: 'Zed' })
1341 (ˇ{ name: 'DeltaDB' })
1342 "},
1343 Mode::Normal,
1344 );
1345 }
1346
1347 // The following test cases all follow tpope/vim-surround's behaviour
1348 // and are more focused on how whitespace is handled.
1349 #[gpui::test]
1350 async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
1351 let mut cx = VimTestContext::new(cx, true).await;
1352
1353 // Changing quote to quote should never change the surrounding
1354 // whitespace.
1355 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1356 cx.simulate_keystrokes("c s ' \"");
1357 cx.assert_state(indoc! {"ˇ\" a \""}, Mode::Normal);
1358
1359 cx.set_state(indoc! {"\" ˇa \""}, Mode::Normal);
1360 cx.simulate_keystrokes("c s \" '");
1361 cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal);
1362
1363 // Changing quote to bracket adds one more space when the opening
1364 // bracket is used, does not affect whitespace when the closing bracket
1365 // is used.
1366 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1367 cx.simulate_keystrokes("c s ' {");
1368 cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal);
1369
1370 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1371 cx.simulate_keystrokes("c s ' }");
1372 cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal);
1373
1374 // Changing bracket to quote should remove all space when the
1375 // opening bracket is used and preserve all space when the
1376 // closing one is used.
1377 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1378 cx.simulate_keystrokes("c s { '");
1379 cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);
1380
1381 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1382 cx.simulate_keystrokes("c s } '");
1383 cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal);
1384
1385 // Changing bracket to bracket follows these rules:
1386 // * opening → opening – keeps only one space.
1387 // * opening → closing – removes all space.
1388 // * closing → opening – adds one space.
1389 // * closing → closing – does not change space.
1390 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1391 cx.simulate_keystrokes("c s { [");
1392 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1393
1394 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1395 cx.simulate_keystrokes("c s { ]");
1396 cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);
1397
1398 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1399 cx.simulate_keystrokes("c s } [");
1400 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1401
1402 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1403 cx.simulate_keystrokes("c s } ]");
1404 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1405 }
1406
1407 #[gpui::test]
1408 async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1409 let mut cx = VimTestContext::new(cx, true).await;
1410
1411 cx.set_state(
1412 indoc! {"
1413 The quˇick brown
1414 fox jumps over
1415 the lazy dog."},
1416 Mode::Normal,
1417 );
1418 cx.simulate_keystrokes("y s i w [");
1419 cx.assert_state(
1420 indoc! {"
1421 The ˇ[ quick ] brown
1422 fox jumps over
1423 the lazy dog."},
1424 Mode::Normal,
1425 );
1426
1427 cx.simulate_keystrokes("c s [ }");
1428 cx.assert_state(
1429 indoc! {"
1430 The ˇ{quick} brown
1431 fox jumps over
1432 the lazy dog."},
1433 Mode::Normal,
1434 );
1435
1436 cx.simulate_keystrokes("d s {");
1437 cx.assert_state(
1438 indoc! {"
1439 The ˇquick brown
1440 fox jumps over
1441 the lazy dog."},
1442 Mode::Normal,
1443 );
1444
1445 cx.simulate_keystrokes("u");
1446 cx.assert_state(
1447 indoc! {"
1448 The ˇ{quick} brown
1449 fox jumps over
1450 the lazy dog."},
1451 Mode::Normal,
1452 );
1453 }
1454
1455 #[gpui::test]
1456 async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1457 let mut cx = VimTestContext::new(cx, true).await;
1458
1459 // add aliases
1460 cx.set_state(
1461 indoc! {"
1462 The quˇick brown
1463 fox jumps over
1464 the lazy dog."},
1465 Mode::Normal,
1466 );
1467 cx.simulate_keystrokes("y s i w b");
1468 cx.assert_state(
1469 indoc! {"
1470 The ˇ(quick) brown
1471 fox jumps over
1472 the lazy dog."},
1473 Mode::Normal,
1474 );
1475
1476 cx.set_state(
1477 indoc! {"
1478 The quˇick brown
1479 fox jumps over
1480 the lazy dog."},
1481 Mode::Normal,
1482 );
1483 cx.simulate_keystrokes("y s i w B");
1484 cx.assert_state(
1485 indoc! {"
1486 The ˇ{quick} brown
1487 fox jumps over
1488 the lazy dog."},
1489 Mode::Normal,
1490 );
1491
1492 cx.set_state(
1493 indoc! {"
1494 The quˇick brown
1495 fox jumps over
1496 the lazy dog."},
1497 Mode::Normal,
1498 );
1499 cx.simulate_keystrokes("y s i w a");
1500 cx.assert_state(
1501 indoc! {"
1502 The ˇ<quick> brown
1503 fox jumps over
1504 the lazy dog."},
1505 Mode::Normal,
1506 );
1507
1508 cx.set_state(
1509 indoc! {"
1510 The quˇick brown
1511 fox jumps over
1512 the lazy dog."},
1513 Mode::Normal,
1514 );
1515 cx.simulate_keystrokes("y s i w r");
1516 cx.assert_state(
1517 indoc! {"
1518 The ˇ[quick] brown
1519 fox jumps over
1520 the lazy dog."},
1521 Mode::Normal,
1522 );
1523
1524 // change aliases
1525 cx.set_state(
1526 indoc! {"
1527 The {quˇick} brown
1528 fox jumps over
1529 the lazy dog."},
1530 Mode::Normal,
1531 );
1532 cx.simulate_keystrokes("c s { b");
1533 cx.assert_state(
1534 indoc! {"
1535 The ˇ(quick) brown
1536 fox jumps over
1537 the lazy dog."},
1538 Mode::Normal,
1539 );
1540
1541 cx.set_state(
1542 indoc! {"
1543 The (quˇick) brown
1544 fox jumps over
1545 the lazy dog."},
1546 Mode::Normal,
1547 );
1548 cx.simulate_keystrokes("c s ( B");
1549 cx.assert_state(
1550 indoc! {"
1551 The ˇ{quick} brown
1552 fox jumps over
1553 the lazy dog."},
1554 Mode::Normal,
1555 );
1556
1557 cx.set_state(
1558 indoc! {"
1559 The (quˇick) brown
1560 fox jumps over
1561 the lazy dog."},
1562 Mode::Normal,
1563 );
1564 cx.simulate_keystrokes("c s ( a");
1565 cx.assert_state(
1566 indoc! {"
1567 The ˇ<quick> brown
1568 fox jumps over
1569 the lazy dog."},
1570 Mode::Normal,
1571 );
1572
1573 cx.set_state(
1574 indoc! {"
1575 The <quˇick> brown
1576 fox jumps over
1577 the lazy dog."},
1578 Mode::Normal,
1579 );
1580 cx.simulate_keystrokes("c s < b");
1581 cx.assert_state(
1582 indoc! {"
1583 The ˇ(quick) brown
1584 fox jumps over
1585 the lazy dog."},
1586 Mode::Normal,
1587 );
1588
1589 cx.set_state(
1590 indoc! {"
1591 The (quˇick) brown
1592 fox jumps over
1593 the lazy dog."},
1594 Mode::Normal,
1595 );
1596 cx.simulate_keystrokes("c s ( r");
1597 cx.assert_state(
1598 indoc! {"
1599 The ˇ[quick] brown
1600 fox jumps over
1601 the lazy dog."},
1602 Mode::Normal,
1603 );
1604
1605 cx.set_state(
1606 indoc! {"
1607 The [quˇick] brown
1608 fox jumps over
1609 the lazy dog."},
1610 Mode::Normal,
1611 );
1612 cx.simulate_keystrokes("c s [ b");
1613 cx.assert_state(
1614 indoc! {"
1615 The ˇ(quick) brown
1616 fox jumps over
1617 the lazy dog."},
1618 Mode::Normal,
1619 );
1620
1621 // delete alias
1622 cx.set_state(
1623 indoc! {"
1624 The {quˇick} brown
1625 fox jumps over
1626 the lazy dog."},
1627 Mode::Normal,
1628 );
1629 cx.simulate_keystrokes("d s B");
1630 cx.assert_state(
1631 indoc! {"
1632 The ˇquick brown
1633 fox jumps over
1634 the lazy dog."},
1635 Mode::Normal,
1636 );
1637
1638 cx.set_state(
1639 indoc! {"
1640 The (quˇick) brown
1641 fox jumps over
1642 the lazy dog."},
1643 Mode::Normal,
1644 );
1645 cx.simulate_keystrokes("d s b");
1646 cx.assert_state(
1647 indoc! {"
1648 The ˇquick brown
1649 fox jumps over
1650 the lazy dog."},
1651 Mode::Normal,
1652 );
1653
1654 cx.set_state(
1655 indoc! {"
1656 The [quˇick] brown
1657 fox jumps over
1658 the lazy dog."},
1659 Mode::Normal,
1660 );
1661 cx.simulate_keystrokes("d s r");
1662 cx.assert_state(
1663 indoc! {"
1664 The ˇquick brown
1665 fox jumps over
1666 the lazy dog."},
1667 Mode::Normal,
1668 );
1669
1670 cx.set_state(
1671 indoc! {"
1672 The <quˇick> brown
1673 fox jumps over
1674 the lazy dog."},
1675 Mode::Normal,
1676 );
1677 cx.simulate_keystrokes("d s a");
1678 cx.assert_state(
1679 indoc! {"
1680 The ˇquick brown
1681 fox jumps over
1682 the lazy dog."},
1683 Mode::Normal,
1684 );
1685 }
1686}