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