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