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, true, 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, 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, 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, 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 true,
538 false,
539 open,
540 close,
541 ) {
542 let start_offset = range.start.to_offset(&display_map, Bias::Left);
543 let end_offset = range.end.to_offset(&display_map, Bias::Right);
544
545 if cursor_offset >= start_offset && cursor_offset <= end_offset {
546 let size = end_offset - start_offset;
547 if size < min_range_size {
548 min_range_size = size;
549 bracket_pair = Some(BracketPair {
550 start: open.to_string(),
551 end: close.to_string(),
552 close: true,
553 surround: true,
554 newline: false,
555 })
556 }
557 }
558 }
559 }
560 }
561 });
562
563 bracket_pair
564 }
565 _ => None,
566 }
567 }
568}
569
570fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
571 pairs
572 .iter()
573 .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
574}
575
576fn surround_alias(ch: &str) -> &str {
577 match ch {
578 "b" => ")",
579 "B" => "}",
580 "a" => ">",
581 "r" => "]",
582 _ => ch,
583 }
584}
585
586fn all_support_surround_pair() -> Vec<BracketPair> {
587 vec![
588 BracketPair {
589 start: "{".into(),
590 end: "}".into(),
591 close: true,
592 surround: true,
593 newline: false,
594 },
595 BracketPair {
596 start: "'".into(),
597 end: "'".into(),
598 close: true,
599 surround: true,
600 newline: false,
601 },
602 BracketPair {
603 start: "`".into(),
604 end: "`".into(),
605 close: true,
606 surround: true,
607 newline: false,
608 },
609 BracketPair {
610 start: "\"".into(),
611 end: "\"".into(),
612 close: true,
613 surround: true,
614 newline: false,
615 },
616 BracketPair {
617 start: "(".into(),
618 end: ")".into(),
619 close: true,
620 surround: true,
621 newline: false,
622 },
623 BracketPair {
624 start: "|".into(),
625 end: "|".into(),
626 close: true,
627 surround: true,
628 newline: false,
629 },
630 BracketPair {
631 start: "[".into(),
632 end: "]".into(),
633 close: true,
634 surround: true,
635 newline: false,
636 },
637 BracketPair {
638 start: "<".into(),
639 end: ">".into(),
640 close: true,
641 surround: true,
642 newline: false,
643 },
644 ]
645}
646
647fn pair_to_object(pair: &BracketPair) -> Option<Object> {
648 match pair.start.as_str() {
649 "'" => Some(Object::Quotes),
650 "`" => Some(Object::BackQuotes),
651 "\"" => Some(Object::DoubleQuotes),
652 "|" => Some(Object::VerticalBars),
653 "(" => Some(Object::Parentheses),
654 "[" => Some(Object::SquareBrackets),
655 "{" => Some(Object::CurlyBrackets),
656 "<" => Some(Object::AngleBrackets),
657 _ => None,
658 }
659}
660
661#[cfg(test)]
662mod test {
663 use gpui::KeyBinding;
664 use indoc::indoc;
665
666 use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
667
668 #[gpui::test]
669 async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
670 let mut cx = VimTestContext::new(cx, true).await;
671
672 // test add surrounds with around
673 cx.set_state(
674 indoc! {"
675 The quˇick brown
676 fox jumps over
677 the lazy dog."},
678 Mode::Normal,
679 );
680 cx.simulate_keystrokes("y s i w {");
681 cx.assert_state(
682 indoc! {"
683 The ˇ{ quick } brown
684 fox jumps over
685 the lazy dog."},
686 Mode::Normal,
687 );
688
689 // test add surrounds not with around
690 cx.set_state(
691 indoc! {"
692 The quˇick brown
693 fox jumps over
694 the lazy dog."},
695 Mode::Normal,
696 );
697 cx.simulate_keystrokes("y s i w }");
698 cx.assert_state(
699 indoc! {"
700 The ˇ{quick} brown
701 fox jumps over
702 the lazy dog."},
703 Mode::Normal,
704 );
705
706 // test add surrounds with motion
707 cx.set_state(
708 indoc! {"
709 The quˇick brown
710 fox jumps over
711 the lazy dog."},
712 Mode::Normal,
713 );
714 cx.simulate_keystrokes("y s $ }");
715 cx.assert_state(
716 indoc! {"
717 The quˇ{ick brown}
718 fox jumps over
719 the lazy dog."},
720 Mode::Normal,
721 );
722
723 // test add surrounds with multi cursor
724 cx.set_state(
725 indoc! {"
726 The quˇick brown
727 fox jumps over
728 the laˇzy dog."},
729 Mode::Normal,
730 );
731 cx.simulate_keystrokes("y s i w '");
732 cx.assert_state(
733 indoc! {"
734 The ˇ'quick' brown
735 fox jumps over
736 the ˇ'lazy' dog."},
737 Mode::Normal,
738 );
739
740 // test multi cursor add surrounds with motion
741 cx.set_state(
742 indoc! {"
743 The quˇick brown
744 fox jumps over
745 the laˇzy dog."},
746 Mode::Normal,
747 );
748 cx.simulate_keystrokes("y s $ '");
749 cx.assert_state(
750 indoc! {"
751 The quˇ'ick brown'
752 fox jumps over
753 the laˇ'zy dog.'"},
754 Mode::Normal,
755 );
756
757 // test multi cursor add surrounds with motion and custom string
758 cx.set_state(
759 indoc! {"
760 The quˇick brown
761 fox jumps over
762 the laˇzy dog."},
763 Mode::Normal,
764 );
765 cx.simulate_keystrokes("y s $ 1");
766 cx.assert_state(
767 indoc! {"
768 The quˇ1ick brown1
769 fox jumps over
770 the laˇ1zy dog.1"},
771 Mode::Normal,
772 );
773
774 // test add surrounds with motion current line
775 cx.set_state(
776 indoc! {"
777 The quˇick brown
778 fox jumps over
779 the lazy dog."},
780 Mode::Normal,
781 );
782 cx.simulate_keystrokes("y s s {");
783 cx.assert_state(
784 indoc! {"
785 ˇ{ The quick brown }
786 fox jumps over
787 the lazy dog."},
788 Mode::Normal,
789 );
790
791 cx.set_state(
792 indoc! {"
793 The quˇick brown•
794 fox jumps over
795 the lazy dog."},
796 Mode::Normal,
797 );
798 cx.simulate_keystrokes("y s s {");
799 cx.assert_state(
800 indoc! {"
801 ˇ{ The quick brown }•
802 fox jumps over
803 the lazy dog."},
804 Mode::Normal,
805 );
806 cx.simulate_keystrokes("2 y s s )");
807 cx.assert_state(
808 indoc! {"
809 ˇ({ The quick brown }•
810 fox jumps over)
811 the lazy dog."},
812 Mode::Normal,
813 );
814
815 // test add surrounds around object
816 cx.set_state(
817 indoc! {"
818 The [quˇick] brown
819 fox jumps over
820 the lazy dog."},
821 Mode::Normal,
822 );
823 cx.simulate_keystrokes("y s a ] )");
824 cx.assert_state(
825 indoc! {"
826 The ˇ([quick]) brown
827 fox jumps over
828 the lazy dog."},
829 Mode::Normal,
830 );
831
832 // test add surrounds inside object
833 cx.set_state(
834 indoc! {"
835 The [quˇick] brown
836 fox jumps over
837 the lazy dog."},
838 Mode::Normal,
839 );
840 cx.simulate_keystrokes("y s i ] )");
841 cx.assert_state(
842 indoc! {"
843 The [ˇ(quick)] brown
844 fox jumps over
845 the lazy dog."},
846 Mode::Normal,
847 );
848 }
849
850 #[gpui::test]
851 async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
852 let mut cx = VimTestContext::new(cx, true).await;
853
854 cx.update(|_, cx| {
855 cx.bind_keys([KeyBinding::new(
856 "shift-s",
857 PushAddSurrounds {},
858 Some("vim_mode == visual"),
859 )])
860 });
861
862 // test add surrounds with around
863 cx.set_state(
864 indoc! {"
865 The quˇick brown
866 fox jumps over
867 the lazy dog."},
868 Mode::Normal,
869 );
870 cx.simulate_keystrokes("v i w shift-s {");
871 cx.assert_state(
872 indoc! {"
873 The ˇ{ quick } brown
874 fox jumps over
875 the lazy dog."},
876 Mode::Normal,
877 );
878
879 // test add surrounds not with around
880 cx.set_state(
881 indoc! {"
882 The quˇick brown
883 fox jumps over
884 the lazy dog."},
885 Mode::Normal,
886 );
887 cx.simulate_keystrokes("v i w shift-s }");
888 cx.assert_state(
889 indoc! {"
890 The ˇ{quick} brown
891 fox jumps over
892 the lazy dog."},
893 Mode::Normal,
894 );
895
896 // test add surrounds with motion
897 cx.set_state(
898 indoc! {"
899 The quˇick brown
900 fox jumps over
901 the lazy dog."},
902 Mode::Normal,
903 );
904 cx.simulate_keystrokes("v e shift-s }");
905 cx.assert_state(
906 indoc! {"
907 The quˇ{ick} brown
908 fox jumps over
909 the lazy dog."},
910 Mode::Normal,
911 );
912
913 // test add surrounds with multi cursor
914 cx.set_state(
915 indoc! {"
916 The quˇick brown
917 fox jumps over
918 the laˇzy dog."},
919 Mode::Normal,
920 );
921 cx.simulate_keystrokes("v i w shift-s '");
922 cx.assert_state(
923 indoc! {"
924 The ˇ'quick' brown
925 fox jumps over
926 the ˇ'lazy' dog."},
927 Mode::Normal,
928 );
929
930 // test add surrounds with visual block
931 cx.set_state(
932 indoc! {"
933 The quˇick brown
934 fox jumps over
935 the lazy dog."},
936 Mode::Normal,
937 );
938 cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
939 cx.assert_state(
940 indoc! {"
941 The ˇ'quick' brown
942 fox 'jumps' over
943 the 'lazy 'dog."},
944 Mode::Normal,
945 );
946
947 // test add surrounds with visual line
948 cx.set_state(
949 indoc! {"
950 The quˇick brown
951 fox jumps over
952 the lazy dog."},
953 Mode::Normal,
954 );
955 cx.simulate_keystrokes("j shift-v shift-s '");
956 cx.assert_state(
957 indoc! {"
958 The quick brown
959 ˇ'
960 fox jumps over
961 '
962 the lazy dog."},
963 Mode::Normal,
964 );
965 }
966
967 #[gpui::test]
968 async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
969 let mut cx = VimTestContext::new(cx, true).await;
970
971 // test delete surround
972 cx.set_state(
973 indoc! {"
974 The {quˇick} brown
975 fox jumps over
976 the lazy dog."},
977 Mode::Normal,
978 );
979 cx.simulate_keystrokes("d s {");
980 cx.assert_state(
981 indoc! {"
982 The ˇquick brown
983 fox jumps over
984 the lazy dog."},
985 Mode::Normal,
986 );
987
988 // test delete not exist surrounds
989 cx.set_state(
990 indoc! {"
991 The {quˇick} brown
992 fox jumps over
993 the lazy dog."},
994 Mode::Normal,
995 );
996 cx.simulate_keystrokes("d s [");
997 cx.assert_state(
998 indoc! {"
999 The {quˇick} brown
1000 fox jumps over
1001 the lazy dog."},
1002 Mode::Normal,
1003 );
1004
1005 // test delete surround forward exist, in the surrounds plugin of other editors,
1006 // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
1007 cx.set_state(
1008 indoc! {"
1009 The {quick} brˇown
1010 fox jumps over
1011 the lazy dog."},
1012 Mode::Normal,
1013 );
1014 cx.simulate_keystrokes("d s {");
1015 cx.assert_state(
1016 indoc! {"
1017 The {quick} brˇown
1018 fox jumps over
1019 the lazy dog."},
1020 Mode::Normal,
1021 );
1022
1023 // test cursor delete inner surrounds
1024 cx.set_state(
1025 indoc! {"
1026 The { quick brown
1027 fox jumˇps over }
1028 the lazy dog."},
1029 Mode::Normal,
1030 );
1031 cx.simulate_keystrokes("d s {");
1032 cx.assert_state(
1033 indoc! {"
1034 The ˇquick brown
1035 fox jumps over
1036 the lazy dog."},
1037 Mode::Normal,
1038 );
1039
1040 // test multi cursor delete surrounds
1041 cx.set_state(
1042 indoc! {"
1043 The [quˇick] brown
1044 fox jumps over
1045 the [laˇzy] dog."},
1046 Mode::Normal,
1047 );
1048 cx.simulate_keystrokes("d s ]");
1049 cx.assert_state(
1050 indoc! {"
1051 The ˇquick brown
1052 fox jumps over
1053 the ˇlazy dog."},
1054 Mode::Normal,
1055 );
1056
1057 // test multi cursor delete surrounds with around
1058 cx.set_state(
1059 indoc! {"
1060 Tˇhe [ quick ] brown
1061 fox jumps over
1062 the [laˇzy] dog."},
1063 Mode::Normal,
1064 );
1065 cx.simulate_keystrokes("d s [");
1066 cx.assert_state(
1067 indoc! {"
1068 The ˇquick brown
1069 fox jumps over
1070 the ˇlazy dog."},
1071 Mode::Normal,
1072 );
1073
1074 cx.set_state(
1075 indoc! {"
1076 Tˇhe [ quick ] brown
1077 fox jumps over
1078 the [laˇzy ] dog."},
1079 Mode::Normal,
1080 );
1081 cx.simulate_keystrokes("d s [");
1082 cx.assert_state(
1083 indoc! {"
1084 The ˇquick brown
1085 fox jumps over
1086 the ˇlazy dog."},
1087 Mode::Normal,
1088 );
1089
1090 // test multi cursor delete different surrounds
1091 // the pair corresponding to the two cursors is the same,
1092 // so they are combined into one cursor
1093 cx.set_state(
1094 indoc! {"
1095 The [quˇick] brown
1096 fox jumps over
1097 the {laˇzy} dog."},
1098 Mode::Normal,
1099 );
1100 cx.simulate_keystrokes("d s {");
1101 cx.assert_state(
1102 indoc! {"
1103 The [quick] brown
1104 fox jumps over
1105 the ˇlazy dog."},
1106 Mode::Normal,
1107 );
1108
1109 // test delete surround with multi cursor and nest surrounds
1110 cx.set_state(
1111 indoc! {"
1112 fn test_surround() {
1113 ifˇ 2 > 1 {
1114 ˇprintln!(\"it is fine\");
1115 };
1116 }"},
1117 Mode::Normal,
1118 );
1119 cx.simulate_keystrokes("d s }");
1120 cx.assert_state(
1121 indoc! {"
1122 fn test_surround() ˇ
1123 if 2 > 1 ˇ
1124 println!(\"it is fine\");
1125 ;
1126 "},
1127 Mode::Normal,
1128 );
1129 }
1130
1131 #[gpui::test]
1132 async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1133 let mut cx = VimTestContext::new(cx, true).await;
1134
1135 cx.set_state(
1136 indoc! {"
1137 The {quˇick} brown
1138 fox jumps over
1139 the lazy dog."},
1140 Mode::Normal,
1141 );
1142 cx.simulate_keystrokes("c s { [");
1143 cx.assert_state(
1144 indoc! {"
1145 The ˇ[ quick ] brown
1146 fox jumps over
1147 the lazy dog."},
1148 Mode::Normal,
1149 );
1150
1151 // test multi cursor change surrounds
1152 cx.set_state(
1153 indoc! {"
1154 The {quˇick} brown
1155 fox jumps over
1156 the {laˇzy} dog."},
1157 Mode::Normal,
1158 );
1159 cx.simulate_keystrokes("c s { [");
1160 cx.assert_state(
1161 indoc! {"
1162 The ˇ[ quick ] brown
1163 fox jumps over
1164 the ˇ[ lazy ] dog."},
1165 Mode::Normal,
1166 );
1167
1168 // test multi cursor delete different surrounds with after cursor
1169 cx.set_state(
1170 indoc! {"
1171 Thˇe {quick} brown
1172 fox jumps over
1173 the {laˇzy} dog."},
1174 Mode::Normal,
1175 );
1176 cx.simulate_keystrokes("c s { [");
1177 cx.assert_state(
1178 indoc! {"
1179 The ˇ[ quick ] brown
1180 fox jumps over
1181 the ˇ[ lazy ] dog."},
1182 Mode::Normal,
1183 );
1184
1185 // test multi cursor change surrount with not around
1186 cx.set_state(
1187 indoc! {"
1188 Thˇe { quick } brown
1189 fox jumps over
1190 the {laˇzy} dog."},
1191 Mode::Normal,
1192 );
1193 cx.simulate_keystrokes("c s { ]");
1194 cx.assert_state(
1195 indoc! {"
1196 The ˇ[quick] brown
1197 fox jumps over
1198 the ˇ[lazy] dog."},
1199 Mode::Normal,
1200 );
1201
1202 // test multi cursor change with not exist surround
1203 cx.set_state(
1204 indoc! {"
1205 The {quˇick} brown
1206 fox jumps over
1207 the [laˇzy] dog."},
1208 Mode::Normal,
1209 );
1210 cx.simulate_keystrokes("c s [ '");
1211 cx.assert_state(
1212 indoc! {"
1213 The {quick} brown
1214 fox jumps over
1215 the ˇ'lazy' dog."},
1216 Mode::Normal,
1217 );
1218
1219 // test change nesting surrounds
1220 cx.set_state(
1221 indoc! {"
1222 fn test_surround() {
1223 ifˇ 2 > 1 {
1224 ˇprintln!(\"it is fine\");
1225 }
1226 };"},
1227 Mode::Normal,
1228 );
1229 cx.simulate_keystrokes("c s } ]");
1230 cx.assert_state(
1231 indoc! {"
1232 fn test_surround() ˇ[
1233 if 2 > 1 ˇ[
1234 println!(\"it is fine\");
1235 ]
1236 ];"},
1237 Mode::Normal,
1238 );
1239
1240 // Currently, the same test case but using the closing bracket `]`
1241 // actually removes a whitespace before the closing bracket, something
1242 // that might need to be fixed?
1243 cx.set_state(
1244 indoc! {"
1245 fn test_surround() {
1246 ifˇ 2 > 1 {
1247 ˇprintln!(\"it is fine\");
1248 }
1249 };"},
1250 Mode::Normal,
1251 );
1252 cx.simulate_keystrokes("c s { ]");
1253 cx.assert_state(
1254 indoc! {"
1255 fn test_surround() ˇ[
1256 if 2 > 1 ˇ[
1257 println!(\"it is fine\");
1258 ]
1259 ];"},
1260 Mode::Normal,
1261 );
1262
1263 // test change quotes.
1264 cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
1265 cx.simulate_keystrokes("c s ' \"");
1266 cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
1267
1268 // test multi cursor change quotes
1269 cx.set_state(
1270 indoc! {"
1271 ' ˇstr '
1272 some example text here
1273 ˇ' str '
1274 "},
1275 Mode::Normal,
1276 );
1277 cx.simulate_keystrokes("c s ' \"");
1278 cx.assert_state(
1279 indoc! {"
1280 ˇ\" str \"
1281 some example text here
1282 ˇ\" str \"
1283 "},
1284 Mode::Normal,
1285 );
1286
1287 // test quote to bracket spacing.
1288 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1289 cx.simulate_keystrokes("c s ' {");
1290 cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1291
1292 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1293 cx.simulate_keystrokes("c s ' }");
1294 cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1295 }
1296
1297 #[gpui::test]
1298 async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1299 let mut cx = VimTestContext::new(cx, true).await;
1300
1301 // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1302 // action.
1303 cx.update(|_, cx| {
1304 cx.bind_keys([KeyBinding::new(
1305 "b",
1306 AnyBrackets,
1307 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1308 )]);
1309 });
1310
1311 cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1312 cx.simulate_keystrokes("c s b [");
1313 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1314
1315 cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1316 cx.simulate_keystrokes("c s b {");
1317 cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1318
1319 cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1320 cx.simulate_keystrokes("c s b [");
1321 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1322
1323 cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1324 cx.simulate_keystrokes("c s b [");
1325 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1326
1327 cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1328 cx.simulate_keystrokes("c s b }");
1329 cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1330
1331 cx.set_state(
1332 indoc! {"
1333 (< name: ˇ'Zed' >)
1334 (< nˇame: 'DeltaDB' >)
1335 "},
1336 Mode::Normal,
1337 );
1338 cx.simulate_keystrokes("c s b {");
1339 cx.set_state(
1340 indoc! {"
1341 (ˇ{ name: 'Zed' })
1342 (ˇ{ name: 'DeltaDB' })
1343 "},
1344 Mode::Normal,
1345 );
1346 }
1347
1348 // The following test cases all follow tpope/vim-surround's behaviour
1349 // and are more focused on how whitespace is handled.
1350 #[gpui::test]
1351 async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
1352 let mut cx = VimTestContext::new(cx, true).await;
1353
1354 // Changing quote to quote should never change the surrounding
1355 // whitespace.
1356 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1357 cx.simulate_keystrokes("c s ' \"");
1358 cx.assert_state(indoc! {"ˇ\" a \""}, Mode::Normal);
1359
1360 cx.set_state(indoc! {"\" ˇa \""}, Mode::Normal);
1361 cx.simulate_keystrokes("c s \" '");
1362 cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal);
1363
1364 // Changing quote to bracket adds one more space when the opening
1365 // bracket is used, does not affect whitespace when the closing bracket
1366 // is used.
1367 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1368 cx.simulate_keystrokes("c s ' {");
1369 cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal);
1370
1371 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1372 cx.simulate_keystrokes("c s ' }");
1373 cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal);
1374
1375 // Changing bracket to quote should remove all space when the
1376 // opening bracket is used and preserve all space when the
1377 // closing one is used.
1378 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1379 cx.simulate_keystrokes("c s { '");
1380 cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);
1381
1382 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1383 cx.simulate_keystrokes("c s } '");
1384 cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal);
1385
1386 // Changing bracket to bracket follows these rules:
1387 // * opening → opening – keeps only one space.
1388 // * opening → closing – removes all space.
1389 // * closing → opening – adds one space.
1390 // * closing → closing – does not change space.
1391 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1392 cx.simulate_keystrokes("c s { [");
1393 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1394
1395 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1396 cx.simulate_keystrokes("c s { ]");
1397 cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);
1398
1399 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1400 cx.simulate_keystrokes("c s } [");
1401 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1402
1403 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1404 cx.simulate_keystrokes("c s } ]");
1405 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1406 }
1407
1408 #[gpui::test]
1409 async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1410 let mut cx = VimTestContext::new(cx, true).await;
1411
1412 cx.set_state(
1413 indoc! {"
1414 The quˇick brown
1415 fox jumps over
1416 the lazy dog."},
1417 Mode::Normal,
1418 );
1419 cx.simulate_keystrokes("y s i w [");
1420 cx.assert_state(
1421 indoc! {"
1422 The ˇ[ quick ] brown
1423 fox jumps over
1424 the lazy dog."},
1425 Mode::Normal,
1426 );
1427
1428 cx.simulate_keystrokes("c s [ }");
1429 cx.assert_state(
1430 indoc! {"
1431 The ˇ{quick} brown
1432 fox jumps over
1433 the lazy dog."},
1434 Mode::Normal,
1435 );
1436
1437 cx.simulate_keystrokes("d s {");
1438 cx.assert_state(
1439 indoc! {"
1440 The ˇquick brown
1441 fox jumps over
1442 the lazy dog."},
1443 Mode::Normal,
1444 );
1445
1446 cx.simulate_keystrokes("u");
1447 cx.assert_state(
1448 indoc! {"
1449 The ˇ{quick} brown
1450 fox jumps over
1451 the lazy dog."},
1452 Mode::Normal,
1453 );
1454 }
1455
1456 #[gpui::test]
1457 async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1458 let mut cx = VimTestContext::new(cx, true).await;
1459
1460 // add aliases
1461 cx.set_state(
1462 indoc! {"
1463 The quˇick brown
1464 fox jumps over
1465 the lazy dog."},
1466 Mode::Normal,
1467 );
1468 cx.simulate_keystrokes("y s i w b");
1469 cx.assert_state(
1470 indoc! {"
1471 The ˇ(quick) brown
1472 fox jumps over
1473 the lazy dog."},
1474 Mode::Normal,
1475 );
1476
1477 cx.set_state(
1478 indoc! {"
1479 The quˇick brown
1480 fox jumps over
1481 the lazy dog."},
1482 Mode::Normal,
1483 );
1484 cx.simulate_keystrokes("y s i w B");
1485 cx.assert_state(
1486 indoc! {"
1487 The ˇ{quick} brown
1488 fox jumps over
1489 the lazy dog."},
1490 Mode::Normal,
1491 );
1492
1493 cx.set_state(
1494 indoc! {"
1495 The quˇick brown
1496 fox jumps over
1497 the lazy dog."},
1498 Mode::Normal,
1499 );
1500 cx.simulate_keystrokes("y s i w a");
1501 cx.assert_state(
1502 indoc! {"
1503 The ˇ<quick> brown
1504 fox jumps over
1505 the lazy dog."},
1506 Mode::Normal,
1507 );
1508
1509 cx.set_state(
1510 indoc! {"
1511 The quˇick brown
1512 fox jumps over
1513 the lazy dog."},
1514 Mode::Normal,
1515 );
1516 cx.simulate_keystrokes("y s i w r");
1517 cx.assert_state(
1518 indoc! {"
1519 The ˇ[quick] brown
1520 fox jumps over
1521 the lazy dog."},
1522 Mode::Normal,
1523 );
1524
1525 // change aliases
1526 cx.set_state(
1527 indoc! {"
1528 The {quˇick} brown
1529 fox jumps over
1530 the lazy dog."},
1531 Mode::Normal,
1532 );
1533 cx.simulate_keystrokes("c s { b");
1534 cx.assert_state(
1535 indoc! {"
1536 The ˇ(quick) brown
1537 fox jumps over
1538 the lazy dog."},
1539 Mode::Normal,
1540 );
1541
1542 cx.set_state(
1543 indoc! {"
1544 The (quˇick) brown
1545 fox jumps over
1546 the lazy dog."},
1547 Mode::Normal,
1548 );
1549 cx.simulate_keystrokes("c s ( B");
1550 cx.assert_state(
1551 indoc! {"
1552 The ˇ{quick} brown
1553 fox jumps over
1554 the lazy dog."},
1555 Mode::Normal,
1556 );
1557
1558 cx.set_state(
1559 indoc! {"
1560 The (quˇick) brown
1561 fox jumps over
1562 the lazy dog."},
1563 Mode::Normal,
1564 );
1565 cx.simulate_keystrokes("c s ( a");
1566 cx.assert_state(
1567 indoc! {"
1568 The ˇ<quick> brown
1569 fox jumps over
1570 the lazy dog."},
1571 Mode::Normal,
1572 );
1573
1574 cx.set_state(
1575 indoc! {"
1576 The <quˇick> brown
1577 fox jumps over
1578 the lazy dog."},
1579 Mode::Normal,
1580 );
1581 cx.simulate_keystrokes("c s < b");
1582 cx.assert_state(
1583 indoc! {"
1584 The ˇ(quick) brown
1585 fox jumps over
1586 the lazy dog."},
1587 Mode::Normal,
1588 );
1589
1590 cx.set_state(
1591 indoc! {"
1592 The (quˇick) brown
1593 fox jumps over
1594 the lazy dog."},
1595 Mode::Normal,
1596 );
1597 cx.simulate_keystrokes("c s ( r");
1598 cx.assert_state(
1599 indoc! {"
1600 The ˇ[quick] brown
1601 fox jumps over
1602 the lazy dog."},
1603 Mode::Normal,
1604 );
1605
1606 cx.set_state(
1607 indoc! {"
1608 The [quˇick] brown
1609 fox jumps over
1610 the lazy dog."},
1611 Mode::Normal,
1612 );
1613 cx.simulate_keystrokes("c s [ b");
1614 cx.assert_state(
1615 indoc! {"
1616 The ˇ(quick) brown
1617 fox jumps over
1618 the lazy dog."},
1619 Mode::Normal,
1620 );
1621
1622 // delete alias
1623 cx.set_state(
1624 indoc! {"
1625 The {quˇick} brown
1626 fox jumps over
1627 the lazy dog."},
1628 Mode::Normal,
1629 );
1630 cx.simulate_keystrokes("d s B");
1631 cx.assert_state(
1632 indoc! {"
1633 The ˇquick brown
1634 fox jumps over
1635 the lazy dog."},
1636 Mode::Normal,
1637 );
1638
1639 cx.set_state(
1640 indoc! {"
1641 The (quˇick) brown
1642 fox jumps over
1643 the lazy dog."},
1644 Mode::Normal,
1645 );
1646 cx.simulate_keystrokes("d s b");
1647 cx.assert_state(
1648 indoc! {"
1649 The ˇquick brown
1650 fox jumps over
1651 the lazy dog."},
1652 Mode::Normal,
1653 );
1654
1655 cx.set_state(
1656 indoc! {"
1657 The [quˇick] brown
1658 fox jumps over
1659 the lazy dog."},
1660 Mode::Normal,
1661 );
1662 cx.simulate_keystrokes("d s r");
1663 cx.assert_state(
1664 indoc! {"
1665 The ˇquick brown
1666 fox jumps over
1667 the lazy dog."},
1668 Mode::Normal,
1669 );
1670
1671 cx.set_state(
1672 indoc! {"
1673 The <quˇick> brown
1674 fox jumps over
1675 the lazy dog."},
1676 Mode::Normal,
1677 );
1678 cx.simulate_keystrokes("d s a");
1679 cx.assert_state(
1680 indoc! {"
1681 The ˇquick brown
1682 fox jumps over
1683 the lazy dog."},
1684 Mode::Normal,
1685 );
1686 }
1687}