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 after
245 // and before the surround pairs.
246 // Space is only added in the following cases:
247 // - new surround is not quote and is opening bracket (({[<)
248 // - new surround is quote and original was also quote
249 let surround = if pair.start != pair.end {
250 pair.end != surround_alias((*text).as_ref())
251 } else {
252 will_replace_pair.start == will_replace_pair.end
253 };
254
255 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
256 let mut edits = Vec::new();
257 let mut anchors = Vec::new();
258
259 for selection in &selections {
260 let start = selection.start.to_offset(&display_map, Bias::Left);
261 if let Some(range) =
262 target.range(&display_map, selection.clone(), true, None)
263 {
264 if !target.is_multiline() {
265 let is_same_row = selection.start.row() == range.start.row()
266 && selection.end.row() == range.end.row();
267 if !is_same_row {
268 anchors.push(start..start);
269 continue;
270 }
271 }
272 let mut chars_and_offset = display_map
273 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
274 .peekable();
275 while let Some((ch, offset)) = chars_and_offset.next() {
276 if ch.to_string() == will_replace_pair.start {
277 let mut open_str = pair.start.clone();
278 let start = offset;
279 let mut end = start + 1;
280 if let Some((next_ch, _)) = chars_and_offset.peek() {
281 // If the next position is already a space or line break,
282 // we don't need to splice another space even under around
283 if surround && !next_ch.is_whitespace() {
284 open_str.push(' ');
285 } else if !surround && next_ch.to_string() == " " {
286 end += 1;
287 }
288 }
289 edits.push((start..end, open_str));
290 anchors.push(start..start);
291 break;
292 }
293 }
294
295 let mut reverse_chars_and_offsets = display_map
296 .reverse_buffer_chars_at(
297 range.end.to_offset(&display_map, Bias::Left),
298 )
299 .peekable();
300 while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
301 if ch.to_string() == will_replace_pair.end {
302 let mut close_str = pair.end.clone();
303 let mut start = offset;
304 let end = start + 1;
305 if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
306 if surround && !next_ch.is_whitespace() {
307 close_str.insert(0, ' ')
308 } else if !surround && next_ch.to_string() == " " {
309 start -= 1;
310 }
311 }
312 edits.push((start..end, close_str));
313 break;
314 }
315 }
316 } else {
317 anchors.push(start..start);
318 }
319 }
320
321 let stable_anchors = editor
322 .selections
323 .disjoint_anchors_arc()
324 .iter()
325 .map(|selection| {
326 let start = selection.start.bias_left(&display_map.buffer_snapshot());
327 start..start
328 })
329 .collect::<Vec<_>>();
330 edits.sort_by_key(|(range, _)| range.start);
331 editor.edit(edits, cx);
332 editor.set_clip_at_line_ends(true, cx);
333 editor.change_selections(Default::default(), window, cx, |s| {
334 s.select_anchor_ranges(stable_anchors);
335 });
336 });
337 });
338 }
339 }
340
341 /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
342 ///
343 /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
344 /// A pair of brackets is considered valid if it is well-formed and properly closed.
345 ///
346 /// 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.
347 /// If no valid pair of brackets is found for any cursor, the method returns `false`.
348 pub fn check_and_move_to_valid_bracket_pair(
349 &mut self,
350 object: Object,
351 window: &mut Window,
352 cx: &mut Context<Self>,
353 ) -> bool {
354 let mut valid = false;
355 if let Some(pair) = self.object_to_bracket_pair(object, cx) {
356 self.update_editor(cx, |_, editor, cx| {
357 editor.transact(window, cx, |editor, window, cx| {
358 editor.set_clip_at_line_ends(false, cx);
359 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
360 let mut anchors = Vec::new();
361
362 for selection in &selections {
363 let start = selection.start.to_offset(&display_map, Bias::Left);
364 if let Some(range) =
365 object.range(&display_map, selection.clone(), true, None)
366 {
367 // If the current parenthesis object is single-line,
368 // then we need to filter whether it is the current line or not
369 if object.is_multiline()
370 || (!object.is_multiline()
371 && selection.start.row() == range.start.row()
372 && selection.end.row() == range.end.row())
373 {
374 valid = true;
375 let chars_and_offset = display_map
376 .buffer_chars_at(
377 range.start.to_offset(&display_map, Bias::Left),
378 )
379 .peekable();
380 for (ch, offset) in chars_and_offset {
381 if ch.to_string() == pair.start {
382 anchors.push(offset..offset);
383 break;
384 }
385 }
386 } else {
387 anchors.push(start..start)
388 }
389 } else {
390 anchors.push(start..start)
391 }
392 }
393 editor.change_selections(Default::default(), window, cx, |s| {
394 s.select_ranges(anchors);
395 });
396 editor.set_clip_at_line_ends(true, cx);
397 });
398 });
399 }
400 valid
401 }
402
403 fn object_to_bracket_pair(
404 &self,
405 object: Object,
406 cx: &mut Context<Self>,
407 ) -> Option<BracketPair> {
408 match object {
409 Object::Quotes => Some(BracketPair {
410 start: "'".to_string(),
411 end: "'".to_string(),
412 close: true,
413 surround: true,
414 newline: false,
415 }),
416 Object::BackQuotes => Some(BracketPair {
417 start: "`".to_string(),
418 end: "`".to_string(),
419 close: true,
420 surround: true,
421 newline: false,
422 }),
423 Object::DoubleQuotes => Some(BracketPair {
424 start: "\"".to_string(),
425 end: "\"".to_string(),
426 close: true,
427 surround: true,
428 newline: false,
429 }),
430 Object::VerticalBars => Some(BracketPair {
431 start: "|".to_string(),
432 end: "|".to_string(),
433 close: true,
434 surround: true,
435 newline: false,
436 }),
437 Object::Parentheses => Some(BracketPair {
438 start: "(".to_string(),
439 end: ")".to_string(),
440 close: true,
441 surround: true,
442 newline: false,
443 }),
444 Object::SquareBrackets => Some(BracketPair {
445 start: "[".to_string(),
446 end: "]".to_string(),
447 close: true,
448 surround: true,
449 newline: false,
450 }),
451 Object::CurlyBrackets => Some(BracketPair {
452 start: "{".to_string(),
453 end: "}".to_string(),
454 close: true,
455 surround: true,
456 newline: false,
457 }),
458 Object::AngleBrackets => Some(BracketPair {
459 start: "<".to_string(),
460 end: ">".to_string(),
461 close: true,
462 surround: true,
463 newline: false,
464 }),
465 Object::AnyBrackets => {
466 // If we're dealing with `AnyBrackets`, which can map to multiple
467 // bracket pairs, we'll need to first determine which `BracketPair` to
468 // target.
469 // As such, we keep track of the smallest range size, so
470 // that in cases like `({ name: "John" })` if the cursor is
471 // inside the curly brackets, we target the curly brackets
472 // instead of the parentheses.
473 let mut bracket_pair = None;
474 let mut min_range_size = usize::MAX;
475
476 let _ = self.editor.update(cx, |editor, cx| {
477 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
478 // Even if there's multiple cursors, we'll simply rely on
479 // the first one to understand what bracket pair to map to.
480 // I believe we could, if worth it, go one step above and
481 // have a `BracketPair` per selection, so that `AnyBracket`
482 // could work in situations where the transformation below
483 // could be done.
484 //
485 // ```
486 // (< name:ˇ'Zed' >)
487 // <[ name:ˇ'DeltaDB' ]>
488 // ```
489 //
490 // After using `csb{`:
491 //
492 // ```
493 // (ˇ{ name:'Zed' })
494 // <ˇ{ name:'DeltaDB' }>
495 // ```
496 if let Some(selection) = selections.first() {
497 let relative_to = selection.head();
498 let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
499 let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
500
501 for &(open, close) in bracket_pairs.iter() {
502 if let Some(range) = surrounding_markers(
503 &display_map,
504 relative_to,
505 true,
506 false,
507 open,
508 close,
509 ) {
510 let start_offset = range.start.to_offset(&display_map, Bias::Left);
511 let end_offset = range.end.to_offset(&display_map, Bias::Right);
512
513 if cursor_offset >= start_offset && cursor_offset <= end_offset {
514 let size = end_offset - start_offset;
515 if size < min_range_size {
516 min_range_size = size;
517 bracket_pair = Some(BracketPair {
518 start: open.to_string(),
519 end: close.to_string(),
520 close: true,
521 surround: true,
522 newline: false,
523 })
524 }
525 }
526 }
527 }
528 }
529 });
530
531 bracket_pair
532 }
533 _ => None,
534 }
535 }
536}
537
538fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
539 pairs
540 .iter()
541 .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
542}
543
544fn surround_alias(ch: &str) -> &str {
545 match ch {
546 "b" => ")",
547 "B" => "}",
548 "a" => ">",
549 "r" => "]",
550 _ => ch,
551 }
552}
553
554fn all_support_surround_pair() -> Vec<BracketPair> {
555 vec![
556 BracketPair {
557 start: "{".into(),
558 end: "}".into(),
559 close: true,
560 surround: true,
561 newline: false,
562 },
563 BracketPair {
564 start: "'".into(),
565 end: "'".into(),
566 close: true,
567 surround: true,
568 newline: false,
569 },
570 BracketPair {
571 start: "`".into(),
572 end: "`".into(),
573 close: true,
574 surround: true,
575 newline: false,
576 },
577 BracketPair {
578 start: "\"".into(),
579 end: "\"".into(),
580 close: true,
581 surround: true,
582 newline: false,
583 },
584 BracketPair {
585 start: "(".into(),
586 end: ")".into(),
587 close: true,
588 surround: true,
589 newline: false,
590 },
591 BracketPair {
592 start: "|".into(),
593 end: "|".into(),
594 close: true,
595 surround: true,
596 newline: false,
597 },
598 BracketPair {
599 start: "[".into(),
600 end: "]".into(),
601 close: true,
602 surround: true,
603 newline: false,
604 },
605 BracketPair {
606 start: "<".into(),
607 end: ">".into(),
608 close: true,
609 surround: true,
610 newline: false,
611 },
612 ]
613}
614
615fn pair_to_object(pair: &BracketPair) -> Option<Object> {
616 match pair.start.as_str() {
617 "'" => Some(Object::Quotes),
618 "`" => Some(Object::BackQuotes),
619 "\"" => Some(Object::DoubleQuotes),
620 "|" => Some(Object::VerticalBars),
621 "(" => Some(Object::Parentheses),
622 "[" => Some(Object::SquareBrackets),
623 "{" => Some(Object::CurlyBrackets),
624 "<" => Some(Object::AngleBrackets),
625 _ => None,
626 }
627}
628
629#[cfg(test)]
630mod test {
631 use gpui::KeyBinding;
632 use indoc::indoc;
633
634 use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
635
636 #[gpui::test]
637 async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
638 let mut cx = VimTestContext::new(cx, true).await;
639
640 // test add surrounds with around
641 cx.set_state(
642 indoc! {"
643 The quˇick brown
644 fox jumps over
645 the lazy dog."},
646 Mode::Normal,
647 );
648 cx.simulate_keystrokes("y s i w {");
649 cx.assert_state(
650 indoc! {"
651 The ˇ{ quick } brown
652 fox jumps over
653 the lazy dog."},
654 Mode::Normal,
655 );
656
657 // test add surrounds not with around
658 cx.set_state(
659 indoc! {"
660 The quˇick brown
661 fox jumps over
662 the lazy dog."},
663 Mode::Normal,
664 );
665 cx.simulate_keystrokes("y s i w }");
666 cx.assert_state(
667 indoc! {"
668 The ˇ{quick} brown
669 fox jumps over
670 the lazy dog."},
671 Mode::Normal,
672 );
673
674 // test add surrounds with motion
675 cx.set_state(
676 indoc! {"
677 The quˇick brown
678 fox jumps over
679 the lazy dog."},
680 Mode::Normal,
681 );
682 cx.simulate_keystrokes("y s $ }");
683 cx.assert_state(
684 indoc! {"
685 The quˇ{ick brown}
686 fox jumps over
687 the lazy dog."},
688 Mode::Normal,
689 );
690
691 // test add surrounds with multi cursor
692 cx.set_state(
693 indoc! {"
694 The quˇick brown
695 fox jumps over
696 the laˇzy dog."},
697 Mode::Normal,
698 );
699 cx.simulate_keystrokes("y s i w '");
700 cx.assert_state(
701 indoc! {"
702 The ˇ'quick' brown
703 fox jumps over
704 the ˇ'lazy' dog."},
705 Mode::Normal,
706 );
707
708 // test multi cursor add surrounds with motion
709 cx.set_state(
710 indoc! {"
711 The quˇick brown
712 fox jumps over
713 the laˇzy dog."},
714 Mode::Normal,
715 );
716 cx.simulate_keystrokes("y s $ '");
717 cx.assert_state(
718 indoc! {"
719 The quˇ'ick brown'
720 fox jumps over
721 the laˇ'zy dog.'"},
722 Mode::Normal,
723 );
724
725 // test multi cursor add surrounds with motion and custom string
726 cx.set_state(
727 indoc! {"
728 The quˇick brown
729 fox jumps over
730 the laˇzy dog."},
731 Mode::Normal,
732 );
733 cx.simulate_keystrokes("y s $ 1");
734 cx.assert_state(
735 indoc! {"
736 The quˇ1ick brown1
737 fox jumps over
738 the laˇ1zy dog.1"},
739 Mode::Normal,
740 );
741
742 // test add surrounds with motion current line
743 cx.set_state(
744 indoc! {"
745 The quˇick brown
746 fox jumps over
747 the lazy dog."},
748 Mode::Normal,
749 );
750 cx.simulate_keystrokes("y s s {");
751 cx.assert_state(
752 indoc! {"
753 ˇ{ The quick brown }
754 fox jumps over
755 the lazy dog."},
756 Mode::Normal,
757 );
758
759 cx.set_state(
760 indoc! {"
761 The quˇick brown•
762 fox jumps over
763 the lazy dog."},
764 Mode::Normal,
765 );
766 cx.simulate_keystrokes("y s s {");
767 cx.assert_state(
768 indoc! {"
769 ˇ{ The quick brown }•
770 fox jumps over
771 the lazy dog."},
772 Mode::Normal,
773 );
774 cx.simulate_keystrokes("2 y s s )");
775 cx.assert_state(
776 indoc! {"
777 ˇ({ The quick brown }•
778 fox jumps over)
779 the lazy dog."},
780 Mode::Normal,
781 );
782
783 // test add surrounds around object
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 a ] )");
792 cx.assert_state(
793 indoc! {"
794 The ˇ([quick]) brown
795 fox jumps over
796 the lazy dog."},
797 Mode::Normal,
798 );
799
800 // test add surrounds inside object
801 cx.set_state(
802 indoc! {"
803 The [quˇick] brown
804 fox jumps over
805 the lazy dog."},
806 Mode::Normal,
807 );
808 cx.simulate_keystrokes("y s i ] )");
809 cx.assert_state(
810 indoc! {"
811 The [ˇ(quick)] brown
812 fox jumps over
813 the lazy dog."},
814 Mode::Normal,
815 );
816 }
817
818 #[gpui::test]
819 async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
820 let mut cx = VimTestContext::new(cx, true).await;
821
822 cx.update(|_, cx| {
823 cx.bind_keys([KeyBinding::new(
824 "shift-s",
825 PushAddSurrounds {},
826 Some("vim_mode == visual"),
827 )])
828 });
829
830 // test add surrounds with around
831 cx.set_state(
832 indoc! {"
833 The quˇick brown
834 fox jumps over
835 the lazy dog."},
836 Mode::Normal,
837 );
838 cx.simulate_keystrokes("v i w shift-s {");
839 cx.assert_state(
840 indoc! {"
841 The ˇ{ quick } brown
842 fox jumps over
843 the lazy dog."},
844 Mode::Normal,
845 );
846
847 // test add surrounds not with around
848 cx.set_state(
849 indoc! {"
850 The quˇick brown
851 fox jumps over
852 the lazy dog."},
853 Mode::Normal,
854 );
855 cx.simulate_keystrokes("v i w shift-s }");
856 cx.assert_state(
857 indoc! {"
858 The ˇ{quick} brown
859 fox jumps over
860 the lazy dog."},
861 Mode::Normal,
862 );
863
864 // test add surrounds with motion
865 cx.set_state(
866 indoc! {"
867 The quˇick brown
868 fox jumps over
869 the lazy dog."},
870 Mode::Normal,
871 );
872 cx.simulate_keystrokes("v e shift-s }");
873 cx.assert_state(
874 indoc! {"
875 The quˇ{ick} brown
876 fox jumps over
877 the lazy dog."},
878 Mode::Normal,
879 );
880
881 // test add surrounds with multi cursor
882 cx.set_state(
883 indoc! {"
884 The quˇick brown
885 fox jumps over
886 the laˇzy dog."},
887 Mode::Normal,
888 );
889 cx.simulate_keystrokes("v i w shift-s '");
890 cx.assert_state(
891 indoc! {"
892 The ˇ'quick' brown
893 fox jumps over
894 the ˇ'lazy' dog."},
895 Mode::Normal,
896 );
897
898 // test add surrounds with visual block
899 cx.set_state(
900 indoc! {"
901 The quˇick brown
902 fox jumps over
903 the lazy dog."},
904 Mode::Normal,
905 );
906 cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
907 cx.assert_state(
908 indoc! {"
909 The ˇ'quick' brown
910 fox 'jumps' over
911 the 'lazy 'dog."},
912 Mode::Normal,
913 );
914
915 // test add surrounds with visual line
916 cx.set_state(
917 indoc! {"
918 The quˇick brown
919 fox jumps over
920 the lazy dog."},
921 Mode::Normal,
922 );
923 cx.simulate_keystrokes("j shift-v shift-s '");
924 cx.assert_state(
925 indoc! {"
926 The quick brown
927 ˇ'
928 fox jumps over
929 '
930 the lazy dog."},
931 Mode::Normal,
932 );
933 }
934
935 #[gpui::test]
936 async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
937 let mut cx = VimTestContext::new(cx, true).await;
938
939 // test delete surround
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("d 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 delete not exist surrounds
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("d s [");
965 cx.assert_state(
966 indoc! {"
967 The {quˇick} brown
968 fox jumps over
969 the lazy dog."},
970 Mode::Normal,
971 );
972
973 // test delete surround forward exist, in the surrounds plugin of other editors,
974 // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
975 cx.set_state(
976 indoc! {"
977 The {quick} brˇown
978 fox jumps over
979 the lazy dog."},
980 Mode::Normal,
981 );
982 cx.simulate_keystrokes("d s {");
983 cx.assert_state(
984 indoc! {"
985 The {quick} brˇown
986 fox jumps over
987 the lazy dog."},
988 Mode::Normal,
989 );
990
991 // test cursor delete inner surrounds
992 cx.set_state(
993 indoc! {"
994 The { quick brown
995 fox jumˇps over }
996 the lazy dog."},
997 Mode::Normal,
998 );
999 cx.simulate_keystrokes("d s {");
1000 cx.assert_state(
1001 indoc! {"
1002 The ˇquick brown
1003 fox jumps over
1004 the lazy dog."},
1005 Mode::Normal,
1006 );
1007
1008 // test multi cursor delete surrounds
1009 cx.set_state(
1010 indoc! {"
1011 The [quˇick] brown
1012 fox jumps over
1013 the [laˇzy] dog."},
1014 Mode::Normal,
1015 );
1016 cx.simulate_keystrokes("d s ]");
1017 cx.assert_state(
1018 indoc! {"
1019 The ˇquick brown
1020 fox jumps over
1021 the ˇlazy dog."},
1022 Mode::Normal,
1023 );
1024
1025 // test multi cursor delete surrounds with around
1026 cx.set_state(
1027 indoc! {"
1028 Tˇhe [ quick ] brown
1029 fox jumps over
1030 the [laˇzy] dog."},
1031 Mode::Normal,
1032 );
1033 cx.simulate_keystrokes("d s [");
1034 cx.assert_state(
1035 indoc! {"
1036 The ˇquick brown
1037 fox jumps over
1038 the ˇlazy dog."},
1039 Mode::Normal,
1040 );
1041
1042 cx.set_state(
1043 indoc! {"
1044 Tˇhe [ quick ] brown
1045 fox jumps over
1046 the [laˇzy ] dog."},
1047 Mode::Normal,
1048 );
1049 cx.simulate_keystrokes("d s [");
1050 cx.assert_state(
1051 indoc! {"
1052 The ˇquick brown
1053 fox jumps over
1054 the ˇlazy dog."},
1055 Mode::Normal,
1056 );
1057
1058 // test multi cursor delete different surrounds
1059 // the pair corresponding to the two cursors is the same,
1060 // so they are combined into one cursor
1061 cx.set_state(
1062 indoc! {"
1063 The [quˇick] brown
1064 fox jumps over
1065 the {laˇzy} dog."},
1066 Mode::Normal,
1067 );
1068 cx.simulate_keystrokes("d s {");
1069 cx.assert_state(
1070 indoc! {"
1071 The [quick] brown
1072 fox jumps over
1073 the ˇlazy dog."},
1074 Mode::Normal,
1075 );
1076
1077 // test delete surround with multi cursor and nest surrounds
1078 cx.set_state(
1079 indoc! {"
1080 fn test_surround() {
1081 ifˇ 2 > 1 {
1082 ˇprintln!(\"it is fine\");
1083 };
1084 }"},
1085 Mode::Normal,
1086 );
1087 cx.simulate_keystrokes("d s }");
1088 cx.assert_state(
1089 indoc! {"
1090 fn test_surround() ˇ
1091 if 2 > 1 ˇ
1092 println!(\"it is fine\");
1093 ;
1094 "},
1095 Mode::Normal,
1096 );
1097 }
1098
1099 #[gpui::test]
1100 async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1101 let mut cx = VimTestContext::new(cx, true).await;
1102
1103 cx.set_state(
1104 indoc! {"
1105 The {quˇick} brown
1106 fox jumps over
1107 the lazy dog."},
1108 Mode::Normal,
1109 );
1110 cx.simulate_keystrokes("c s { [");
1111 cx.assert_state(
1112 indoc! {"
1113 The ˇ[ quick ] brown
1114 fox jumps over
1115 the lazy dog."},
1116 Mode::Normal,
1117 );
1118
1119 // test multi cursor change surrounds
1120 cx.set_state(
1121 indoc! {"
1122 The {quˇick} brown
1123 fox jumps over
1124 the {laˇzy} dog."},
1125 Mode::Normal,
1126 );
1127 cx.simulate_keystrokes("c s { [");
1128 cx.assert_state(
1129 indoc! {"
1130 The ˇ[ quick ] brown
1131 fox jumps over
1132 the ˇ[ lazy ] dog."},
1133 Mode::Normal,
1134 );
1135
1136 // test multi cursor delete different surrounds with after cursor
1137 cx.set_state(
1138 indoc! {"
1139 Thˇe {quick} brown
1140 fox jumps over
1141 the {laˇzy} dog."},
1142 Mode::Normal,
1143 );
1144 cx.simulate_keystrokes("c s { [");
1145 cx.assert_state(
1146 indoc! {"
1147 The ˇ[ quick ] brown
1148 fox jumps over
1149 the ˇ[ lazy ] dog."},
1150 Mode::Normal,
1151 );
1152
1153 // test multi cursor change surrount with not around
1154 cx.set_state(
1155 indoc! {"
1156 Thˇe { quick } brown
1157 fox jumps over
1158 the {laˇzy} dog."},
1159 Mode::Normal,
1160 );
1161 cx.simulate_keystrokes("c s { ]");
1162 cx.assert_state(
1163 indoc! {"
1164 The ˇ[quick] brown
1165 fox jumps over
1166 the ˇ[lazy] dog."},
1167 Mode::Normal,
1168 );
1169
1170 // test multi cursor change with not exist surround
1171 cx.set_state(
1172 indoc! {"
1173 The {quˇick} brown
1174 fox jumps over
1175 the [laˇzy] dog."},
1176 Mode::Normal,
1177 );
1178 cx.simulate_keystrokes("c s [ '");
1179 cx.assert_state(
1180 indoc! {"
1181 The {quick} brown
1182 fox jumps over
1183 the ˇ'lazy' dog."},
1184 Mode::Normal,
1185 );
1186
1187 // test change nesting surrounds
1188 cx.set_state(
1189 indoc! {"
1190 fn test_surround() {
1191 ifˇ 2 > 1 {
1192 ˇprintln!(\"it is fine\");
1193 }
1194 };"},
1195 Mode::Normal,
1196 );
1197 cx.simulate_keystrokes("c s { [");
1198 cx.assert_state(
1199 indoc! {"
1200 fn test_surround() ˇ[
1201 if 2 > 1 ˇ[
1202 println!(\"it is fine\");
1203 ]
1204 ];"},
1205 Mode::Normal,
1206 );
1207
1208 // test change quotes.
1209 cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
1210 cx.simulate_keystrokes("c s ' \"");
1211 cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
1212
1213 // test multi cursor change quotes
1214 cx.set_state(
1215 indoc! {"
1216 ' ˇstr '
1217 some example text here
1218 ˇ' str '
1219 "},
1220 Mode::Normal,
1221 );
1222 cx.simulate_keystrokes("c s ' \"");
1223 cx.assert_state(
1224 indoc! {"
1225 ˇ\" str \"
1226 some example text here
1227 ˇ\" str \"
1228 "},
1229 Mode::Normal,
1230 );
1231
1232 // test quote to bracket spacing.
1233 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1234 cx.simulate_keystrokes("c s ' {");
1235 cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1236
1237 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1238 cx.simulate_keystrokes("c s ' }");
1239 cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1240 }
1241
1242 #[gpui::test]
1243 async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1244 let mut cx = VimTestContext::new(cx, true).await;
1245
1246 // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1247 // action.
1248 cx.update(|_, cx| {
1249 cx.bind_keys([KeyBinding::new(
1250 "b",
1251 AnyBrackets,
1252 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1253 )]);
1254 });
1255
1256 cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1257 cx.simulate_keystrokes("c s b [");
1258 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
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! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1273 cx.simulate_keystrokes("c s b {");
1274 cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1275
1276 cx.set_state(
1277 indoc! {"
1278 (< name: ˇ'Zed' >)
1279 (< nˇame: 'DeltaDB' >)
1280 "},
1281 Mode::Normal,
1282 );
1283 cx.simulate_keystrokes("c s b {");
1284 cx.set_state(
1285 indoc! {"
1286 (ˇ{ name: 'Zed' })
1287 (ˇ{ name: 'DeltaDB' })
1288 "},
1289 Mode::Normal,
1290 );
1291 }
1292
1293 #[gpui::test]
1294 async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1295 let mut cx = VimTestContext::new(cx, true).await;
1296
1297 cx.set_state(
1298 indoc! {"
1299 The quˇick brown
1300 fox jumps over
1301 the lazy dog."},
1302 Mode::Normal,
1303 );
1304 cx.simulate_keystrokes("y s i w [");
1305 cx.assert_state(
1306 indoc! {"
1307 The ˇ[ quick ] brown
1308 fox jumps over
1309 the lazy dog."},
1310 Mode::Normal,
1311 );
1312
1313 cx.simulate_keystrokes("c s [ }");
1314 cx.assert_state(
1315 indoc! {"
1316 The ˇ{quick} brown
1317 fox jumps over
1318 the lazy dog."},
1319 Mode::Normal,
1320 );
1321
1322 cx.simulate_keystrokes("d s {");
1323 cx.assert_state(
1324 indoc! {"
1325 The ˇquick brown
1326 fox jumps over
1327 the lazy dog."},
1328 Mode::Normal,
1329 );
1330
1331 cx.simulate_keystrokes("u");
1332 cx.assert_state(
1333 indoc! {"
1334 The ˇ{quick} brown
1335 fox jumps over
1336 the lazy dog."},
1337 Mode::Normal,
1338 );
1339 }
1340
1341 #[gpui::test]
1342 async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1343 let mut cx = VimTestContext::new(cx, true).await;
1344
1345 // add aliases
1346 cx.set_state(
1347 indoc! {"
1348 The quˇick brown
1349 fox jumps over
1350 the lazy dog."},
1351 Mode::Normal,
1352 );
1353 cx.simulate_keystrokes("y s i w b");
1354 cx.assert_state(
1355 indoc! {"
1356 The ˇ(quick) brown
1357 fox jumps over
1358 the lazy dog."},
1359 Mode::Normal,
1360 );
1361
1362 cx.set_state(
1363 indoc! {"
1364 The quˇick brown
1365 fox jumps over
1366 the lazy dog."},
1367 Mode::Normal,
1368 );
1369 cx.simulate_keystrokes("y s i w B");
1370 cx.assert_state(
1371 indoc! {"
1372 The ˇ{quick} brown
1373 fox jumps over
1374 the lazy dog."},
1375 Mode::Normal,
1376 );
1377
1378 cx.set_state(
1379 indoc! {"
1380 The quˇick brown
1381 fox jumps over
1382 the lazy dog."},
1383 Mode::Normal,
1384 );
1385 cx.simulate_keystrokes("y s i w a");
1386 cx.assert_state(
1387 indoc! {"
1388 The ˇ<quick> brown
1389 fox jumps over
1390 the lazy dog."},
1391 Mode::Normal,
1392 );
1393
1394 cx.set_state(
1395 indoc! {"
1396 The quˇick brown
1397 fox jumps over
1398 the lazy dog."},
1399 Mode::Normal,
1400 );
1401 cx.simulate_keystrokes("y s i w r");
1402 cx.assert_state(
1403 indoc! {"
1404 The ˇ[quick] brown
1405 fox jumps over
1406 the lazy dog."},
1407 Mode::Normal,
1408 );
1409
1410 // change aliases
1411 cx.set_state(
1412 indoc! {"
1413 The {quˇick} brown
1414 fox jumps over
1415 the lazy dog."},
1416 Mode::Normal,
1417 );
1418 cx.simulate_keystrokes("c s { b");
1419 cx.assert_state(
1420 indoc! {"
1421 The ˇ(quick) brown
1422 fox jumps over
1423 the lazy dog."},
1424 Mode::Normal,
1425 );
1426
1427 cx.set_state(
1428 indoc! {"
1429 The (quˇick) brown
1430 fox jumps over
1431 the lazy dog."},
1432 Mode::Normal,
1433 );
1434 cx.simulate_keystrokes("c s ( B");
1435 cx.assert_state(
1436 indoc! {"
1437 The ˇ{quick} brown
1438 fox jumps over
1439 the lazy dog."},
1440 Mode::Normal,
1441 );
1442
1443 cx.set_state(
1444 indoc! {"
1445 The (quˇick) brown
1446 fox jumps over
1447 the lazy dog."},
1448 Mode::Normal,
1449 );
1450 cx.simulate_keystrokes("c s ( a");
1451 cx.assert_state(
1452 indoc! {"
1453 The ˇ<quick> brown
1454 fox jumps over
1455 the lazy dog."},
1456 Mode::Normal,
1457 );
1458
1459 cx.set_state(
1460 indoc! {"
1461 The <quˇick> brown
1462 fox jumps over
1463 the lazy dog."},
1464 Mode::Normal,
1465 );
1466 cx.simulate_keystrokes("c s < b");
1467 cx.assert_state(
1468 indoc! {"
1469 The ˇ(quick) brown
1470 fox jumps over
1471 the lazy dog."},
1472 Mode::Normal,
1473 );
1474
1475 cx.set_state(
1476 indoc! {"
1477 The (quˇick) brown
1478 fox jumps over
1479 the lazy dog."},
1480 Mode::Normal,
1481 );
1482 cx.simulate_keystrokes("c s ( r");
1483 cx.assert_state(
1484 indoc! {"
1485 The ˇ[quick] brown
1486 fox jumps over
1487 the lazy dog."},
1488 Mode::Normal,
1489 );
1490
1491 cx.set_state(
1492 indoc! {"
1493 The [quˇick] brown
1494 fox jumps over
1495 the lazy dog."},
1496 Mode::Normal,
1497 );
1498 cx.simulate_keystrokes("c s [ b");
1499 cx.assert_state(
1500 indoc! {"
1501 The ˇ(quick) brown
1502 fox jumps over
1503 the lazy dog."},
1504 Mode::Normal,
1505 );
1506
1507 // delete alias
1508 cx.set_state(
1509 indoc! {"
1510 The {quˇick} brown
1511 fox jumps over
1512 the lazy dog."},
1513 Mode::Normal,
1514 );
1515 cx.simulate_keystrokes("d s B");
1516 cx.assert_state(
1517 indoc! {"
1518 The ˇquick brown
1519 fox jumps over
1520 the lazy dog."},
1521 Mode::Normal,
1522 );
1523
1524 cx.set_state(
1525 indoc! {"
1526 The (quˇick) brown
1527 fox jumps over
1528 the lazy dog."},
1529 Mode::Normal,
1530 );
1531 cx.simulate_keystrokes("d s b");
1532 cx.assert_state(
1533 indoc! {"
1534 The ˇquick brown
1535 fox jumps over
1536 the lazy dog."},
1537 Mode::Normal,
1538 );
1539
1540 cx.set_state(
1541 indoc! {"
1542 The [quˇick] brown
1543 fox jumps over
1544 the lazy dog."},
1545 Mode::Normal,
1546 );
1547 cx.simulate_keystrokes("d s r");
1548 cx.assert_state(
1549 indoc! {"
1550 The ˇquick brown
1551 fox jumps over
1552 the lazy dog."},
1553 Mode::Normal,
1554 );
1555
1556 cx.set_state(
1557 indoc! {"
1558 The <quˇick> brown
1559 fox jumps over
1560 the lazy dog."},
1561 Mode::Normal,
1562 );
1563 cx.simulate_keystrokes("d s a");
1564 cx.assert_state(
1565 indoc! {"
1566 The ˇquick brown
1567 fox jumps over
1568 the lazy dog."},
1569 Mode::Normal,
1570 );
1571 }
1572}