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