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