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