surrounds.rs

   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}