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