surrounds.rs

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