surrounds.rs

   1use crate::{
   2    motion::{self, Motion},
   3    object::Object,
   4    state::Mode,
   5    Vim,
   6};
   7use editor::{movement, scroll::Autoscroll, Bias};
   8use gpui::WindowContext;
   9use language::BracketPair;
  10use serde::Deserialize;
  11use std::sync::Arc;
  12#[derive(Clone, Debug, PartialEq, Eq)]
  13pub enum SurroundsType {
  14    Motion(Motion),
  15    Object(Object),
  16}
  17
  18// This exists so that we can have Deserialize on Operators, but not on Motions.
  19impl<'de> Deserialize<'de> for SurroundsType {
  20    fn deserialize<D>(_: D) -> Result<Self, D::Error>
  21    where
  22        D: serde::Deserializer<'de>,
  23    {
  24        Err(serde::de::Error::custom("Cannot deserialize SurroundsType"))
  25    }
  26}
  27
  28pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
  29    Vim::update(cx, |vim, cx| {
  30        vim.stop_recording();
  31        let count = vim.take_count(cx);
  32        vim.update_active_editor(cx, |_, editor, cx| {
  33            let text_layout_details = editor.text_layout_details(cx);
  34            editor.transact(cx, |editor, 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 != *text;
  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) => {
  55                            object.range(&display_map, selection.clone(), false)
  56                        }
  57                        SurroundsType::Motion(motion) => {
  58                            let range = motion
  59                                .range(
  60                                    &display_map,
  61                                    selection.clone(),
  62                                    count,
  63                                    true,
  64                                    &text_layout_details,
  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(
  77                                                &display_map,
  78                                                movement::left(&display_map, range.end),
  79                                                1,
  80                                            ),
  81                                        );
  82                                    }
  83                                    range
  84                                });
  85                            range
  86                        }
  87                    };
  88
  89                    if let Some(range) = range {
  90                        let start = range.start.to_offset(&display_map, Bias::Right);
  91                        let end = range.end.to_offset(&display_map, Bias::Left);
  92                        let start_cursor_str =
  93                            format!("{}{}", pair.start, if surround { " " } else { "" });
  94                        let close_cursor_str =
  95                            format!("{}{}", if surround { " " } else { "" }, pair.end);
  96                        let start_anchor = display_map.buffer_snapshot.anchor_before(start);
  97
  98                        edits.push((start..start, start_cursor_str));
  99                        edits.push((end..end, close_cursor_str));
 100                        anchors.push(start_anchor..start_anchor);
 101                    } else {
 102                        let start_anchor = display_map
 103                            .buffer_snapshot
 104                            .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
 105                        anchors.push(start_anchor..start_anchor);
 106                    }
 107                }
 108
 109                editor.buffer().update(cx, |buffer, cx| {
 110                    buffer.edit(edits, None, cx);
 111                });
 112                editor.set_clip_at_line_ends(true, cx);
 113                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 114                    s.select_anchor_ranges(anchors)
 115                });
 116            });
 117        });
 118        vim.switch_mode(Mode::Normal, false, cx);
 119    });
 120}
 121
 122pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
 123    Vim::update(cx, |vim, cx| {
 124        vim.stop_recording();
 125
 126        // only legitimate surrounds can be removed
 127        let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 128            Some(pair) => pair.clone(),
 129            None => return,
 130        };
 131        let pair_object = match pair_to_object(&pair) {
 132            Some(pair_object) => pair_object,
 133            None => return,
 134        };
 135        let surround = pair.end != *text;
 136
 137        vim.update_active_editor(cx, |_, editor, cx| {
 138            editor.transact(cx, |editor, cx| {
 139                editor.set_clip_at_line_ends(false, cx);
 140
 141                let (display_map, display_selections) = editor.selections.all_display(cx);
 142                let mut edits = Vec::new();
 143                let mut anchors = Vec::new();
 144
 145                for selection in &display_selections {
 146                    let start = selection.start.to_offset(&display_map, Bias::Left);
 147                    if let Some(range) = pair_object.range(&display_map, selection.clone(), true) {
 148                        // If the current parenthesis object is single-line,
 149                        // then we need to filter whether it is the current line or not
 150                        if !pair_object.is_multiline() {
 151                            let is_same_row = selection.start.row() == range.start.row()
 152                                && selection.end.row() == range.end.row();
 153                            if !is_same_row {
 154                                anchors.push(start..start);
 155                                continue;
 156                            }
 157                        }
 158                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
 159                        // hello«ˇ  "hello in a word"  »again.
 160                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
 161                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
 162                        let mut chars_and_offset = display_map
 163                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 164                            .peekable();
 165                        while let Some((ch, offset)) = chars_and_offset.next() {
 166                            if ch.to_string() == pair.start {
 167                                let start = offset;
 168                                let mut end = start + 1;
 169                                if surround {
 170                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 171                                        if next_ch.eq(&' ') {
 172                                            end += 1;
 173                                        }
 174                                    }
 175                                }
 176                                edits.push((start..end, ""));
 177                                anchors.push(start..start);
 178                                break;
 179                            }
 180                        }
 181                        let mut reverse_chars_and_offsets = display_map
 182                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
 183                            .peekable();
 184                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 185                            if ch.to_string() == pair.end {
 186                                let mut start = offset;
 187                                let end = start + 1;
 188                                if surround {
 189                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 190                                        if next_ch.eq(&' ') {
 191                                            start -= 1;
 192                                        }
 193                                    }
 194                                }
 195                                edits.push((start..end, ""));
 196                                break;
 197                            }
 198                        }
 199                    } else {
 200                        anchors.push(start..start);
 201                    }
 202                }
 203
 204                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 205                    s.select_ranges(anchors);
 206                });
 207                edits.sort_by_key(|(range, _)| range.start);
 208                editor.buffer().update(cx, |buffer, cx| {
 209                    buffer.edit(edits, None, cx);
 210                });
 211                editor.set_clip_at_line_ends(true, cx);
 212            });
 213        });
 214    });
 215}
 216
 217pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext) {
 218    if let Some(will_replace_pair) = object_to_bracket_pair(target) {
 219        Vim::update(cx, |vim, cx| {
 220            vim.stop_recording();
 221            vim.update_active_editor(cx, |_, editor, cx| {
 222                editor.transact(cx, |editor, cx| {
 223                    editor.set_clip_at_line_ends(false, cx);
 224
 225                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 226                        Some(pair) => pair.clone(),
 227                        None => BracketPair {
 228                            start: text.to_string(),
 229                            end: text.to_string(),
 230                            close: true,
 231                            surround: true,
 232                            newline: false,
 233                        },
 234                    };
 235                    let surround = pair.end != *text;
 236                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 237                    let mut edits = Vec::new();
 238                    let mut anchors = Vec::new();
 239
 240                    for selection in &selections {
 241                        let start = selection.start.to_offset(&display_map, Bias::Left);
 242                        if let Some(range) = target.range(&display_map, selection.clone(), true) {
 243                            if !target.is_multiline() {
 244                                let is_same_row = selection.start.row() == range.start.row()
 245                                    && selection.end.row() == range.end.row();
 246                                if !is_same_row {
 247                                    anchors.push(start..start);
 248                                    continue;
 249                                }
 250                            }
 251                            let mut chars_and_offset = display_map
 252                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 253                                .peekable();
 254                            while let Some((ch, offset)) = chars_and_offset.next() {
 255                                if ch.to_string() == will_replace_pair.start {
 256                                    let mut open_str = pair.start.clone();
 257                                    let start = offset;
 258                                    let mut end = start + 1;
 259                                    match chars_and_offset.peek() {
 260                                        Some((next_ch, _)) => {
 261                                            // If the next position is already a space or line break,
 262                                            // we don't need to splice another space even under arround
 263                                            if surround && !next_ch.is_whitespace() {
 264                                                open_str.push_str(" ");
 265                                            } else if !surround && next_ch.to_string() == " " {
 266                                                end += 1;
 267                                            }
 268                                        }
 269                                        None => {}
 270                                    }
 271                                    edits.push((start..end, open_str));
 272                                    anchors.push(start..start);
 273                                    break;
 274                                }
 275                            }
 276
 277                            let mut reverse_chars_and_offsets = display_map
 278                                .reverse_buffer_chars_at(
 279                                    range.end.to_offset(&display_map, Bias::Left),
 280                                )
 281                                .peekable();
 282                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 283                                if ch.to_string() == will_replace_pair.end {
 284                                    let mut close_str = pair.end.clone();
 285                                    let mut start = offset;
 286                                    let end = start + 1;
 287                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 288                                        if surround && !next_ch.is_whitespace() {
 289                                            close_str.insert_str(0, " ")
 290                                        } else if !surround && next_ch.to_string() == " " {
 291                                            start -= 1;
 292                                        }
 293                                    }
 294                                    edits.push((start..end, close_str));
 295                                    break;
 296                                }
 297                            }
 298                        } else {
 299                            anchors.push(start..start);
 300                        }
 301                    }
 302
 303                    let stable_anchors = editor
 304                        .selections
 305                        .disjoint_anchors()
 306                        .into_iter()
 307                        .map(|selection| {
 308                            let start = selection.start.bias_left(&display_map.buffer_snapshot);
 309                            start..start
 310                        })
 311                        .collect::<Vec<_>>();
 312                    edits.sort_by_key(|(range, _)| range.start);
 313                    editor.buffer().update(cx, |buffer, cx| {
 314                        buffer.edit(edits, None, cx);
 315                    });
 316                    editor.set_clip_at_line_ends(true, cx);
 317                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 318                        s.select_anchor_ranges(stable_anchors);
 319                    });
 320                });
 321            });
 322        });
 323    }
 324}
 325
 326/// Checks if any of the current cursors are surrounded by a valid pair of brackets.
 327///
 328/// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
 329/// A pair of brackets is considered valid if it is well-formed and properly closed.
 330///
 331/// 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.
 332/// If no valid pair of brackets is found for any cursor, the method returns `false`.
 333pub fn check_and_move_to_valid_bracket_pair(
 334    vim: &mut Vim,
 335    object: Object,
 336    cx: &mut WindowContext,
 337) -> bool {
 338    let mut valid = false;
 339    if let Some(pair) = object_to_bracket_pair(object) {
 340        vim.update_active_editor(cx, |_, editor, cx| {
 341            editor.transact(cx, |editor, cx| {
 342                editor.set_clip_at_line_ends(false, cx);
 343                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 344                let mut anchors = Vec::new();
 345
 346                for selection in &selections {
 347                    let start = selection.start.to_offset(&display_map, Bias::Left);
 348                    if let Some(range) = object.range(&display_map, selection.clone(), true) {
 349                        // If the current parenthesis object is single-line,
 350                        // then we need to filter whether it is the current line or not
 351                        if object.is_multiline()
 352                            || (!object.is_multiline()
 353                                && selection.start.row() == range.start.row()
 354                                && selection.end.row() == range.end.row())
 355                        {
 356                            valid = true;
 357                            let mut chars_and_offset = display_map
 358                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 359                                .peekable();
 360                            while let Some((ch, offset)) = chars_and_offset.next() {
 361                                if ch.to_string() == pair.start {
 362                                    anchors.push(offset..offset);
 363                                    break;
 364                                }
 365                            }
 366                        } else {
 367                            anchors.push(start..start)
 368                        }
 369                    } else {
 370                        anchors.push(start..start)
 371                    }
 372                }
 373                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 374                    s.select_ranges(anchors);
 375                });
 376                editor.set_clip_at_line_ends(true, cx);
 377            });
 378        });
 379    }
 380    return valid;
 381}
 382
 383fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 384    pairs.iter().find(|pair| pair.start == ch || pair.end == ch)
 385}
 386
 387fn all_support_surround_pair() -> Vec<BracketPair> {
 388    return vec![
 389        BracketPair {
 390            start: "{".into(),
 391            end: "}".into(),
 392            close: true,
 393            surround: true,
 394            newline: false,
 395        },
 396        BracketPair {
 397            start: "'".into(),
 398            end: "'".into(),
 399            close: true,
 400            surround: true,
 401            newline: false,
 402        },
 403        BracketPair {
 404            start: "`".into(),
 405            end: "`".into(),
 406            close: true,
 407            surround: true,
 408            newline: false,
 409        },
 410        BracketPair {
 411            start: "\"".into(),
 412            end: "\"".into(),
 413            close: true,
 414            surround: true,
 415            newline: false,
 416        },
 417        BracketPair {
 418            start: "(".into(),
 419            end: ")".into(),
 420            close: true,
 421            surround: true,
 422            newline: false,
 423        },
 424        BracketPair {
 425            start: "|".into(),
 426            end: "|".into(),
 427            close: true,
 428            surround: true,
 429            newline: false,
 430        },
 431        BracketPair {
 432            start: "[".into(),
 433            end: "]".into(),
 434            close: true,
 435            surround: true,
 436            newline: false,
 437        },
 438        BracketPair {
 439            start: "{".into(),
 440            end: "}".into(),
 441            close: true,
 442            surround: true,
 443            newline: false,
 444        },
 445        BracketPair {
 446            start: "<".into(),
 447            end: ">".into(),
 448            close: true,
 449            surround: true,
 450            newline: false,
 451        },
 452    ];
 453}
 454
 455fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 456    match pair.start.as_str() {
 457        "'" => Some(Object::Quotes),
 458        "`" => Some(Object::BackQuotes),
 459        "\"" => Some(Object::DoubleQuotes),
 460        "|" => Some(Object::VerticalBars),
 461        "(" => Some(Object::Parentheses),
 462        "[" => Some(Object::SquareBrackets),
 463        "{" => Some(Object::CurlyBrackets),
 464        "<" => Some(Object::AngleBrackets),
 465        _ => None,
 466    }
 467}
 468
 469fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
 470    match object {
 471        Object::Quotes => Some(BracketPair {
 472            start: "'".to_string(),
 473            end: "'".to_string(),
 474            close: true,
 475            surround: true,
 476            newline: false,
 477        }),
 478        Object::BackQuotes => Some(BracketPair {
 479            start: "`".to_string(),
 480            end: "`".to_string(),
 481            close: true,
 482            surround: true,
 483            newline: false,
 484        }),
 485        Object::DoubleQuotes => Some(BracketPair {
 486            start: "\"".to_string(),
 487            end: "\"".to_string(),
 488            close: true,
 489            surround: true,
 490            newline: false,
 491        }),
 492        Object::VerticalBars => Some(BracketPair {
 493            start: "|".to_string(),
 494            end: "|".to_string(),
 495            close: true,
 496            surround: true,
 497            newline: false,
 498        }),
 499        Object::Parentheses => Some(BracketPair {
 500            start: "(".to_string(),
 501            end: ")".to_string(),
 502            close: true,
 503            surround: true,
 504            newline: false,
 505        }),
 506        Object::SquareBrackets => Some(BracketPair {
 507            start: "[".to_string(),
 508            end: "]".to_string(),
 509            close: true,
 510            surround: true,
 511            newline: false,
 512        }),
 513        Object::CurlyBrackets => Some(BracketPair {
 514            start: "{".to_string(),
 515            end: "}".to_string(),
 516            close: true,
 517            surround: true,
 518            newline: false,
 519        }),
 520        Object::AngleBrackets => Some(BracketPair {
 521            start: "<".to_string(),
 522            end: ">".to_string(),
 523            close: true,
 524            surround: true,
 525            newline: false,
 526        }),
 527        _ => None,
 528    }
 529}
 530
 531#[cfg(test)]
 532mod test {
 533    use indoc::indoc;
 534
 535    use crate::{state::Mode, test::VimTestContext};
 536
 537    #[gpui::test]
 538    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 539        let mut cx = VimTestContext::new(cx, true).await;
 540
 541        // test add surrounds with arround
 542        cx.set_state(
 543            indoc! {"
 544            The quˇick brown
 545            fox jumps over
 546            the lazy dog."},
 547            Mode::Normal,
 548        );
 549        cx.simulate_keystrokes("y s i w {");
 550        cx.assert_state(
 551            indoc! {"
 552            The ˇ{ quick } brown
 553            fox jumps over
 554            the lazy dog."},
 555            Mode::Normal,
 556        );
 557
 558        // test add surrounds not with arround
 559        cx.set_state(
 560            indoc! {"
 561            The quˇick brown
 562            fox jumps over
 563            the lazy dog."},
 564            Mode::Normal,
 565        );
 566        cx.simulate_keystrokes("y s i w }");
 567        cx.assert_state(
 568            indoc! {"
 569            The ˇ{quick} brown
 570            fox jumps over
 571            the lazy dog."},
 572            Mode::Normal,
 573        );
 574
 575        // test add surrounds with motion
 576        cx.set_state(
 577            indoc! {"
 578            The quˇick brown
 579            fox jumps over
 580            the lazy dog."},
 581            Mode::Normal,
 582        );
 583        cx.simulate_keystrokes("y s $ }");
 584        cx.assert_state(
 585            indoc! {"
 586            The quˇ{ick brown}
 587            fox jumps over
 588            the lazy dog."},
 589            Mode::Normal,
 590        );
 591
 592        // test add surrounds with multi cursor
 593        cx.set_state(
 594            indoc! {"
 595            The quˇick brown
 596            fox jumps over
 597            the laˇzy dog."},
 598            Mode::Normal,
 599        );
 600        cx.simulate_keystrokes("y s i w '");
 601        cx.assert_state(
 602            indoc! {"
 603            The ˇ'quick' brown
 604            fox jumps over
 605            the ˇ'lazy' dog."},
 606            Mode::Normal,
 607        );
 608
 609        // test multi cursor add surrounds with motion
 610        cx.set_state(
 611            indoc! {"
 612            The quˇick brown
 613            fox jumps over
 614            the laˇzy dog."},
 615            Mode::Normal,
 616        );
 617        cx.simulate_keystrokes("y s $ '");
 618        cx.assert_state(
 619            indoc! {"
 620            The quˇ'ick brown'
 621            fox jumps over
 622            the laˇ'zy dog.'"},
 623            Mode::Normal,
 624        );
 625
 626        // test multi cursor add surrounds with motion and custom string
 627        cx.set_state(
 628            indoc! {"
 629            The quˇick brown
 630            fox jumps over
 631            the laˇzy dog."},
 632            Mode::Normal,
 633        );
 634        cx.simulate_keystrokes("y s $ 1");
 635        cx.assert_state(
 636            indoc! {"
 637            The quˇ1ick brown1
 638            fox jumps over
 639            the laˇ1zy dog.1"},
 640            Mode::Normal,
 641        );
 642
 643        // test add surrounds with motion current line
 644        cx.set_state(
 645            indoc! {"
 646            The quˇick brown
 647            fox jumps over
 648            the lazy dog."},
 649            Mode::Normal,
 650        );
 651        cx.simulate_keystrokes("y s s {");
 652        cx.assert_state(
 653            indoc! {"
 654            ˇ{ The quick brown }
 655            fox jumps over
 656            the lazy dog."},
 657            Mode::Normal,
 658        );
 659
 660        cx.set_state(
 661            indoc! {"
 662                The quˇick brown•
 663            fox jumps over
 664            the lazy dog."},
 665            Mode::Normal,
 666        );
 667        cx.simulate_keystrokes("y s s {");
 668        cx.assert_state(
 669            indoc! {"
 670                ˇ{ The quick brown }•
 671            fox jumps over
 672            the lazy dog."},
 673            Mode::Normal,
 674        );
 675        cx.simulate_keystrokes("2 y s s )");
 676        cx.assert_state(
 677            indoc! {"
 678                ˇ({ The quick brown }•
 679            fox jumps over)
 680            the lazy dog."},
 681            Mode::Normal,
 682        );
 683    }
 684
 685    #[gpui::test]
 686    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 687        let mut cx = VimTestContext::new(cx, true).await;
 688
 689        // test delete surround
 690        cx.set_state(
 691            indoc! {"
 692            The {quˇick} brown
 693            fox jumps over
 694            the lazy dog."},
 695            Mode::Normal,
 696        );
 697        cx.simulate_keystrokes("d s {");
 698        cx.assert_state(
 699            indoc! {"
 700            The ˇquick brown
 701            fox jumps over
 702            the lazy dog."},
 703            Mode::Normal,
 704        );
 705
 706        // test delete not exist surrounds
 707        cx.set_state(
 708            indoc! {"
 709            The {quˇick} brown
 710            fox jumps over
 711            the lazy dog."},
 712            Mode::Normal,
 713        );
 714        cx.simulate_keystrokes("d s [");
 715        cx.assert_state(
 716            indoc! {"
 717            The {quˇick} brown
 718            fox jumps over
 719            the lazy dog."},
 720            Mode::Normal,
 721        );
 722
 723        // test delete surround forward exist, in the surrounds plugin of other editors,
 724        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 725        cx.set_state(
 726            indoc! {"
 727            The {quick} brˇown
 728            fox jumps over
 729            the lazy dog."},
 730            Mode::Normal,
 731        );
 732        cx.simulate_keystrokes("d s {");
 733        cx.assert_state(
 734            indoc! {"
 735            The {quick} brˇown
 736            fox jumps over
 737            the lazy dog."},
 738            Mode::Normal,
 739        );
 740
 741        // test cursor delete inner surrounds
 742        cx.set_state(
 743            indoc! {"
 744            The { quick brown
 745            fox jumˇps over }
 746            the lazy dog."},
 747            Mode::Normal,
 748        );
 749        cx.simulate_keystrokes("d s {");
 750        cx.assert_state(
 751            indoc! {"
 752            The ˇquick brown
 753            fox jumps over
 754            the lazy dog."},
 755            Mode::Normal,
 756        );
 757
 758        // test multi cursor delete surrounds
 759        cx.set_state(
 760            indoc! {"
 761            The [quˇick] brown
 762            fox jumps over
 763            the [laˇzy] dog."},
 764            Mode::Normal,
 765        );
 766        cx.simulate_keystrokes("d s ]");
 767        cx.assert_state(
 768            indoc! {"
 769            The ˇquick brown
 770            fox jumps over
 771            the ˇlazy dog."},
 772            Mode::Normal,
 773        );
 774
 775        // test multi cursor delete surrounds with arround
 776        cx.set_state(
 777            indoc! {"
 778            Tˇhe [ quick ] brown
 779            fox jumps over
 780            the [laˇzy] dog."},
 781            Mode::Normal,
 782        );
 783        cx.simulate_keystrokes("d s [");
 784        cx.assert_state(
 785            indoc! {"
 786            The ˇquick brown
 787            fox jumps over
 788            the ˇlazy dog."},
 789            Mode::Normal,
 790        );
 791
 792        cx.set_state(
 793            indoc! {"
 794            Tˇhe [ quick ] brown
 795            fox jumps over
 796            the [laˇzy ] dog."},
 797            Mode::Normal,
 798        );
 799        cx.simulate_keystrokes("d s [");
 800        cx.assert_state(
 801            indoc! {"
 802            The ˇquick brown
 803            fox jumps over
 804            the ˇlazy dog."},
 805            Mode::Normal,
 806        );
 807
 808        // test multi cursor delete different surrounds
 809        // the pair corresponding to the two cursors is the same,
 810        // so they are combined into one cursor
 811        cx.set_state(
 812            indoc! {"
 813            The [quˇick] brown
 814            fox jumps over
 815            the {laˇzy} dog."},
 816            Mode::Normal,
 817        );
 818        cx.simulate_keystrokes("d s {");
 819        cx.assert_state(
 820            indoc! {"
 821            The [quick] brown
 822            fox jumps over
 823            the ˇlazy dog."},
 824            Mode::Normal,
 825        );
 826
 827        // test delete surround with multi cursor and nest surrounds
 828        cx.set_state(
 829            indoc! {"
 830            fn test_surround() {
 831                ifˇ 2 > 1 {
 832                    ˇprintln!(\"it is fine\");
 833                };
 834            }"},
 835            Mode::Normal,
 836        );
 837        cx.simulate_keystrokes("d s }");
 838        cx.assert_state(
 839            indoc! {"
 840            fn test_surround() ˇ
 841                if 2 > 1 ˇ
 842                    println!(\"it is fine\");
 843                ;
 844            "},
 845            Mode::Normal,
 846        );
 847    }
 848
 849    #[gpui::test]
 850    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
 851        let mut cx = VimTestContext::new(cx, true).await;
 852
 853        cx.set_state(
 854            indoc! {"
 855            The {quˇick} brown
 856            fox jumps over
 857            the lazy dog."},
 858            Mode::Normal,
 859        );
 860        cx.simulate_keystrokes("c s { [");
 861        cx.assert_state(
 862            indoc! {"
 863            The ˇ[ quick ] brown
 864            fox jumps over
 865            the lazy dog."},
 866            Mode::Normal,
 867        );
 868
 869        // test multi cursor change surrounds
 870        cx.set_state(
 871            indoc! {"
 872            The {quˇick} brown
 873            fox jumps over
 874            the {laˇzy} dog."},
 875            Mode::Normal,
 876        );
 877        cx.simulate_keystrokes("c s { [");
 878        cx.assert_state(
 879            indoc! {"
 880            The ˇ[ quick ] brown
 881            fox jumps over
 882            the ˇ[ lazy ] dog."},
 883            Mode::Normal,
 884        );
 885
 886        // test multi cursor delete different surrounds with after cursor
 887        cx.set_state(
 888            indoc! {"
 889            Thˇe {quick} brown
 890            fox jumps over
 891            the {laˇzy} dog."},
 892            Mode::Normal,
 893        );
 894        cx.simulate_keystrokes("c s { [");
 895        cx.assert_state(
 896            indoc! {"
 897            The ˇ[ quick ] brown
 898            fox jumps over
 899            the ˇ[ lazy ] dog."},
 900            Mode::Normal,
 901        );
 902
 903        // test multi cursor change surrount with not arround
 904        cx.set_state(
 905            indoc! {"
 906            Thˇe { quick } brown
 907            fox jumps over
 908            the {laˇzy} dog."},
 909            Mode::Normal,
 910        );
 911        cx.simulate_keystrokes("c s { ]");
 912        cx.assert_state(
 913            indoc! {"
 914            The ˇ[quick] brown
 915            fox jumps over
 916            the ˇ[lazy] dog."},
 917            Mode::Normal,
 918        );
 919
 920        // test multi cursor change with not exist surround
 921        cx.set_state(
 922            indoc! {"
 923            The {quˇick} brown
 924            fox jumps over
 925            the [laˇzy] dog."},
 926            Mode::Normal,
 927        );
 928        cx.simulate_keystrokes("c s [ '");
 929        cx.assert_state(
 930            indoc! {"
 931            The {quick} brown
 932            fox jumps over
 933            the ˇ'lazy' dog."},
 934            Mode::Normal,
 935        );
 936
 937        // test change nesting surrounds
 938        cx.set_state(
 939            indoc! {"
 940            fn test_surround() {
 941                ifˇ 2 > 1 {
 942                    ˇprintln!(\"it is fine\");
 943                }
 944            };"},
 945            Mode::Normal,
 946        );
 947        cx.simulate_keystrokes("c s { [");
 948        cx.assert_state(
 949            indoc! {"
 950            fn test_surround() ˇ[
 951                if 2 > 1 ˇ[
 952                    println!(\"it is fine\");
 953                ]
 954            ];"},
 955            Mode::Normal,
 956        );
 957    }
 958
 959    #[gpui::test]
 960    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
 961        let mut cx = VimTestContext::new(cx, true).await;
 962
 963        cx.set_state(
 964            indoc! {"
 965            The quˇick brown
 966            fox jumps over
 967            the lazy dog."},
 968            Mode::Normal,
 969        );
 970        cx.simulate_keystrokes("y s i w [");
 971        cx.assert_state(
 972            indoc! {"
 973            The ˇ[ quick ] brown
 974            fox jumps over
 975            the lazy dog."},
 976            Mode::Normal,
 977        );
 978
 979        cx.simulate_keystrokes("c s [ }");
 980        cx.assert_state(
 981            indoc! {"
 982            The ˇ{quick} brown
 983            fox jumps over
 984            the lazy dog."},
 985            Mode::Normal,
 986        );
 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        cx.simulate_keystrokes("u");
 998        cx.assert_state(
 999            indoc! {"
1000            The ˇ{quick} brown
1001            fox jumps over
1002            the lazy dog."},
1003            Mode::Normal,
1004        );
1005    }
1006}