surrounds.rs

   1use crate::{
   2    Vim,
   3    motion::{self, Motion},
   4    object::Object,
   5    state::Mode,
   6};
   7use editor::{Bias, movement};
   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(cx, |_, editor, 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, None)
  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(Default::default(), 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(cx, |_, editor, 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) =
 154                        pair_object.range(&display_map, selection.clone(), true, None)
 155                    {
 156                        // If the current parenthesis object is single-line,
 157                        // then we need to filter whether it is the current line or not
 158                        if !pair_object.is_multiline() {
 159                            let is_same_row = selection.start.row() == range.start.row()
 160                                && selection.end.row() == range.end.row();
 161                            if !is_same_row {
 162                                anchors.push(start..start);
 163                                continue;
 164                            }
 165                        }
 166                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
 167                        // hello«ˇ  "hello in a word"  »again.
 168                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
 169                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
 170                        let mut chars_and_offset = display_map
 171                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 172                            .peekable();
 173                        while let Some((ch, offset)) = chars_and_offset.next() {
 174                            if ch.to_string() == pair.start {
 175                                let start = offset;
 176                                let mut end = start + 1;
 177                                if surround
 178                                    && let Some((next_ch, _)) = chars_and_offset.peek()
 179                                        && next_ch.eq(&' ') {
 180                                            end += 1;
 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                                    && let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
 196                                        && next_ch.eq(&' ') {
 197                                            start -= 1;
 198                                        }
 199                                edits.push((start..end, ""));
 200                                break;
 201                            }
 202                        }
 203                    } else {
 204                        anchors.push(start..start);
 205                    }
 206                }
 207
 208                editor.change_selections(Default::default(), window, cx, |s| {
 209                    s.select_ranges(anchors);
 210                });
 211                edits.sort_by_key(|(range, _)| range.start);
 212                editor.edit(edits, cx);
 213                editor.set_clip_at_line_ends(true, cx);
 214            });
 215        });
 216    }
 217
 218    pub fn change_surrounds(
 219        &mut self,
 220        text: Arc<str>,
 221        target: Object,
 222        window: &mut Window,
 223        cx: &mut Context<Self>,
 224    ) {
 225        if let Some(will_replace_pair) = object_to_bracket_pair(target) {
 226            self.stop_recording(cx);
 227            self.update_editor(cx, |_, editor, cx| {
 228                editor.transact(window, cx, |editor, window, cx| {
 229                    editor.set_clip_at_line_ends(false, cx);
 230
 231                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 232                        Some(pair) => pair.clone(),
 233                        None => BracketPair {
 234                            start: text.to_string(),
 235                            end: text.to_string(),
 236                            close: true,
 237                            surround: true,
 238                            newline: false,
 239                        },
 240                    };
 241                    let surround = pair.end != surround_alias((*text).as_ref());
 242                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 243                    let mut edits = Vec::new();
 244                    let mut anchors = Vec::new();
 245
 246                    for selection in &selections {
 247                        let start = selection.start.to_offset(&display_map, Bias::Left);
 248                        if let Some(range) =
 249                            target.range(&display_map, selection.clone(), true, None)
 250                        {
 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(Default::default(), 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(cx, |_, editor, 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) =
 352                            object.range(&display_map, selection.clone(), true, None)
 353                        {
 354                            // If the current parenthesis object is single-line,
 355                            // then we need to filter whether it is the current line or not
 356                            if object.is_multiline()
 357                                || (!object.is_multiline()
 358                                    && selection.start.row() == range.start.row()
 359                                    && selection.end.row() == range.end.row())
 360                            {
 361                                valid = true;
 362                                let chars_and_offset = display_map
 363                                    .buffer_chars_at(
 364                                        range.start.to_offset(&display_map, Bias::Left),
 365                                    )
 366                                    .peekable();
 367                                for (ch, offset) in chars_and_offset {
 368                                    if ch.to_string() == pair.start {
 369                                        anchors.push(offset..offset);
 370                                        break;
 371                                    }
 372                                }
 373                            } else {
 374                                anchors.push(start..start)
 375                            }
 376                        } else {
 377                            anchors.push(start..start)
 378                        }
 379                    }
 380                    editor.change_selections(Default::default(), window, cx, |s| {
 381                        s.select_ranges(anchors);
 382                    });
 383                    editor.set_clip_at_line_ends(true, cx);
 384                });
 385            });
 386        }
 387        valid
 388    }
 389}
 390
 391fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 392    pairs
 393        .iter()
 394        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
 395}
 396
 397fn surround_alias(ch: &str) -> &str {
 398    match ch {
 399        "b" => ")",
 400        "B" => "}",
 401        "a" => ">",
 402        "r" => "]",
 403        _ => ch,
 404    }
 405}
 406
 407fn all_support_surround_pair() -> Vec<BracketPair> {
 408    vec![
 409        BracketPair {
 410            start: "{".into(),
 411            end: "}".into(),
 412            close: true,
 413            surround: true,
 414            newline: false,
 415        },
 416        BracketPair {
 417            start: "'".into(),
 418            end: "'".into(),
 419            close: true,
 420            surround: true,
 421            newline: false,
 422        },
 423        BracketPair {
 424            start: "`".into(),
 425            end: "`".into(),
 426            close: true,
 427            surround: true,
 428            newline: false,
 429        },
 430        BracketPair {
 431            start: "\"".into(),
 432            end: "\"".into(),
 433            close: true,
 434            surround: true,
 435            newline: false,
 436        },
 437        BracketPair {
 438            start: "(".into(),
 439            end: ")".into(),
 440            close: true,
 441            surround: true,
 442            newline: false,
 443        },
 444        BracketPair {
 445            start: "|".into(),
 446            end: "|".into(),
 447            close: true,
 448            surround: true,
 449            newline: false,
 450        },
 451        BracketPair {
 452            start: "[".into(),
 453            end: "]".into(),
 454            close: true,
 455            surround: true,
 456            newline: false,
 457        },
 458        BracketPair {
 459            start: "{".into(),
 460            end: "}".into(),
 461            close: true,
 462            surround: true,
 463            newline: false,
 464        },
 465        BracketPair {
 466            start: "<".into(),
 467            end: ">".into(),
 468            close: true,
 469            surround: true,
 470            newline: false,
 471        },
 472    ]
 473}
 474
 475fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 476    match pair.start.as_str() {
 477        "'" => Some(Object::Quotes),
 478        "`" => Some(Object::BackQuotes),
 479        "\"" => Some(Object::DoubleQuotes),
 480        "|" => Some(Object::VerticalBars),
 481        "(" => Some(Object::Parentheses),
 482        "[" => Some(Object::SquareBrackets),
 483        "{" => Some(Object::CurlyBrackets),
 484        "<" => Some(Object::AngleBrackets),
 485        _ => None,
 486    }
 487}
 488
 489fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
 490    match object {
 491        Object::Quotes => Some(BracketPair {
 492            start: "'".to_string(),
 493            end: "'".to_string(),
 494            close: true,
 495            surround: true,
 496            newline: false,
 497        }),
 498        Object::BackQuotes => Some(BracketPair {
 499            start: "`".to_string(),
 500            end: "`".to_string(),
 501            close: true,
 502            surround: true,
 503            newline: false,
 504        }),
 505        Object::DoubleQuotes => Some(BracketPair {
 506            start: "\"".to_string(),
 507            end: "\"".to_string(),
 508            close: true,
 509            surround: true,
 510            newline: false,
 511        }),
 512        Object::VerticalBars => Some(BracketPair {
 513            start: "|".to_string(),
 514            end: "|".to_string(),
 515            close: true,
 516            surround: true,
 517            newline: false,
 518        }),
 519        Object::Parentheses => Some(BracketPair {
 520            start: "(".to_string(),
 521            end: ")".to_string(),
 522            close: true,
 523            surround: true,
 524            newline: false,
 525        }),
 526        Object::SquareBrackets => Some(BracketPair {
 527            start: "[".to_string(),
 528            end: "]".to_string(),
 529            close: true,
 530            surround: true,
 531            newline: false,
 532        }),
 533        Object::CurlyBrackets => Some(BracketPair {
 534            start: "{".to_string(),
 535            end: "}".to_string(),
 536            close: true,
 537            surround: true,
 538            newline: false,
 539        }),
 540        Object::AngleBrackets => Some(BracketPair {
 541            start: "<".to_string(),
 542            end: ">".to_string(),
 543            close: true,
 544            surround: true,
 545            newline: false,
 546        }),
 547        _ => None,
 548    }
 549}
 550
 551#[cfg(test)]
 552mod test {
 553    use gpui::KeyBinding;
 554    use indoc::indoc;
 555
 556    use crate::{PushAddSurrounds, state::Mode, test::VimTestContext};
 557
 558    #[gpui::test]
 559    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 560        let mut cx = VimTestContext::new(cx, true).await;
 561
 562        // test add surrounds with around
 563        cx.set_state(
 564            indoc! {"
 565            The quˇick brown
 566            fox jumps over
 567            the lazy dog."},
 568            Mode::Normal,
 569        );
 570        cx.simulate_keystrokes("y s i w {");
 571        cx.assert_state(
 572            indoc! {"
 573            The ˇ{ quick } brown
 574            fox jumps over
 575            the lazy dog."},
 576            Mode::Normal,
 577        );
 578
 579        // test add surrounds not with around
 580        cx.set_state(
 581            indoc! {"
 582            The quˇick brown
 583            fox jumps over
 584            the lazy dog."},
 585            Mode::Normal,
 586        );
 587        cx.simulate_keystrokes("y s i w }");
 588        cx.assert_state(
 589            indoc! {"
 590            The ˇ{quick} brown
 591            fox jumps over
 592            the lazy dog."},
 593            Mode::Normal,
 594        );
 595
 596        // test add surrounds with motion
 597        cx.set_state(
 598            indoc! {"
 599            The quˇick brown
 600            fox jumps over
 601            the lazy dog."},
 602            Mode::Normal,
 603        );
 604        cx.simulate_keystrokes("y s $ }");
 605        cx.assert_state(
 606            indoc! {"
 607            The quˇ{ick brown}
 608            fox jumps over
 609            the lazy dog."},
 610            Mode::Normal,
 611        );
 612
 613        // test add surrounds with multi cursor
 614        cx.set_state(
 615            indoc! {"
 616            The quˇick brown
 617            fox jumps over
 618            the laˇzy dog."},
 619            Mode::Normal,
 620        );
 621        cx.simulate_keystrokes("y s i w '");
 622        cx.assert_state(
 623            indoc! {"
 624            The ˇ'quick' brown
 625            fox jumps over
 626            the ˇ'lazy' dog."},
 627            Mode::Normal,
 628        );
 629
 630        // test multi cursor add surrounds with motion
 631        cx.set_state(
 632            indoc! {"
 633            The quˇick brown
 634            fox jumps over
 635            the laˇzy dog."},
 636            Mode::Normal,
 637        );
 638        cx.simulate_keystrokes("y s $ '");
 639        cx.assert_state(
 640            indoc! {"
 641            The quˇ'ick brown'
 642            fox jumps over
 643            the laˇ'zy dog.'"},
 644            Mode::Normal,
 645        );
 646
 647        // test multi cursor add surrounds with motion and custom string
 648        cx.set_state(
 649            indoc! {"
 650            The quˇick brown
 651            fox jumps over
 652            the laˇzy dog."},
 653            Mode::Normal,
 654        );
 655        cx.simulate_keystrokes("y s $ 1");
 656        cx.assert_state(
 657            indoc! {"
 658            The quˇ1ick brown1
 659            fox jumps over
 660            the laˇ1zy dog.1"},
 661            Mode::Normal,
 662        );
 663
 664        // test add surrounds with motion current line
 665        cx.set_state(
 666            indoc! {"
 667            The quˇick brown
 668            fox jumps over
 669            the lazy dog."},
 670            Mode::Normal,
 671        );
 672        cx.simulate_keystrokes("y s s {");
 673        cx.assert_state(
 674            indoc! {"
 675            ˇ{ The quick brown }
 676            fox jumps over
 677            the lazy dog."},
 678            Mode::Normal,
 679        );
 680
 681        cx.set_state(
 682            indoc! {"
 683                The quˇick brown•
 684            fox jumps over
 685            the lazy dog."},
 686            Mode::Normal,
 687        );
 688        cx.simulate_keystrokes("y s s {");
 689        cx.assert_state(
 690            indoc! {"
 691                ˇ{ The quick brown }•
 692            fox jumps over
 693            the lazy dog."},
 694            Mode::Normal,
 695        );
 696        cx.simulate_keystrokes("2 y s s )");
 697        cx.assert_state(
 698            indoc! {"
 699                ˇ({ The quick brown }•
 700            fox jumps over)
 701            the lazy dog."},
 702            Mode::Normal,
 703        );
 704
 705        // test add surrounds around object
 706        cx.set_state(
 707            indoc! {"
 708            The [quˇick] brown
 709            fox jumps over
 710            the lazy dog."},
 711            Mode::Normal,
 712        );
 713        cx.simulate_keystrokes("y s a ] )");
 714        cx.assert_state(
 715            indoc! {"
 716            The ˇ([quick]) brown
 717            fox jumps over
 718            the lazy dog."},
 719            Mode::Normal,
 720        );
 721
 722        // test add surrounds inside object
 723        cx.set_state(
 724            indoc! {"
 725            The [quˇick] brown
 726            fox jumps over
 727            the lazy dog."},
 728            Mode::Normal,
 729        );
 730        cx.simulate_keystrokes("y s i ] )");
 731        cx.assert_state(
 732            indoc! {"
 733            The [ˇ(quick)] brown
 734            fox jumps over
 735            the lazy dog."},
 736            Mode::Normal,
 737        );
 738    }
 739
 740    #[gpui::test]
 741    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 742        let mut cx = VimTestContext::new(cx, true).await;
 743
 744        cx.update(|_, cx| {
 745            cx.bind_keys([KeyBinding::new(
 746                "shift-s",
 747                PushAddSurrounds {},
 748                Some("vim_mode == visual"),
 749            )])
 750        });
 751
 752        // test add surrounds with around
 753        cx.set_state(
 754            indoc! {"
 755            The quˇick brown
 756            fox jumps over
 757            the lazy dog."},
 758            Mode::Normal,
 759        );
 760        cx.simulate_keystrokes("v i w shift-s {");
 761        cx.assert_state(
 762            indoc! {"
 763            The ˇ{ quick } brown
 764            fox jumps over
 765            the lazy dog."},
 766            Mode::Normal,
 767        );
 768
 769        // test add surrounds not with around
 770        cx.set_state(
 771            indoc! {"
 772            The quˇick brown
 773            fox jumps over
 774            the lazy dog."},
 775            Mode::Normal,
 776        );
 777        cx.simulate_keystrokes("v i w shift-s }");
 778        cx.assert_state(
 779            indoc! {"
 780            The ˇ{quick} brown
 781            fox jumps over
 782            the lazy dog."},
 783            Mode::Normal,
 784        );
 785
 786        // test add surrounds with motion
 787        cx.set_state(
 788            indoc! {"
 789            The quˇick brown
 790            fox jumps over
 791            the lazy dog."},
 792            Mode::Normal,
 793        );
 794        cx.simulate_keystrokes("v e shift-s }");
 795        cx.assert_state(
 796            indoc! {"
 797            The quˇ{ick} brown
 798            fox jumps over
 799            the lazy dog."},
 800            Mode::Normal,
 801        );
 802
 803        // test add surrounds with multi cursor
 804        cx.set_state(
 805            indoc! {"
 806            The quˇick brown
 807            fox jumps over
 808            the laˇzy dog."},
 809            Mode::Normal,
 810        );
 811        cx.simulate_keystrokes("v i w shift-s '");
 812        cx.assert_state(
 813            indoc! {"
 814            The ˇ'quick' brown
 815            fox jumps over
 816            the ˇ'lazy' dog."},
 817            Mode::Normal,
 818        );
 819
 820        // test add surrounds with visual block
 821        cx.set_state(
 822            indoc! {"
 823            The quˇick brown
 824            fox jumps over
 825            the lazy dog."},
 826            Mode::Normal,
 827        );
 828        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 829        cx.assert_state(
 830            indoc! {"
 831            The ˇ'quick' brown
 832            fox 'jumps' over
 833            the 'lazy 'dog."},
 834            Mode::Normal,
 835        );
 836
 837        // test add surrounds with visual line
 838        cx.set_state(
 839            indoc! {"
 840            The quˇick brown
 841            fox jumps over
 842            the lazy dog."},
 843            Mode::Normal,
 844        );
 845        cx.simulate_keystrokes("j shift-v shift-s '");
 846        cx.assert_state(
 847            indoc! {"
 848            The quick brown
 849            ˇ'
 850            fox jumps over
 851            '
 852            the lazy dog."},
 853            Mode::Normal,
 854        );
 855    }
 856
 857    #[gpui::test]
 858    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 859        let mut cx = VimTestContext::new(cx, true).await;
 860
 861        // test delete surround
 862        cx.set_state(
 863            indoc! {"
 864            The {quˇick} brown
 865            fox jumps over
 866            the lazy dog."},
 867            Mode::Normal,
 868        );
 869        cx.simulate_keystrokes("d s {");
 870        cx.assert_state(
 871            indoc! {"
 872            The ˇquick brown
 873            fox jumps over
 874            the lazy dog."},
 875            Mode::Normal,
 876        );
 877
 878        // test delete not exist surrounds
 879        cx.set_state(
 880            indoc! {"
 881            The {quˇick} brown
 882            fox jumps over
 883            the lazy dog."},
 884            Mode::Normal,
 885        );
 886        cx.simulate_keystrokes("d s [");
 887        cx.assert_state(
 888            indoc! {"
 889            The {quˇick} brown
 890            fox jumps over
 891            the lazy dog."},
 892            Mode::Normal,
 893        );
 894
 895        // test delete surround forward exist, in the surrounds plugin of other editors,
 896        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 897        cx.set_state(
 898            indoc! {"
 899            The {quick} brˇown
 900            fox jumps over
 901            the lazy dog."},
 902            Mode::Normal,
 903        );
 904        cx.simulate_keystrokes("d s {");
 905        cx.assert_state(
 906            indoc! {"
 907            The {quick} brˇown
 908            fox jumps over
 909            the lazy dog."},
 910            Mode::Normal,
 911        );
 912
 913        // test cursor delete inner surrounds
 914        cx.set_state(
 915            indoc! {"
 916            The { quick brown
 917            fox jumˇps over }
 918            the lazy dog."},
 919            Mode::Normal,
 920        );
 921        cx.simulate_keystrokes("d s {");
 922        cx.assert_state(
 923            indoc! {"
 924            The ˇquick brown
 925            fox jumps over
 926            the lazy dog."},
 927            Mode::Normal,
 928        );
 929
 930        // test multi cursor delete surrounds
 931        cx.set_state(
 932            indoc! {"
 933            The [quˇick] brown
 934            fox jumps over
 935            the [laˇzy] dog."},
 936            Mode::Normal,
 937        );
 938        cx.simulate_keystrokes("d s ]");
 939        cx.assert_state(
 940            indoc! {"
 941            The ˇquick brown
 942            fox jumps over
 943            the ˇlazy dog."},
 944            Mode::Normal,
 945        );
 946
 947        // test multi cursor delete surrounds with around
 948        cx.set_state(
 949            indoc! {"
 950            Tˇhe [ quick ] brown
 951            fox jumps over
 952            the [laˇzy] dog."},
 953            Mode::Normal,
 954        );
 955        cx.simulate_keystrokes("d s [");
 956        cx.assert_state(
 957            indoc! {"
 958            The ˇquick brown
 959            fox jumps over
 960            the ˇlazy dog."},
 961            Mode::Normal,
 962        );
 963
 964        cx.set_state(
 965            indoc! {"
 966            Tˇhe [ quick ] brown
 967            fox jumps over
 968            the [laˇzy ] dog."},
 969            Mode::Normal,
 970        );
 971        cx.simulate_keystrokes("d s [");
 972        cx.assert_state(
 973            indoc! {"
 974            The ˇquick brown
 975            fox jumps over
 976            the ˇlazy dog."},
 977            Mode::Normal,
 978        );
 979
 980        // test multi cursor delete different surrounds
 981        // the pair corresponding to the two cursors is the same,
 982        // so they are combined into one cursor
 983        cx.set_state(
 984            indoc! {"
 985            The [quˇick] brown
 986            fox jumps over
 987            the {laˇzy} dog."},
 988            Mode::Normal,
 989        );
 990        cx.simulate_keystrokes("d s {");
 991        cx.assert_state(
 992            indoc! {"
 993            The [quick] brown
 994            fox jumps over
 995            the ˇlazy dog."},
 996            Mode::Normal,
 997        );
 998
 999        // test delete surround with multi cursor and nest surrounds
1000        cx.set_state(
1001            indoc! {"
1002            fn test_surround() {
1003                ifˇ 2 > 1 {
1004                    ˇprintln!(\"it is fine\");
1005                };
1006            }"},
1007            Mode::Normal,
1008        );
1009        cx.simulate_keystrokes("d s }");
1010        cx.assert_state(
1011            indoc! {"
1012            fn test_surround() ˇ
1013                if 2 > 1 ˇ
1014                    println!(\"it is fine\");
1015                ;
1016            "},
1017            Mode::Normal,
1018        );
1019    }
1020
1021    #[gpui::test]
1022    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1023        let mut cx = VimTestContext::new(cx, true).await;
1024
1025        cx.set_state(
1026            indoc! {"
1027            The {quˇick} brown
1028            fox jumps over
1029            the lazy dog."},
1030            Mode::Normal,
1031        );
1032        cx.simulate_keystrokes("c s { [");
1033        cx.assert_state(
1034            indoc! {"
1035            The ˇ[ quick ] brown
1036            fox jumps over
1037            the lazy dog."},
1038            Mode::Normal,
1039        );
1040
1041        // test multi cursor change surrounds
1042        cx.set_state(
1043            indoc! {"
1044            The {quˇick} brown
1045            fox jumps over
1046            the {laˇzy} dog."},
1047            Mode::Normal,
1048        );
1049        cx.simulate_keystrokes("c s { [");
1050        cx.assert_state(
1051            indoc! {"
1052            The ˇ[ quick ] brown
1053            fox jumps over
1054            the ˇ[ lazy ] dog."},
1055            Mode::Normal,
1056        );
1057
1058        // test multi cursor delete different surrounds with after cursor
1059        cx.set_state(
1060            indoc! {"
1061            Thˇe {quick} brown
1062            fox jumps over
1063            the {laˇzy} dog."},
1064            Mode::Normal,
1065        );
1066        cx.simulate_keystrokes("c s { [");
1067        cx.assert_state(
1068            indoc! {"
1069            The ˇ[ quick ] brown
1070            fox jumps over
1071            the ˇ[ lazy ] dog."},
1072            Mode::Normal,
1073        );
1074
1075        // test multi cursor change surrount with not around
1076        cx.set_state(
1077            indoc! {"
1078            Thˇe { quick } brown
1079            fox jumps over
1080            the {laˇzy} dog."},
1081            Mode::Normal,
1082        );
1083        cx.simulate_keystrokes("c s { ]");
1084        cx.assert_state(
1085            indoc! {"
1086            The ˇ[quick] brown
1087            fox jumps over
1088            the ˇ[lazy] dog."},
1089            Mode::Normal,
1090        );
1091
1092        // test multi cursor change with not exist surround
1093        cx.set_state(
1094            indoc! {"
1095            The {quˇick} brown
1096            fox jumps over
1097            the [laˇzy] dog."},
1098            Mode::Normal,
1099        );
1100        cx.simulate_keystrokes("c s [ '");
1101        cx.assert_state(
1102            indoc! {"
1103            The {quick} brown
1104            fox jumps over
1105            the ˇ'lazy' dog."},
1106            Mode::Normal,
1107        );
1108
1109        // test change nesting surrounds
1110        cx.set_state(
1111            indoc! {"
1112            fn test_surround() {
1113                ifˇ 2 > 1 {
1114                    ˇprintln!(\"it is fine\");
1115                }
1116            };"},
1117            Mode::Normal,
1118        );
1119        cx.simulate_keystrokes("c s { [");
1120        cx.assert_state(
1121            indoc! {"
1122            fn test_surround() ˇ[
1123                if 2 > 1 ˇ[
1124                    println!(\"it is fine\");
1125                ]
1126            ];"},
1127            Mode::Normal,
1128        );
1129    }
1130
1131    #[gpui::test]
1132    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1133        let mut cx = VimTestContext::new(cx, true).await;
1134
1135        cx.set_state(
1136            indoc! {"
1137            The quˇick brown
1138            fox jumps over
1139            the lazy dog."},
1140            Mode::Normal,
1141        );
1142        cx.simulate_keystrokes("y s i w [");
1143        cx.assert_state(
1144            indoc! {"
1145            The ˇ[ quick ] brown
1146            fox jumps over
1147            the lazy dog."},
1148            Mode::Normal,
1149        );
1150
1151        cx.simulate_keystrokes("c s [ }");
1152        cx.assert_state(
1153            indoc! {"
1154            The ˇ{quick} brown
1155            fox jumps over
1156            the lazy dog."},
1157            Mode::Normal,
1158        );
1159
1160        cx.simulate_keystrokes("d s {");
1161        cx.assert_state(
1162            indoc! {"
1163            The ˇquick brown
1164            fox jumps over
1165            the lazy dog."},
1166            Mode::Normal,
1167        );
1168
1169        cx.simulate_keystrokes("u");
1170        cx.assert_state(
1171            indoc! {"
1172            The ˇ{quick} brown
1173            fox jumps over
1174            the lazy dog."},
1175            Mode::Normal,
1176        );
1177    }
1178
1179    #[gpui::test]
1180    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1181        let mut cx = VimTestContext::new(cx, true).await;
1182
1183        // add aliases
1184        cx.set_state(
1185            indoc! {"
1186            The quˇick brown
1187            fox jumps over
1188            the lazy dog."},
1189            Mode::Normal,
1190        );
1191        cx.simulate_keystrokes("y s i w b");
1192        cx.assert_state(
1193            indoc! {"
1194            The ˇ(quick) brown
1195            fox jumps over
1196            the lazy dog."},
1197            Mode::Normal,
1198        );
1199
1200        cx.set_state(
1201            indoc! {"
1202            The quˇick brown
1203            fox jumps over
1204            the lazy dog."},
1205            Mode::Normal,
1206        );
1207        cx.simulate_keystrokes("y s i w B");
1208        cx.assert_state(
1209            indoc! {"
1210            The ˇ{quick} brown
1211            fox jumps over
1212            the lazy dog."},
1213            Mode::Normal,
1214        );
1215
1216        cx.set_state(
1217            indoc! {"
1218            The quˇick brown
1219            fox jumps over
1220            the lazy dog."},
1221            Mode::Normal,
1222        );
1223        cx.simulate_keystrokes("y s i w a");
1224        cx.assert_state(
1225            indoc! {"
1226            The ˇ<quick> brown
1227            fox jumps over
1228            the lazy dog."},
1229            Mode::Normal,
1230        );
1231
1232        cx.set_state(
1233            indoc! {"
1234            The quˇick brown
1235            fox jumps over
1236            the lazy dog."},
1237            Mode::Normal,
1238        );
1239        cx.simulate_keystrokes("y s i w r");
1240        cx.assert_state(
1241            indoc! {"
1242            The ˇ[quick] brown
1243            fox jumps over
1244            the lazy dog."},
1245            Mode::Normal,
1246        );
1247
1248        // change aliases
1249        cx.set_state(
1250            indoc! {"
1251            The {quˇick} brown
1252            fox jumps over
1253            the lazy dog."},
1254            Mode::Normal,
1255        );
1256        cx.simulate_keystrokes("c s { b");
1257        cx.assert_state(
1258            indoc! {"
1259            The ˇ(quick) brown
1260            fox jumps over
1261            the lazy dog."},
1262            Mode::Normal,
1263        );
1264
1265        cx.set_state(
1266            indoc! {"
1267            The (quˇick) brown
1268            fox jumps over
1269            the lazy dog."},
1270            Mode::Normal,
1271        );
1272        cx.simulate_keystrokes("c s ( B");
1273        cx.assert_state(
1274            indoc! {"
1275            The ˇ{quick} brown
1276            fox jumps over
1277            the lazy dog."},
1278            Mode::Normal,
1279        );
1280
1281        cx.set_state(
1282            indoc! {"
1283            The (quˇick) brown
1284            fox jumps over
1285            the lazy dog."},
1286            Mode::Normal,
1287        );
1288        cx.simulate_keystrokes("c s ( a");
1289        cx.assert_state(
1290            indoc! {"
1291            The ˇ<quick> brown
1292            fox jumps over
1293            the lazy dog."},
1294            Mode::Normal,
1295        );
1296
1297        cx.set_state(
1298            indoc! {"
1299            The <quˇick> brown
1300            fox jumps over
1301            the lazy dog."},
1302            Mode::Normal,
1303        );
1304        cx.simulate_keystrokes("c s < b");
1305        cx.assert_state(
1306            indoc! {"
1307            The ˇ(quick) brown
1308            fox jumps over
1309            the lazy dog."},
1310            Mode::Normal,
1311        );
1312
1313        cx.set_state(
1314            indoc! {"
1315            The (quˇick) brown
1316            fox jumps over
1317            the lazy dog."},
1318            Mode::Normal,
1319        );
1320        cx.simulate_keystrokes("c s ( r");
1321        cx.assert_state(
1322            indoc! {"
1323            The ˇ[quick] brown
1324            fox jumps over
1325            the lazy dog."},
1326            Mode::Normal,
1327        );
1328
1329        cx.set_state(
1330            indoc! {"
1331            The [quˇick] brown
1332            fox jumps over
1333            the lazy dog."},
1334            Mode::Normal,
1335        );
1336        cx.simulate_keystrokes("c s [ b");
1337        cx.assert_state(
1338            indoc! {"
1339            The ˇ(quick) brown
1340            fox jumps over
1341            the lazy dog."},
1342            Mode::Normal,
1343        );
1344
1345        // delete alias
1346        cx.set_state(
1347            indoc! {"
1348            The {quˇick} brown
1349            fox jumps over
1350            the lazy dog."},
1351            Mode::Normal,
1352        );
1353        cx.simulate_keystrokes("d s B");
1354        cx.assert_state(
1355            indoc! {"
1356            The ˇquick brown
1357            fox jumps over
1358            the lazy dog."},
1359            Mode::Normal,
1360        );
1361
1362        cx.set_state(
1363            indoc! {"
1364            The (quˇick) brown
1365            fox jumps over
1366            the lazy dog."},
1367            Mode::Normal,
1368        );
1369        cx.simulate_keystrokes("d s b");
1370        cx.assert_state(
1371            indoc! {"
1372            The ˇquick brown
1373            fox jumps over
1374            the lazy dog."},
1375            Mode::Normal,
1376        );
1377
1378        cx.set_state(
1379            indoc! {"
1380            The [quˇick] brown
1381            fox jumps over
1382            the lazy dog."},
1383            Mode::Normal,
1384        );
1385        cx.simulate_keystrokes("d s r");
1386        cx.assert_state(
1387            indoc! {"
1388            The ˇquick brown
1389            fox jumps over
1390            the lazy dog."},
1391            Mode::Normal,
1392        );
1393
1394        cx.set_state(
1395            indoc! {"
1396            The <quˇick> brown
1397            fox jumps over
1398            the lazy dog."},
1399            Mode::Normal,
1400        );
1401        cx.simulate_keystrokes("d s a");
1402        cx.assert_state(
1403            indoc! {"
1404            The ˇquick brown
1405            fox jumps over
1406            the lazy dog."},
1407            Mode::Normal,
1408        );
1409    }
1410}