surrounds.rs

   1use crate::{
   2    Vim,
   3    motion::{self, Motion},
   4    object::Object,
   5    state::Mode,
   6};
   7use editor::{Bias, movement, scroll::Autoscroll};
   8use gpui::{Context, Window};
   9use language::BracketPair;
  10
  11use std::sync::Arc;
  12
  13#[derive(Clone, Debug, PartialEq, Eq)]
  14pub enum SurroundsType {
  15    Motion(Motion),
  16    Object(Object, bool),
  17    Selection,
  18}
  19
  20impl Vim {
  21    pub fn add_surrounds(
  22        &mut self,
  23        text: Arc<str>,
  24        target: SurroundsType,
  25        window: &mut Window,
  26        cx: &mut Context<Self>,
  27    ) {
  28        self.stop_recording(cx);
  29        let count = Vim::take_count(cx);
  30        let forced_motion = Vim::take_forced_motion(cx);
  31        let mode = self.mode;
  32        self.update_editor(window, cx, |_, editor, window, cx| {
  33            let text_layout_details = editor.text_layout_details(window);
  34            editor.transact(window, cx, |editor, window, cx| {
  35                editor.set_clip_at_line_ends(false, cx);
  36
  37                let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
  38                    Some(pair) => pair.clone(),
  39                    None => BracketPair {
  40                        start: text.to_string(),
  41                        end: text.to_string(),
  42                        close: true,
  43                        surround: true,
  44                        newline: false,
  45                    },
  46                };
  47                let surround = pair.end != surround_alias((*text).as_ref());
  48                let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
  49                let mut edits = Vec::new();
  50                let mut anchors = Vec::new();
  51
  52                for selection in &display_selections {
  53                    let range = match &target {
  54                        SurroundsType::Object(object, around) => {
  55                            object.range(&display_map, selection.clone(), *around)
  56                        }
  57                        SurroundsType::Motion(motion) => {
  58                            motion
  59                                .range(
  60                                    &display_map,
  61                                    selection.clone(),
  62                                    count,
  63                                    &text_layout_details,
  64                                    forced_motion,
  65                                )
  66                                .map(|(mut range, _)| {
  67                                    // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
  68                                    if let Motion::CurrentLine = motion {
  69                                        range.start = motion::first_non_whitespace(
  70                                            &display_map,
  71                                            false,
  72                                            range.start,
  73                                        );
  74                                        range.end = movement::saturating_right(
  75                                            &display_map,
  76                                            motion::last_non_whitespace(&display_map, range.end, 1),
  77                                        );
  78                                    }
  79                                    range
  80                                })
  81                        }
  82                        SurroundsType::Selection => Some(selection.range()),
  83                    };
  84
  85                    if let Some(range) = range {
  86                        let start = range.start.to_offset(&display_map, Bias::Right);
  87                        let end = range.end.to_offset(&display_map, Bias::Left);
  88                        let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine {
  89                            (format!("{}\n", pair.start), format!("\n{}", pair.end))
  90                        } else {
  91                            let maybe_space = if surround { " " } else { "" };
  92                            (
  93                                format!("{}{}", pair.start, maybe_space),
  94                                format!("{}{}", maybe_space, pair.end),
  95                            )
  96                        };
  97                        let start_anchor = display_map.buffer_snapshot.anchor_before(start);
  98
  99                        edits.push((start..start, start_cursor_str));
 100                        edits.push((end..end, end_cursor_str));
 101                        anchors.push(start_anchor..start_anchor);
 102                    } else {
 103                        let start_anchor = display_map
 104                            .buffer_snapshot
 105                            .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
 106                        anchors.push(start_anchor..start_anchor);
 107                    }
 108                }
 109
 110                editor.edit(edits, cx);
 111                editor.set_clip_at_line_ends(true, cx);
 112                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 113                    if mode == Mode::VisualBlock {
 114                        s.select_anchor_ranges(anchors.into_iter().take(1))
 115                    } else {
 116                        s.select_anchor_ranges(anchors)
 117                    }
 118                });
 119            });
 120        });
 121        self.switch_mode(Mode::Normal, false, window, cx);
 122    }
 123
 124    pub fn delete_surrounds(
 125        &mut self,
 126        text: Arc<str>,
 127        window: &mut Window,
 128        cx: &mut Context<Self>,
 129    ) {
 130        self.stop_recording(cx);
 131
 132        // only legitimate surrounds can be removed
 133        let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 134            Some(pair) => pair.clone(),
 135            None => return,
 136        };
 137        let pair_object = match pair_to_object(&pair) {
 138            Some(pair_object) => pair_object,
 139            None => return,
 140        };
 141        let surround = pair.end != *text;
 142
 143        self.update_editor(window, cx, |_, editor, window, cx| {
 144            editor.transact(window, cx, |editor, window, cx| {
 145                editor.set_clip_at_line_ends(false, cx);
 146
 147                let (display_map, display_selections) = editor.selections.all_display(cx);
 148                let mut edits = Vec::new();
 149                let mut anchors = Vec::new();
 150
 151                for selection in &display_selections {
 152                    let start = selection.start.to_offset(&display_map, Bias::Left);
 153                    if let Some(range) = pair_object.range(&display_map, selection.clone(), true) {
 154                        // If the current parenthesis object is single-line,
 155                        // then we need to filter whether it is the current line or not
 156                        if !pair_object.is_multiline() {
 157                            let is_same_row = selection.start.row() == range.start.row()
 158                                && selection.end.row() == range.end.row();
 159                            if !is_same_row {
 160                                anchors.push(start..start);
 161                                continue;
 162                            }
 163                        }
 164                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
 165                        // hello«ˇ  "hello in a word"  »again.
 166                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
 167                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
 168                        let mut chars_and_offset = display_map
 169                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 170                            .peekable();
 171                        while let Some((ch, offset)) = chars_and_offset.next() {
 172                            if ch.to_string() == pair.start {
 173                                let start = offset;
 174                                let mut end = start + 1;
 175                                if surround {
 176                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 177                                        if next_ch.eq(&' ') {
 178                                            end += 1;
 179                                        }
 180                                    }
 181                                }
 182                                edits.push((start..end, ""));
 183                                anchors.push(start..start);
 184                                break;
 185                            }
 186                        }
 187                        let mut reverse_chars_and_offsets = display_map
 188                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
 189                            .peekable();
 190                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 191                            if ch.to_string() == pair.end {
 192                                let mut start = offset;
 193                                let end = start + 1;
 194                                if surround {
 195                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 196                                        if next_ch.eq(&' ') {
 197                                            start -= 1;
 198                                        }
 199                                    }
 200                                }
 201                                edits.push((start..end, ""));
 202                                break;
 203                            }
 204                        }
 205                    } else {
 206                        anchors.push(start..start);
 207                    }
 208                }
 209
 210                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 211                    s.select_ranges(anchors);
 212                });
 213                edits.sort_by_key(|(range, _)| range.start);
 214                editor.edit(edits, cx);
 215                editor.set_clip_at_line_ends(true, cx);
 216            });
 217        });
 218    }
 219
 220    pub fn change_surrounds(
 221        &mut self,
 222        text: Arc<str>,
 223        target: Object,
 224        window: &mut Window,
 225        cx: &mut Context<Self>,
 226    ) {
 227        if let Some(will_replace_pair) = object_to_bracket_pair(target) {
 228            self.stop_recording(cx);
 229            self.update_editor(window, cx, |_, editor, window, cx| {
 230                editor.transact(window, cx, |editor, window, cx| {
 231                    editor.set_clip_at_line_ends(false, cx);
 232
 233                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 234                        Some(pair) => pair.clone(),
 235                        None => BracketPair {
 236                            start: text.to_string(),
 237                            end: text.to_string(),
 238                            close: true,
 239                            surround: true,
 240                            newline: false,
 241                        },
 242                    };
 243                    let surround = pair.end != surround_alias((*text).as_ref());
 244                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 245                    let mut edits = Vec::new();
 246                    let mut anchors = Vec::new();
 247
 248                    for selection in &selections {
 249                        let start = selection.start.to_offset(&display_map, Bias::Left);
 250                        if let Some(range) = target.range(&display_map, selection.clone(), true) {
 251                            if !target.is_multiline() {
 252                                let is_same_row = selection.start.row() == range.start.row()
 253                                    && selection.end.row() == range.end.row();
 254                                if !is_same_row {
 255                                    anchors.push(start..start);
 256                                    continue;
 257                                }
 258                            }
 259                            let mut chars_and_offset = display_map
 260                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 261                                .peekable();
 262                            while let Some((ch, offset)) = chars_and_offset.next() {
 263                                if ch.to_string() == will_replace_pair.start {
 264                                    let mut open_str = pair.start.clone();
 265                                    let start = offset;
 266                                    let mut end = start + 1;
 267                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 268                                        // If the next position is already a space or line break,
 269                                        // we don't need to splice another space even under around
 270                                        if surround && !next_ch.is_whitespace() {
 271                                            open_str.push(' ');
 272                                        } else if !surround && next_ch.to_string() == " " {
 273                                            end += 1;
 274                                        }
 275                                    }
 276                                    edits.push((start..end, open_str));
 277                                    anchors.push(start..start);
 278                                    break;
 279                                }
 280                            }
 281
 282                            let mut reverse_chars_and_offsets = display_map
 283                                .reverse_buffer_chars_at(
 284                                    range.end.to_offset(&display_map, Bias::Left),
 285                                )
 286                                .peekable();
 287                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 288                                if ch.to_string() == will_replace_pair.end {
 289                                    let mut close_str = pair.end.clone();
 290                                    let mut start = offset;
 291                                    let end = start + 1;
 292                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 293                                        if surround && !next_ch.is_whitespace() {
 294                                            close_str.insert(0, ' ')
 295                                        } else if !surround && next_ch.to_string() == " " {
 296                                            start -= 1;
 297                                        }
 298                                    }
 299                                    edits.push((start..end, close_str));
 300                                    break;
 301                                }
 302                            }
 303                        } else {
 304                            anchors.push(start..start);
 305                        }
 306                    }
 307
 308                    let stable_anchors = editor
 309                        .selections
 310                        .disjoint_anchors()
 311                        .iter()
 312                        .map(|selection| {
 313                            let start = selection.start.bias_left(&display_map.buffer_snapshot);
 314                            start..start
 315                        })
 316                        .collect::<Vec<_>>();
 317                    edits.sort_by_key(|(range, _)| range.start);
 318                    editor.edit(edits, cx);
 319                    editor.set_clip_at_line_ends(true, cx);
 320                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 321                        s.select_anchor_ranges(stable_anchors);
 322                    });
 323                });
 324            });
 325        }
 326    }
 327
 328    /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
 329    ///
 330    /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
 331    /// A pair of brackets is considered valid if it is well-formed and properly closed.
 332    ///
 333    /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
 334    /// If no valid pair of brackets is found for any cursor, the method returns `false`.
 335    pub fn check_and_move_to_valid_bracket_pair(
 336        &mut self,
 337        object: Object,
 338        window: &mut Window,
 339        cx: &mut Context<Self>,
 340    ) -> bool {
 341        let mut valid = false;
 342        if let Some(pair) = object_to_bracket_pair(object) {
 343            self.update_editor(window, cx, |_, editor, window, cx| {
 344                editor.transact(window, cx, |editor, window, cx| {
 345                    editor.set_clip_at_line_ends(false, cx);
 346                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 347                    let mut anchors = Vec::new();
 348
 349                    for selection in &selections {
 350                        let start = selection.start.to_offset(&display_map, Bias::Left);
 351                        if let Some(range) = object.range(&display_map, selection.clone(), true) {
 352                            // If the current parenthesis object is single-line,
 353                            // then we need to filter whether it is the current line or not
 354                            if object.is_multiline()
 355                                || (!object.is_multiline()
 356                                    && selection.start.row() == range.start.row()
 357                                    && selection.end.row() == range.end.row())
 358                            {
 359                                valid = true;
 360                                let chars_and_offset = display_map
 361                                    .buffer_chars_at(
 362                                        range.start.to_offset(&display_map, Bias::Left),
 363                                    )
 364                                    .peekable();
 365                                for (ch, offset) in chars_and_offset {
 366                                    if ch.to_string() == pair.start {
 367                                        anchors.push(offset..offset);
 368                                        break;
 369                                    }
 370                                }
 371                            } else {
 372                                anchors.push(start..start)
 373                            }
 374                        } else {
 375                            anchors.push(start..start)
 376                        }
 377                    }
 378                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 379                        s.select_ranges(anchors);
 380                    });
 381                    editor.set_clip_at_line_ends(true, cx);
 382                });
 383            });
 384        }
 385        valid
 386    }
 387}
 388
 389fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 390    pairs
 391        .iter()
 392        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
 393}
 394
 395fn surround_alias(ch: &str) -> &str {
 396    match ch {
 397        "b" => ")",
 398        "B" => "}",
 399        "a" => ">",
 400        "r" => "]",
 401        _ => ch,
 402    }
 403}
 404
 405fn all_support_surround_pair() -> Vec<BracketPair> {
 406    vec![
 407        BracketPair {
 408            start: "{".into(),
 409            end: "}".into(),
 410            close: true,
 411            surround: true,
 412            newline: false,
 413        },
 414        BracketPair {
 415            start: "'".into(),
 416            end: "'".into(),
 417            close: true,
 418            surround: true,
 419            newline: false,
 420        },
 421        BracketPair {
 422            start: "`".into(),
 423            end: "`".into(),
 424            close: true,
 425            surround: true,
 426            newline: false,
 427        },
 428        BracketPair {
 429            start: "\"".into(),
 430            end: "\"".into(),
 431            close: true,
 432            surround: true,
 433            newline: false,
 434        },
 435        BracketPair {
 436            start: "(".into(),
 437            end: ")".into(),
 438            close: true,
 439            surround: true,
 440            newline: false,
 441        },
 442        BracketPair {
 443            start: "|".into(),
 444            end: "|".into(),
 445            close: true,
 446            surround: true,
 447            newline: false,
 448        },
 449        BracketPair {
 450            start: "[".into(),
 451            end: "]".into(),
 452            close: true,
 453            surround: true,
 454            newline: false,
 455        },
 456        BracketPair {
 457            start: "{".into(),
 458            end: "}".into(),
 459            close: true,
 460            surround: true,
 461            newline: false,
 462        },
 463        BracketPair {
 464            start: "<".into(),
 465            end: ">".into(),
 466            close: true,
 467            surround: true,
 468            newline: false,
 469        },
 470    ]
 471}
 472
 473fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 474    match pair.start.as_str() {
 475        "'" => Some(Object::Quotes),
 476        "`" => Some(Object::BackQuotes),
 477        "\"" => Some(Object::DoubleQuotes),
 478        "|" => Some(Object::VerticalBars),
 479        "(" => Some(Object::Parentheses),
 480        "[" => Some(Object::SquareBrackets),
 481        "{" => Some(Object::CurlyBrackets),
 482        "<" => Some(Object::AngleBrackets),
 483        _ => None,
 484    }
 485}
 486
 487fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
 488    match object {
 489        Object::Quotes => Some(BracketPair {
 490            start: "'".to_string(),
 491            end: "'".to_string(),
 492            close: true,
 493            surround: true,
 494            newline: false,
 495        }),
 496        Object::BackQuotes => Some(BracketPair {
 497            start: "`".to_string(),
 498            end: "`".to_string(),
 499            close: true,
 500            surround: true,
 501            newline: false,
 502        }),
 503        Object::DoubleQuotes => Some(BracketPair {
 504            start: "\"".to_string(),
 505            end: "\"".to_string(),
 506            close: true,
 507            surround: true,
 508            newline: false,
 509        }),
 510        Object::VerticalBars => Some(BracketPair {
 511            start: "|".to_string(),
 512            end: "|".to_string(),
 513            close: true,
 514            surround: true,
 515            newline: false,
 516        }),
 517        Object::Parentheses => Some(BracketPair {
 518            start: "(".to_string(),
 519            end: ")".to_string(),
 520            close: true,
 521            surround: true,
 522            newline: false,
 523        }),
 524        Object::SquareBrackets => Some(BracketPair {
 525            start: "[".to_string(),
 526            end: "]".to_string(),
 527            close: true,
 528            surround: true,
 529            newline: false,
 530        }),
 531        Object::CurlyBrackets => Some(BracketPair {
 532            start: "{".to_string(),
 533            end: "}".to_string(),
 534            close: true,
 535            surround: true,
 536            newline: false,
 537        }),
 538        Object::AngleBrackets => Some(BracketPair {
 539            start: "<".to_string(),
 540            end: ">".to_string(),
 541            close: true,
 542            surround: true,
 543            newline: false,
 544        }),
 545        _ => None,
 546    }
 547}
 548
 549#[cfg(test)]
 550mod test {
 551    use gpui::KeyBinding;
 552    use indoc::indoc;
 553
 554    use crate::{PushAddSurrounds, state::Mode, test::VimTestContext};
 555
 556    #[gpui::test]
 557    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 558        let mut cx = VimTestContext::new(cx, true).await;
 559
 560        // test add surrounds with around
 561        cx.set_state(
 562            indoc! {"
 563            The quˇick brown
 564            fox jumps over
 565            the lazy dog."},
 566            Mode::Normal,
 567        );
 568        cx.simulate_keystrokes("y s i w {");
 569        cx.assert_state(
 570            indoc! {"
 571            The ˇ{ quick } brown
 572            fox jumps over
 573            the lazy dog."},
 574            Mode::Normal,
 575        );
 576
 577        // test add surrounds not with around
 578        cx.set_state(
 579            indoc! {"
 580            The quˇick brown
 581            fox jumps over
 582            the lazy dog."},
 583            Mode::Normal,
 584        );
 585        cx.simulate_keystrokes("y s i w }");
 586        cx.assert_state(
 587            indoc! {"
 588            The ˇ{quick} brown
 589            fox jumps over
 590            the lazy dog."},
 591            Mode::Normal,
 592        );
 593
 594        // test add surrounds with motion
 595        cx.set_state(
 596            indoc! {"
 597            The quˇick brown
 598            fox jumps over
 599            the lazy dog."},
 600            Mode::Normal,
 601        );
 602        cx.simulate_keystrokes("y s $ }");
 603        cx.assert_state(
 604            indoc! {"
 605            The quˇ{ick brown}
 606            fox jumps over
 607            the lazy dog."},
 608            Mode::Normal,
 609        );
 610
 611        // test add surrounds with multi cursor
 612        cx.set_state(
 613            indoc! {"
 614            The quˇick brown
 615            fox jumps over
 616            the laˇzy dog."},
 617            Mode::Normal,
 618        );
 619        cx.simulate_keystrokes("y s i w '");
 620        cx.assert_state(
 621            indoc! {"
 622            The ˇ'quick' brown
 623            fox jumps over
 624            the ˇ'lazy' dog."},
 625            Mode::Normal,
 626        );
 627
 628        // test multi cursor add surrounds with motion
 629        cx.set_state(
 630            indoc! {"
 631            The quˇick brown
 632            fox jumps over
 633            the laˇzy dog."},
 634            Mode::Normal,
 635        );
 636        cx.simulate_keystrokes("y s $ '");
 637        cx.assert_state(
 638            indoc! {"
 639            The quˇ'ick brown'
 640            fox jumps over
 641            the laˇ'zy dog.'"},
 642            Mode::Normal,
 643        );
 644
 645        // test multi cursor add surrounds with motion and custom string
 646        cx.set_state(
 647            indoc! {"
 648            The quˇick brown
 649            fox jumps over
 650            the laˇzy dog."},
 651            Mode::Normal,
 652        );
 653        cx.simulate_keystrokes("y s $ 1");
 654        cx.assert_state(
 655            indoc! {"
 656            The quˇ1ick brown1
 657            fox jumps over
 658            the laˇ1zy dog.1"},
 659            Mode::Normal,
 660        );
 661
 662        // test add surrounds with motion current line
 663        cx.set_state(
 664            indoc! {"
 665            The quˇick brown
 666            fox jumps over
 667            the lazy dog."},
 668            Mode::Normal,
 669        );
 670        cx.simulate_keystrokes("y s s {");
 671        cx.assert_state(
 672            indoc! {"
 673            ˇ{ The quick brown }
 674            fox jumps over
 675            the lazy dog."},
 676            Mode::Normal,
 677        );
 678
 679        cx.set_state(
 680            indoc! {"
 681                The quˇick brown•
 682            fox jumps over
 683            the lazy dog."},
 684            Mode::Normal,
 685        );
 686        cx.simulate_keystrokes("y s s {");
 687        cx.assert_state(
 688            indoc! {"
 689                ˇ{ The quick brown }•
 690            fox jumps over
 691            the lazy dog."},
 692            Mode::Normal,
 693        );
 694        cx.simulate_keystrokes("2 y s s )");
 695        cx.assert_state(
 696            indoc! {"
 697                ˇ({ The quick brown }•
 698            fox jumps over)
 699            the lazy dog."},
 700            Mode::Normal,
 701        );
 702
 703        // test add surrounds around object
 704        cx.set_state(
 705            indoc! {"
 706            The [quˇick] brown
 707            fox jumps over
 708            the lazy dog."},
 709            Mode::Normal,
 710        );
 711        cx.simulate_keystrokes("y s a ] )");
 712        cx.assert_state(
 713            indoc! {"
 714            The ˇ([quick]) brown
 715            fox jumps over
 716            the lazy dog."},
 717            Mode::Normal,
 718        );
 719
 720        // test add surrounds inside object
 721        cx.set_state(
 722            indoc! {"
 723            The [quˇick] brown
 724            fox jumps over
 725            the lazy dog."},
 726            Mode::Normal,
 727        );
 728        cx.simulate_keystrokes("y s i ] )");
 729        cx.assert_state(
 730            indoc! {"
 731            The [ˇ(quick)] brown
 732            fox jumps over
 733            the lazy dog."},
 734            Mode::Normal,
 735        );
 736    }
 737
 738    #[gpui::test]
 739    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 740        let mut cx = VimTestContext::new(cx, true).await;
 741
 742        cx.update(|_, cx| {
 743            cx.bind_keys([KeyBinding::new(
 744                "shift-s",
 745                PushAddSurrounds {},
 746                Some("vim_mode == visual"),
 747            )])
 748        });
 749
 750        // test add surrounds with around
 751        cx.set_state(
 752            indoc! {"
 753            The quˇick brown
 754            fox jumps over
 755            the lazy dog."},
 756            Mode::Normal,
 757        );
 758        cx.simulate_keystrokes("v i w shift-s {");
 759        cx.assert_state(
 760            indoc! {"
 761            The ˇ{ quick } brown
 762            fox jumps over
 763            the lazy dog."},
 764            Mode::Normal,
 765        );
 766
 767        // test add surrounds not with around
 768        cx.set_state(
 769            indoc! {"
 770            The quˇick brown
 771            fox jumps over
 772            the lazy dog."},
 773            Mode::Normal,
 774        );
 775        cx.simulate_keystrokes("v i w shift-s }");
 776        cx.assert_state(
 777            indoc! {"
 778            The ˇ{quick} brown
 779            fox jumps over
 780            the lazy dog."},
 781            Mode::Normal,
 782        );
 783
 784        // test add surrounds with motion
 785        cx.set_state(
 786            indoc! {"
 787            The quˇick brown
 788            fox jumps over
 789            the lazy dog."},
 790            Mode::Normal,
 791        );
 792        cx.simulate_keystrokes("v e shift-s }");
 793        cx.assert_state(
 794            indoc! {"
 795            The quˇ{ick} brown
 796            fox jumps over
 797            the lazy dog."},
 798            Mode::Normal,
 799        );
 800
 801        // test add surrounds with multi cursor
 802        cx.set_state(
 803            indoc! {"
 804            The quˇick brown
 805            fox jumps over
 806            the laˇzy dog."},
 807            Mode::Normal,
 808        );
 809        cx.simulate_keystrokes("v i w shift-s '");
 810        cx.assert_state(
 811            indoc! {"
 812            The ˇ'quick' brown
 813            fox jumps over
 814            the ˇ'lazy' dog."},
 815            Mode::Normal,
 816        );
 817
 818        // test add surrounds with visual block
 819        cx.set_state(
 820            indoc! {"
 821            The quˇick brown
 822            fox jumps over
 823            the lazy dog."},
 824            Mode::Normal,
 825        );
 826        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 827        cx.assert_state(
 828            indoc! {"
 829            The ˇ'quick' brown
 830            fox 'jumps' over
 831            the 'lazy 'dog."},
 832            Mode::Normal,
 833        );
 834
 835        // test add surrounds with visual line
 836        cx.set_state(
 837            indoc! {"
 838            The quˇick brown
 839            fox jumps over
 840            the lazy dog."},
 841            Mode::Normal,
 842        );
 843        cx.simulate_keystrokes("j shift-v shift-s '");
 844        cx.assert_state(
 845            indoc! {"
 846            The quick brown
 847            ˇ'
 848            fox jumps over
 849            '
 850            the lazy dog."},
 851            Mode::Normal,
 852        );
 853    }
 854
 855    #[gpui::test]
 856    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 857        let mut cx = VimTestContext::new(cx, true).await;
 858
 859        // test delete surround
 860        cx.set_state(
 861            indoc! {"
 862            The {quˇick} brown
 863            fox jumps over
 864            the lazy dog."},
 865            Mode::Normal,
 866        );
 867        cx.simulate_keystrokes("d s {");
 868        cx.assert_state(
 869            indoc! {"
 870            The ˇquick brown
 871            fox jumps over
 872            the lazy dog."},
 873            Mode::Normal,
 874        );
 875
 876        // test delete not exist surrounds
 877        cx.set_state(
 878            indoc! {"
 879            The {quˇick} brown
 880            fox jumps over
 881            the lazy dog."},
 882            Mode::Normal,
 883        );
 884        cx.simulate_keystrokes("d s [");
 885        cx.assert_state(
 886            indoc! {"
 887            The {quˇick} brown
 888            fox jumps over
 889            the lazy dog."},
 890            Mode::Normal,
 891        );
 892
 893        // test delete surround forward exist, in the surrounds plugin of other editors,
 894        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 895        cx.set_state(
 896            indoc! {"
 897            The {quick} brˇown
 898            fox jumps over
 899            the lazy dog."},
 900            Mode::Normal,
 901        );
 902        cx.simulate_keystrokes("d s {");
 903        cx.assert_state(
 904            indoc! {"
 905            The {quick} brˇown
 906            fox jumps over
 907            the lazy dog."},
 908            Mode::Normal,
 909        );
 910
 911        // test cursor delete inner surrounds
 912        cx.set_state(
 913            indoc! {"
 914            The { quick brown
 915            fox jumˇps over }
 916            the lazy dog."},
 917            Mode::Normal,
 918        );
 919        cx.simulate_keystrokes("d s {");
 920        cx.assert_state(
 921            indoc! {"
 922            The ˇquick brown
 923            fox jumps over
 924            the lazy dog."},
 925            Mode::Normal,
 926        );
 927
 928        // test multi cursor delete surrounds
 929        cx.set_state(
 930            indoc! {"
 931            The [quˇick] brown
 932            fox jumps over
 933            the [laˇzy] dog."},
 934            Mode::Normal,
 935        );
 936        cx.simulate_keystrokes("d s ]");
 937        cx.assert_state(
 938            indoc! {"
 939            The ˇquick brown
 940            fox jumps over
 941            the ˇlazy dog."},
 942            Mode::Normal,
 943        );
 944
 945        // test multi cursor delete surrounds with around
 946        cx.set_state(
 947            indoc! {"
 948            Tˇhe [ quick ] brown
 949            fox jumps over
 950            the [laˇzy] dog."},
 951            Mode::Normal,
 952        );
 953        cx.simulate_keystrokes("d s [");
 954        cx.assert_state(
 955            indoc! {"
 956            The ˇquick brown
 957            fox jumps over
 958            the ˇlazy dog."},
 959            Mode::Normal,
 960        );
 961
 962        cx.set_state(
 963            indoc! {"
 964            Tˇhe [ quick ] brown
 965            fox jumps over
 966            the [laˇzy ] dog."},
 967            Mode::Normal,
 968        );
 969        cx.simulate_keystrokes("d s [");
 970        cx.assert_state(
 971            indoc! {"
 972            The ˇquick brown
 973            fox jumps over
 974            the ˇlazy dog."},
 975            Mode::Normal,
 976        );
 977
 978        // test multi cursor delete different surrounds
 979        // the pair corresponding to the two cursors is the same,
 980        // so they are combined into one cursor
 981        cx.set_state(
 982            indoc! {"
 983            The [quˇick] brown
 984            fox jumps over
 985            the {laˇzy} dog."},
 986            Mode::Normal,
 987        );
 988        cx.simulate_keystrokes("d s {");
 989        cx.assert_state(
 990            indoc! {"
 991            The [quick] brown
 992            fox jumps over
 993            the ˇlazy dog."},
 994            Mode::Normal,
 995        );
 996
 997        // test delete surround with multi cursor and nest surrounds
 998        cx.set_state(
 999            indoc! {"
1000            fn test_surround() {
1001                ifˇ 2 > 1 {
1002                    ˇprintln!(\"it is fine\");
1003                };
1004            }"},
1005            Mode::Normal,
1006        );
1007        cx.simulate_keystrokes("d s }");
1008        cx.assert_state(
1009            indoc! {"
1010            fn test_surround() ˇ
1011                if 2 > 1 ˇ
1012                    println!(\"it is fine\");
1013                ;
1014            "},
1015            Mode::Normal,
1016        );
1017    }
1018
1019    #[gpui::test]
1020    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1021        let mut cx = VimTestContext::new(cx, true).await;
1022
1023        cx.set_state(
1024            indoc! {"
1025            The {quˇick} brown
1026            fox jumps over
1027            the lazy dog."},
1028            Mode::Normal,
1029        );
1030        cx.simulate_keystrokes("c 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        // test multi cursor change surrounds
1040        cx.set_state(
1041            indoc! {"
1042            The {quˇick} brown
1043            fox jumps over
1044            the {laˇzy} dog."},
1045            Mode::Normal,
1046        );
1047        cx.simulate_keystrokes("c s { [");
1048        cx.assert_state(
1049            indoc! {"
1050            The ˇ[ quick ] brown
1051            fox jumps over
1052            the ˇ[ lazy ] dog."},
1053            Mode::Normal,
1054        );
1055
1056        // test multi cursor delete different surrounds with after cursor
1057        cx.set_state(
1058            indoc! {"
1059            Thˇe {quick} brown
1060            fox jumps over
1061            the {laˇzy} dog."},
1062            Mode::Normal,
1063        );
1064        cx.simulate_keystrokes("c s { [");
1065        cx.assert_state(
1066            indoc! {"
1067            The ˇ[ quick ] brown
1068            fox jumps over
1069            the ˇ[ lazy ] dog."},
1070            Mode::Normal,
1071        );
1072
1073        // test multi cursor change surrount with not around
1074        cx.set_state(
1075            indoc! {"
1076            Thˇe { quick } brown
1077            fox jumps over
1078            the {laˇzy} dog."},
1079            Mode::Normal,
1080        );
1081        cx.simulate_keystrokes("c s { ]");
1082        cx.assert_state(
1083            indoc! {"
1084            The ˇ[quick] brown
1085            fox jumps over
1086            the ˇ[lazy] dog."},
1087            Mode::Normal,
1088        );
1089
1090        // test multi cursor change with not exist surround
1091        cx.set_state(
1092            indoc! {"
1093            The {quˇick} brown
1094            fox jumps over
1095            the [laˇzy] dog."},
1096            Mode::Normal,
1097        );
1098        cx.simulate_keystrokes("c s [ '");
1099        cx.assert_state(
1100            indoc! {"
1101            The {quick} brown
1102            fox jumps over
1103            the ˇ'lazy' dog."},
1104            Mode::Normal,
1105        );
1106
1107        // test change nesting surrounds
1108        cx.set_state(
1109            indoc! {"
1110            fn test_surround() {
1111                ifˇ 2 > 1 {
1112                    ˇprintln!(\"it is fine\");
1113                }
1114            };"},
1115            Mode::Normal,
1116        );
1117        cx.simulate_keystrokes("c s { [");
1118        cx.assert_state(
1119            indoc! {"
1120            fn test_surround() ˇ[
1121                if 2 > 1 ˇ[
1122                    println!(\"it is fine\");
1123                ]
1124            ];"},
1125            Mode::Normal,
1126        );
1127    }
1128
1129    #[gpui::test]
1130    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1131        let mut cx = VimTestContext::new(cx, true).await;
1132
1133        cx.set_state(
1134            indoc! {"
1135            The quˇick brown
1136            fox jumps over
1137            the lazy dog."},
1138            Mode::Normal,
1139        );
1140        cx.simulate_keystrokes("y s i w [");
1141        cx.assert_state(
1142            indoc! {"
1143            The ˇ[ quick ] brown
1144            fox jumps over
1145            the lazy dog."},
1146            Mode::Normal,
1147        );
1148
1149        cx.simulate_keystrokes("c s [ }");
1150        cx.assert_state(
1151            indoc! {"
1152            The ˇ{quick} brown
1153            fox jumps over
1154            the lazy dog."},
1155            Mode::Normal,
1156        );
1157
1158        cx.simulate_keystrokes("d 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        cx.simulate_keystrokes("u");
1168        cx.assert_state(
1169            indoc! {"
1170            The ˇ{quick} brown
1171            fox jumps over
1172            the lazy dog."},
1173            Mode::Normal,
1174        );
1175    }
1176
1177    #[gpui::test]
1178    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1179        let mut cx = VimTestContext::new(cx, true).await;
1180
1181        // add aliases
1182        cx.set_state(
1183            indoc! {"
1184            The quˇick brown
1185            fox jumps over
1186            the lazy dog."},
1187            Mode::Normal,
1188        );
1189        cx.simulate_keystrokes("y s i w b");
1190        cx.assert_state(
1191            indoc! {"
1192            The ˇ(quick) brown
1193            fox jumps over
1194            the lazy dog."},
1195            Mode::Normal,
1196        );
1197
1198        cx.set_state(
1199            indoc! {"
1200            The quˇick brown
1201            fox jumps over
1202            the lazy dog."},
1203            Mode::Normal,
1204        );
1205        cx.simulate_keystrokes("y s i w B");
1206        cx.assert_state(
1207            indoc! {"
1208            The ˇ{quick} brown
1209            fox jumps over
1210            the lazy dog."},
1211            Mode::Normal,
1212        );
1213
1214        cx.set_state(
1215            indoc! {"
1216            The quˇick brown
1217            fox jumps over
1218            the lazy dog."},
1219            Mode::Normal,
1220        );
1221        cx.simulate_keystrokes("y s i w a");
1222        cx.assert_state(
1223            indoc! {"
1224            The ˇ<quick> brown
1225            fox jumps over
1226            the lazy dog."},
1227            Mode::Normal,
1228        );
1229
1230        cx.set_state(
1231            indoc! {"
1232            The quˇick brown
1233            fox jumps over
1234            the lazy dog."},
1235            Mode::Normal,
1236        );
1237        cx.simulate_keystrokes("y s i w r");
1238        cx.assert_state(
1239            indoc! {"
1240            The ˇ[quick] brown
1241            fox jumps over
1242            the lazy dog."},
1243            Mode::Normal,
1244        );
1245
1246        // change aliases
1247        cx.set_state(
1248            indoc! {"
1249            The {quˇick} brown
1250            fox jumps over
1251            the lazy dog."},
1252            Mode::Normal,
1253        );
1254        cx.simulate_keystrokes("c s { b");
1255        cx.assert_state(
1256            indoc! {"
1257            The ˇ(quick) brown
1258            fox jumps over
1259            the lazy dog."},
1260            Mode::Normal,
1261        );
1262
1263        cx.set_state(
1264            indoc! {"
1265            The (quˇick) brown
1266            fox jumps over
1267            the lazy dog."},
1268            Mode::Normal,
1269        );
1270        cx.simulate_keystrokes("c s ( B");
1271        cx.assert_state(
1272            indoc! {"
1273            The ˇ{quick} brown
1274            fox jumps over
1275            the lazy dog."},
1276            Mode::Normal,
1277        );
1278
1279        cx.set_state(
1280            indoc! {"
1281            The (quˇick) brown
1282            fox jumps over
1283            the lazy dog."},
1284            Mode::Normal,
1285        );
1286        cx.simulate_keystrokes("c s ( a");
1287        cx.assert_state(
1288            indoc! {"
1289            The ˇ<quick> brown
1290            fox jumps over
1291            the lazy dog."},
1292            Mode::Normal,
1293        );
1294
1295        cx.set_state(
1296            indoc! {"
1297            The <quˇick> brown
1298            fox jumps over
1299            the lazy dog."},
1300            Mode::Normal,
1301        );
1302        cx.simulate_keystrokes("c s < b");
1303        cx.assert_state(
1304            indoc! {"
1305            The ˇ(quick) brown
1306            fox jumps over
1307            the lazy dog."},
1308            Mode::Normal,
1309        );
1310
1311        cx.set_state(
1312            indoc! {"
1313            The (quˇick) brown
1314            fox jumps over
1315            the lazy dog."},
1316            Mode::Normal,
1317        );
1318        cx.simulate_keystrokes("c s ( r");
1319        cx.assert_state(
1320            indoc! {"
1321            The ˇ[quick] brown
1322            fox jumps over
1323            the lazy dog."},
1324            Mode::Normal,
1325        );
1326
1327        cx.set_state(
1328            indoc! {"
1329            The [quˇick] brown
1330            fox jumps over
1331            the lazy dog."},
1332            Mode::Normal,
1333        );
1334        cx.simulate_keystrokes("c s [ b");
1335        cx.assert_state(
1336            indoc! {"
1337            The ˇ(quick) brown
1338            fox jumps over
1339            the lazy dog."},
1340            Mode::Normal,
1341        );
1342
1343        // delete alias
1344        cx.set_state(
1345            indoc! {"
1346            The {quˇick} brown
1347            fox jumps over
1348            the lazy dog."},
1349            Mode::Normal,
1350        );
1351        cx.simulate_keystrokes("d s B");
1352        cx.assert_state(
1353            indoc! {"
1354            The ˇquick brown
1355            fox jumps over
1356            the lazy dog."},
1357            Mode::Normal,
1358        );
1359
1360        cx.set_state(
1361            indoc! {"
1362            The (quˇick) brown
1363            fox jumps over
1364            the lazy dog."},
1365            Mode::Normal,
1366        );
1367        cx.simulate_keystrokes("d s b");
1368        cx.assert_state(
1369            indoc! {"
1370            The ˇquick brown
1371            fox jumps over
1372            the lazy dog."},
1373            Mode::Normal,
1374        );
1375
1376        cx.set_state(
1377            indoc! {"
1378            The [quˇick] brown
1379            fox jumps over
1380            the lazy dog."},
1381            Mode::Normal,
1382        );
1383        cx.simulate_keystrokes("d s r");
1384        cx.assert_state(
1385            indoc! {"
1386            The ˇquick brown
1387            fox jumps over
1388            the lazy dog."},
1389            Mode::Normal,
1390        );
1391
1392        cx.set_state(
1393            indoc! {"
1394            The <quˇick> brown
1395            fox jumps over
1396            the lazy dog."},
1397            Mode::Normal,
1398        );
1399        cx.simulate_keystrokes("d s a");
1400        cx.assert_state(
1401            indoc! {"
1402            The ˇquick brown
1403            fox jumps over
1404            the lazy dog."},
1405            Mode::Normal,
1406        );
1407    }
1408}