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