Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:34:25.827Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

surrounds.rs

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