1use crate::{motion::Motion, object::Object, state::Mode, Vim};
2use editor::{scroll::Autoscroll, Bias};
3use gpui::WindowContext;
4use language::BracketPair;
5use serde::Deserialize;
6use std::sync::Arc;
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum SurroundsType {
9 Motion(Motion),
10 Object(Object),
11}
12
13// This exists so that we can have Deserialize on Operators, but not on Motions.
14impl<'de> Deserialize<'de> for SurroundsType {
15 fn deserialize<D>(_: D) -> Result<Self, D::Error>
16 where
17 D: serde::Deserializer<'de>,
18 {
19 Err(serde::de::Error::custom("Cannot deserialize SurroundsType"))
20 }
21}
22
23pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
24 Vim::update(cx, |vim, cx| {
25 vim.stop_recording();
26 vim.update_active_editor(cx, |_, editor, cx| {
27 let text_layout_details = editor.text_layout_details(cx);
28 editor.transact(cx, |editor, cx| {
29 editor.set_clip_at_line_ends(false, cx);
30
31 let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
32 Some(pair) => pair.clone(),
33 None => BracketPair {
34 start: text.to_string(),
35 end: text.to_string(),
36 close: true,
37 newline: false,
38 },
39 };
40 let surround = pair.end != *text;
41 let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
42 let mut edits = Vec::new();
43 let mut anchors = Vec::new();
44
45 for selection in &display_selections {
46 let range = match &target {
47 SurroundsType::Object(object) => {
48 object.range(&display_map, selection.clone(), false)
49 }
50 SurroundsType::Motion(motion) => motion.range(
51 &display_map,
52 selection.clone(),
53 Some(1),
54 true,
55 &text_layout_details,
56 ),
57 };
58
59 if let Some(range) = range {
60 let start = range.start.to_offset(&display_map, Bias::Right);
61 let end = range.end.to_offset(&display_map, Bias::Left);
62 let start_cursor_str =
63 format!("{}{}", pair.start, if surround { " " } else { "" });
64 let close_cursor_str =
65 format!("{}{}", if surround { " " } else { "" }, pair.end);
66 let start_anchor = display_map.buffer_snapshot.anchor_before(start);
67
68 edits.push((start..start, start_cursor_str));
69 edits.push((end..end, close_cursor_str));
70 anchors.push(start_anchor..start_anchor);
71 } else {
72 let start_anchor = display_map
73 .buffer_snapshot
74 .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
75 anchors.push(start_anchor..start_anchor);
76 }
77 }
78
79 editor.buffer().update(cx, |buffer, cx| {
80 buffer.edit(edits, None, cx);
81 });
82 editor.set_clip_at_line_ends(true, cx);
83 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
84 s.select_anchor_ranges(anchors)
85 });
86 });
87 });
88 vim.switch_mode(Mode::Normal, false, cx);
89 });
90}
91
92pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
93 Vim::update(cx, |vim, cx| {
94 vim.stop_recording();
95
96 // only legitimate surrounds can be removed
97 let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
98 Some(pair) => pair.clone(),
99 None => return,
100 };
101 let pair_object = match pair_to_object(&pair) {
102 Some(pair_object) => pair_object,
103 None => return,
104 };
105 let surround = pair.end != *text;
106
107 vim.update_active_editor(cx, |_, editor, cx| {
108 editor.transact(cx, |editor, cx| {
109 editor.set_clip_at_line_ends(false, cx);
110
111 let (display_map, display_selections) = editor.selections.all_display(cx);
112 let mut edits = Vec::new();
113 let mut anchors = Vec::new();
114
115 for selection in &display_selections {
116 let start = selection.start.to_offset(&display_map, Bias::Left);
117 if let Some(range) = pair_object.range(&display_map, selection.clone(), true) {
118 // If the current parenthesis object is single-line,
119 // then we need to filter whether it is the current line or not
120 if !pair_object.is_multiline() {
121 let is_same_row = selection.start.row() == range.start.row()
122 && selection.end.row() == range.end.row();
123 if !is_same_row {
124 anchors.push(start..start);
125 continue;
126 }
127 }
128 // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
129 // hello«ˇ "hello in a word" »again.
130 // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
131 // In order to be able to accurately match and replace in this case, some cumbersome methods are used
132 let mut chars_and_offset = display_map
133 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
134 .peekable();
135 while let Some((ch, offset)) = chars_and_offset.next() {
136 if ch.to_string() == pair.start {
137 let start = offset;
138 let mut end = start + 1;
139 if surround {
140 if let Some((next_ch, _)) = chars_and_offset.peek() {
141 if next_ch.eq(&' ') {
142 end += 1;
143 }
144 }
145 }
146 edits.push((start..end, ""));
147 anchors.push(start..start);
148 break;
149 }
150 }
151 let mut reverse_chars_and_offsets = display_map
152 .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
153 .peekable();
154 while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
155 if ch.to_string() == pair.end {
156 let mut start = offset;
157 let end = start + 1;
158 if surround {
159 if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
160 if next_ch.eq(&' ') {
161 start -= 1;
162 }
163 }
164 }
165 edits.push((start..end, ""));
166 break;
167 }
168 }
169 } else {
170 anchors.push(start..start);
171 }
172 }
173
174 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
175 s.select_ranges(anchors);
176 });
177 edits.sort_by_key(|(range, _)| range.start);
178 editor.buffer().update(cx, |buffer, cx| {
179 buffer.edit(edits, None, cx);
180 });
181 editor.set_clip_at_line_ends(true, cx);
182 });
183 });
184 });
185}
186
187pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext) {
188 if let Some(will_replace_pair) = object_to_bracket_pair(target) {
189 Vim::update(cx, |vim, cx| {
190 vim.stop_recording();
191 vim.update_active_editor(cx, |_, editor, cx| {
192 editor.transact(cx, |editor, cx| {
193 editor.set_clip_at_line_ends(false, cx);
194
195 let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
196 Some(pair) => pair.clone(),
197 None => BracketPair {
198 start: text.to_string(),
199 end: text.to_string(),
200 close: true,
201 newline: false,
202 },
203 };
204 let surround = pair.end != *text;
205 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
206 let mut edits = Vec::new();
207 let mut anchors = Vec::new();
208
209 for selection in &selections {
210 let start = selection.start.to_offset(&display_map, Bias::Left);
211 if let Some(range) = target.range(&display_map, selection.clone(), true) {
212 if !target.is_multiline() {
213 let is_same_row = selection.start.row() == range.start.row()
214 && selection.end.row() == range.end.row();
215 if !is_same_row {
216 anchors.push(start..start);
217 continue;
218 }
219 }
220 let mut chars_and_offset = display_map
221 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
222 .peekable();
223 while let Some((ch, offset)) = chars_and_offset.next() {
224 if ch.to_string() == will_replace_pair.start {
225 let mut open_str = pair.start.clone();
226 let start = offset;
227 let mut end = start + 1;
228 match chars_and_offset.peek() {
229 Some((next_ch, _)) => {
230 // If the next position is already a space or line break,
231 // we don't need to splice another space even under arround
232 if surround && !next_ch.is_whitespace() {
233 open_str.push_str(" ");
234 } else if !surround && next_ch.to_string() == " " {
235 end += 1;
236 }
237 }
238 None => {}
239 }
240 edits.push((start..end, open_str));
241 anchors.push(start..start);
242 break;
243 }
244 }
245
246 let mut reverse_chars_and_offsets = display_map
247 .reverse_buffer_chars_at(
248 range.end.to_offset(&display_map, Bias::Left),
249 )
250 .peekable();
251 while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
252 if ch.to_string() == will_replace_pair.end {
253 let mut close_str = pair.end.clone();
254 let mut start = offset;
255 let end = start + 1;
256 if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
257 if surround && !next_ch.is_whitespace() {
258 close_str.insert_str(0, " ")
259 } else if !surround && next_ch.to_string() == " " {
260 start -= 1;
261 }
262 }
263 edits.push((start..end, close_str));
264 break;
265 }
266 }
267 } else {
268 anchors.push(start..start);
269 }
270 }
271
272 let stable_anchors = editor
273 .selections
274 .disjoint_anchors()
275 .into_iter()
276 .map(|selection| {
277 let start = selection.start.bias_left(&display_map.buffer_snapshot);
278 start..start
279 })
280 .collect::<Vec<_>>();
281 edits.sort_by_key(|(range, _)| range.start);
282 editor.buffer().update(cx, |buffer, cx| {
283 buffer.edit(edits, None, cx);
284 });
285 editor.set_clip_at_line_ends(true, cx);
286 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
287 s.select_anchor_ranges(stable_anchors);
288 });
289 });
290 });
291 });
292 }
293}
294
295/// Checks if any of the current cursors are surrounded by a valid pair of brackets.
296///
297/// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
298/// A pair of brackets is considered valid if it is well-formed and properly closed.
299///
300/// 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.
301/// If no valid pair of brackets is found for any cursor, the method returns `false`.
302pub fn check_and_move_to_valid_bracket_pair(
303 vim: &mut Vim,
304 object: Object,
305 cx: &mut WindowContext,
306) -> bool {
307 let mut valid = false;
308 if let Some(pair) = object_to_bracket_pair(object) {
309 vim.update_active_editor(cx, |_, editor, cx| {
310 editor.transact(cx, |editor, cx| {
311 editor.set_clip_at_line_ends(false, cx);
312 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
313 let mut anchors = Vec::new();
314
315 for selection in &selections {
316 let start = selection.start.to_offset(&display_map, Bias::Left);
317 if let Some(range) = object.range(&display_map, selection.clone(), true) {
318 // If the current parenthesis object is single-line,
319 // then we need to filter whether it is the current line or not
320 if object.is_multiline()
321 || (!object.is_multiline()
322 && selection.start.row() == range.start.row()
323 && selection.end.row() == range.end.row())
324 {
325 valid = true;
326 let mut chars_and_offset = display_map
327 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
328 .peekable();
329 while let Some((ch, offset)) = chars_and_offset.next() {
330 if ch.to_string() == pair.start {
331 anchors.push(offset..offset);
332 break;
333 }
334 }
335 } else {
336 anchors.push(start..start)
337 }
338 } else {
339 anchors.push(start..start)
340 }
341 }
342 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
343 s.select_ranges(anchors);
344 });
345 editor.set_clip_at_line_ends(true, cx);
346 });
347 });
348 }
349 return valid;
350}
351
352fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
353 pairs.iter().find(|pair| pair.start == ch || pair.end == ch)
354}
355
356fn all_support_surround_pair() -> Vec<BracketPair> {
357 return vec![
358 BracketPair {
359 start: "{".into(),
360 end: "}".into(),
361 close: true,
362 newline: false,
363 },
364 BracketPair {
365 start: "'".into(),
366 end: "'".into(),
367 close: true,
368 newline: false,
369 },
370 BracketPair {
371 start: "`".into(),
372 end: "`".into(),
373 close: true,
374 newline: false,
375 },
376 BracketPair {
377 start: "\"".into(),
378 end: "\"".into(),
379 close: true,
380 newline: false,
381 },
382 BracketPair {
383 start: "(".into(),
384 end: ")".into(),
385 close: true,
386 newline: false,
387 },
388 BracketPair {
389 start: "|".into(),
390 end: "|".into(),
391 close: true,
392 newline: false,
393 },
394 BracketPair {
395 start: "[".into(),
396 end: "]".into(),
397 close: true,
398 newline: false,
399 },
400 BracketPair {
401 start: "{".into(),
402 end: "}".into(),
403 close: true,
404 newline: false,
405 },
406 BracketPair {
407 start: "<".into(),
408 end: ">".into(),
409 close: true,
410 newline: false,
411 },
412 ];
413}
414
415fn pair_to_object(pair: &BracketPair) -> Option<Object> {
416 match pair.start.as_str() {
417 "'" => Some(Object::Quotes),
418 "`" => Some(Object::BackQuotes),
419 "\"" => Some(Object::DoubleQuotes),
420 "|" => Some(Object::VerticalBars),
421 "(" => Some(Object::Parentheses),
422 "[" => Some(Object::SquareBrackets),
423 "{" => Some(Object::CurlyBrackets),
424 "<" => Some(Object::AngleBrackets),
425 _ => None,
426 }
427}
428
429fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
430 match object {
431 Object::Quotes => Some(BracketPair {
432 start: "'".to_string(),
433 end: "'".to_string(),
434 close: true,
435 newline: false,
436 }),
437 Object::BackQuotes => Some(BracketPair {
438 start: "`".to_string(),
439 end: "`".to_string(),
440 close: true,
441 newline: false,
442 }),
443 Object::DoubleQuotes => Some(BracketPair {
444 start: "\"".to_string(),
445 end: "\"".to_string(),
446 close: true,
447 newline: false,
448 }),
449 Object::VerticalBars => Some(BracketPair {
450 start: "|".to_string(),
451 end: "|".to_string(),
452 close: true,
453 newline: false,
454 }),
455 Object::Parentheses => Some(BracketPair {
456 start: "(".to_string(),
457 end: ")".to_string(),
458 close: true,
459 newline: false,
460 }),
461 Object::SquareBrackets => Some(BracketPair {
462 start: "[".to_string(),
463 end: "]".to_string(),
464 close: true,
465 newline: false,
466 }),
467 Object::CurlyBrackets => Some(BracketPair {
468 start: "{".to_string(),
469 end: "}".to_string(),
470 close: true,
471 newline: false,
472 }),
473 Object::AngleBrackets => Some(BracketPair {
474 start: "<".to_string(),
475 end: ">".to_string(),
476 close: true,
477 newline: false,
478 }),
479 _ => None,
480 }
481}
482
483#[cfg(test)]
484mod test {
485 use indoc::indoc;
486
487 use crate::{state::Mode, test::VimTestContext};
488
489 #[gpui::test]
490 async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
491 let mut cx = VimTestContext::new(cx, true).await;
492
493 // test add surrounds with arround
494 cx.set_state(
495 indoc! {"
496 The quˇick brown
497 fox jumps over
498 the lazy dog."},
499 Mode::Normal,
500 );
501 cx.simulate_keystrokes(["y", "s", "i", "w", "{"]);
502 cx.assert_state(
503 indoc! {"
504 The ˇ{ quick } brown
505 fox jumps over
506 the lazy dog."},
507 Mode::Normal,
508 );
509
510 // test add surrounds not with arround
511 cx.set_state(
512 indoc! {"
513 The quˇick brown
514 fox jumps over
515 the lazy dog."},
516 Mode::Normal,
517 );
518 cx.simulate_keystrokes(["y", "s", "i", "w", "}"]);
519 cx.assert_state(
520 indoc! {"
521 The ˇ{quick} brown
522 fox jumps over
523 the lazy dog."},
524 Mode::Normal,
525 );
526
527 // test add surrounds with motion
528 cx.set_state(
529 indoc! {"
530 The quˇick brown
531 fox jumps over
532 the lazy dog."},
533 Mode::Normal,
534 );
535 cx.simulate_keystrokes(["y", "s", "$", "}"]);
536 cx.assert_state(
537 indoc! {"
538 The quˇ{ick brown}
539 fox jumps over
540 the lazy dog."},
541 Mode::Normal,
542 );
543
544 // test add surrounds with multi cursor
545 cx.set_state(
546 indoc! {"
547 The quˇick brown
548 fox jumps over
549 the laˇzy dog."},
550 Mode::Normal,
551 );
552 cx.simulate_keystrokes(["y", "s", "i", "w", "'"]);
553 cx.assert_state(
554 indoc! {"
555 The ˇ'quick' brown
556 fox jumps over
557 the ˇ'lazy' dog."},
558 Mode::Normal,
559 );
560
561 // test multi cursor add surrounds with motion
562 cx.set_state(
563 indoc! {"
564 The quˇick brown
565 fox jumps over
566 the laˇzy dog."},
567 Mode::Normal,
568 );
569 cx.simulate_keystrokes(["y", "s", "$", "'"]);
570 cx.assert_state(
571 indoc! {"
572 The quˇ'ick brown'
573 fox jumps over
574 the laˇ'zy dog.'"},
575 Mode::Normal,
576 );
577
578 // test multi cursor add surrounds with motion and custom string
579 cx.set_state(
580 indoc! {"
581 The quˇick brown
582 fox jumps over
583 the laˇzy dog."},
584 Mode::Normal,
585 );
586 cx.simulate_keystrokes(["y", "s", "$", "1"]);
587 cx.assert_state(
588 indoc! {"
589 The quˇ1ick brown1
590 fox jumps over
591 the laˇ1zy dog.1"},
592 Mode::Normal,
593 );
594 }
595
596 #[gpui::test]
597 async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
598 let mut cx = VimTestContext::new(cx, true).await;
599
600 // test delete surround
601 cx.set_state(
602 indoc! {"
603 The {quˇick} brown
604 fox jumps over
605 the lazy dog."},
606 Mode::Normal,
607 );
608 cx.simulate_keystrokes(["d", "s", "{"]);
609 cx.assert_state(
610 indoc! {"
611 The ˇquick brown
612 fox jumps over
613 the lazy dog."},
614 Mode::Normal,
615 );
616
617 // test delete not exist surrounds
618 cx.set_state(
619 indoc! {"
620 The {quˇick} brown
621 fox jumps over
622 the lazy dog."},
623 Mode::Normal,
624 );
625 cx.simulate_keystrokes(["d", "s", "["]);
626 cx.assert_state(
627 indoc! {"
628 The {quˇick} brown
629 fox jumps over
630 the lazy dog."},
631 Mode::Normal,
632 );
633
634 // test delete surround forward exist, in the surrounds plugin of other editors,
635 // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
636 cx.set_state(
637 indoc! {"
638 The {quick} brˇown
639 fox jumps over
640 the lazy dog."},
641 Mode::Normal,
642 );
643 cx.simulate_keystrokes(["d", "s", "{"]);
644 cx.assert_state(
645 indoc! {"
646 The {quick} brˇown
647 fox jumps over
648 the lazy dog."},
649 Mode::Normal,
650 );
651
652 // test cursor delete inner surrounds
653 cx.set_state(
654 indoc! {"
655 The { quick brown
656 fox jumˇps over }
657 the lazy dog."},
658 Mode::Normal,
659 );
660 cx.simulate_keystrokes(["d", "s", "{"]);
661 cx.assert_state(
662 indoc! {"
663 The ˇquick brown
664 fox jumps over
665 the lazy dog."},
666 Mode::Normal,
667 );
668
669 // test multi cursor delete surrounds
670 cx.set_state(
671 indoc! {"
672 The [quˇick] brown
673 fox jumps over
674 the [laˇzy] dog."},
675 Mode::Normal,
676 );
677 cx.simulate_keystrokes(["d", "s", "]"]);
678 cx.assert_state(
679 indoc! {"
680 The ˇquick brown
681 fox jumps over
682 the ˇlazy dog."},
683 Mode::Normal,
684 );
685
686 // test multi cursor delete surrounds with arround
687 cx.set_state(
688 indoc! {"
689 Tˇhe [ quick ] brown
690 fox jumps over
691 the [laˇzy] dog."},
692 Mode::Normal,
693 );
694 cx.simulate_keystrokes(["d", "s", "["]);
695 cx.assert_state(
696 indoc! {"
697 The ˇquick brown
698 fox jumps over
699 the ˇlazy dog."},
700 Mode::Normal,
701 );
702
703 cx.set_state(
704 indoc! {"
705 Tˇhe [ quick ] brown
706 fox jumps over
707 the [laˇzy ] dog."},
708 Mode::Normal,
709 );
710 cx.simulate_keystrokes(["d", "s", "["]);
711 cx.assert_state(
712 indoc! {"
713 The ˇquick brown
714 fox jumps over
715 the ˇlazy dog."},
716 Mode::Normal,
717 );
718
719 // test multi cursor delete different surrounds
720 // the pair corresponding to the two cursors is the same,
721 // so they are combined into one cursor
722 cx.set_state(
723 indoc! {"
724 The [quˇick] brown
725 fox jumps over
726 the {laˇzy} dog."},
727 Mode::Normal,
728 );
729 cx.simulate_keystrokes(["d", "s", "{"]);
730 cx.assert_state(
731 indoc! {"
732 The [quick] brown
733 fox jumps over
734 the ˇlazy dog."},
735 Mode::Normal,
736 );
737
738 // test delete surround with multi cursor and nest surrounds
739 cx.set_state(
740 indoc! {"
741 fn test_surround() {
742 ifˇ 2 > 1 {
743 ˇprintln!(\"it is fine\");
744 };
745 }"},
746 Mode::Normal,
747 );
748 cx.simulate_keystrokes(["d", "s", "}"]);
749 cx.assert_state(
750 indoc! {"
751 fn test_surround() ˇ
752 if 2 > 1 ˇ
753 println!(\"it is fine\");
754 ;
755 "},
756 Mode::Normal,
757 );
758 }
759
760 #[gpui::test]
761 async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
762 let mut cx = VimTestContext::new(cx, true).await;
763
764 cx.set_state(
765 indoc! {"
766 The {quˇick} brown
767 fox jumps over
768 the lazy dog."},
769 Mode::Normal,
770 );
771 cx.simulate_keystrokes(["c", "s", "{", "["]);
772 cx.assert_state(
773 indoc! {"
774 The ˇ[ quick ] brown
775 fox jumps over
776 the lazy dog."},
777 Mode::Normal,
778 );
779
780 // test multi cursor change surrounds
781 cx.set_state(
782 indoc! {"
783 The {quˇick} brown
784 fox jumps over
785 the {laˇzy} dog."},
786 Mode::Normal,
787 );
788 cx.simulate_keystrokes(["c", "s", "{", "["]);
789 cx.assert_state(
790 indoc! {"
791 The ˇ[ quick ] brown
792 fox jumps over
793 the ˇ[ lazy ] dog."},
794 Mode::Normal,
795 );
796
797 // test multi cursor delete different surrounds with after cursor
798 cx.set_state(
799 indoc! {"
800 Thˇe {quick} brown
801 fox jumps over
802 the {laˇzy} dog."},
803 Mode::Normal,
804 );
805 cx.simulate_keystrokes(["c", "s", "{", "["]);
806 cx.assert_state(
807 indoc! {"
808 The ˇ[ quick ] brown
809 fox jumps over
810 the ˇ[ lazy ] dog."},
811 Mode::Normal,
812 );
813
814 // test multi cursor change surrount with not arround
815 cx.set_state(
816 indoc! {"
817 Thˇe { quick } brown
818 fox jumps over
819 the {laˇzy} dog."},
820 Mode::Normal,
821 );
822 cx.simulate_keystrokes(["c", "s", "{", "]"]);
823 cx.assert_state(
824 indoc! {"
825 The ˇ[quick] brown
826 fox jumps over
827 the ˇ[lazy] dog."},
828 Mode::Normal,
829 );
830
831 // test multi cursor change with not exist surround
832 cx.set_state(
833 indoc! {"
834 The {quˇick} brown
835 fox jumps over
836 the [laˇzy] dog."},
837 Mode::Normal,
838 );
839 cx.simulate_keystrokes(["c", "s", "[", "'"]);
840 cx.assert_state(
841 indoc! {"
842 The {quick} brown
843 fox jumps over
844 the ˇ'lazy' dog."},
845 Mode::Normal,
846 );
847
848 // test change nesting surrounds
849 cx.set_state(
850 indoc! {"
851 fn test_surround() {
852 ifˇ 2 > 1 {
853 ˇprintln!(\"it is fine\");
854 }
855 };"},
856 Mode::Normal,
857 );
858 cx.simulate_keystrokes(["c", "s", "{", "["]);
859 cx.assert_state(
860 indoc! {"
861 fn test_surround() ˇ[
862 if 2 > 1 ˇ[
863 println!(\"it is fine\");
864 ]
865 ];"},
866 Mode::Normal,
867 );
868 }
869
870 #[gpui::test]
871 async fn test_surrounds(cx: &mut gpui::TestAppContext) {
872 let mut cx = VimTestContext::new(cx, true).await;
873
874 cx.set_state(
875 indoc! {"
876 The quˇick brown
877 fox jumps over
878 the lazy dog."},
879 Mode::Normal,
880 );
881 cx.simulate_keystrokes(["y", "s", "i", "w", "["]);
882 cx.assert_state(
883 indoc! {"
884 The ˇ[ quick ] brown
885 fox jumps over
886 the lazy dog."},
887 Mode::Normal,
888 );
889
890 cx.simulate_keystrokes(["c", "s", "[", "}"]);
891 cx.assert_state(
892 indoc! {"
893 The ˇ{quick} brown
894 fox jumps over
895 the lazy dog."},
896 Mode::Normal,
897 );
898
899 cx.simulate_keystrokes(["d", "s", "{"]);
900 cx.assert_state(
901 indoc! {"
902 The ˇquick brown
903 fox jumps over
904 the lazy dog."},
905 Mode::Normal,
906 );
907
908 cx.simulate_keystrokes(["u"]);
909 cx.assert_state(
910 indoc! {"
911 The ˇ{quick} brown
912 fox jumps over
913 the lazy dog."},
914 Mode::Normal,
915 );
916 }
917}