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