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