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                                {
 181                                    end += 1;
 182                                }
 183                                edits.push((start..end, ""));
 184                                anchors.push(start..start);
 185                                break;
 186                            }
 187                        }
 188                        let mut reverse_chars_and_offsets = display_map
 189                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
 190                            .peekable();
 191                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 192                            if ch.to_string() == pair.end {
 193                                let mut start = offset;
 194                                let end = start + 1;
 195                                if surround
 196                                    && let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
 197                                    && next_ch.eq(&' ')
 198                                {
 199                                    start -= 1;
 200                                }
 201                                edits.push((start..end, ""));
 202                                break;
 203                            }
 204                        }
 205                    } else {
 206                        anchors.push(start..start);
 207                    }
 208                }
 209
 210                editor.change_selections(Default::default(), window, cx, |s| {
 211                    s.select_ranges(anchors);
 212                });
 213                edits.sort_by_key(|(range, _)| range.start);
 214                editor.edit(edits, cx);
 215                editor.set_clip_at_line_ends(true, cx);
 216            });
 217        });
 218    }
 219
 220    pub fn change_surrounds(
 221        &mut self,
 222        text: Arc<str>,
 223        target: Object,
 224        window: &mut Window,
 225        cx: &mut Context<Self>,
 226    ) {
 227        if let Some(will_replace_pair) = object_to_bracket_pair(target) {
 228            self.stop_recording(cx);
 229            self.update_editor(cx, |_, editor, cx| {
 230                editor.transact(window, cx, |editor, window, cx| {
 231                    editor.set_clip_at_line_ends(false, cx);
 232
 233                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 234                        Some(pair) => pair.clone(),
 235                        None => BracketPair {
 236                            start: text.to_string(),
 237                            end: text.to_string(),
 238                            close: true,
 239                            surround: true,
 240                            newline: false,
 241                        },
 242                    };
 243                    let surround = pair.end != surround_alias((*text).as_ref());
 244                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 245                    let mut edits = Vec::new();
 246                    let mut anchors = Vec::new();
 247
 248                    for selection in &selections {
 249                        let start = selection.start.to_offset(&display_map, Bias::Left);
 250                        if let Some(range) =
 251                            target.range(&display_map, selection.clone(), true, None)
 252                        {
 253                            if !target.is_multiline() {
 254                                let is_same_row = selection.start.row() == range.start.row()
 255                                    && selection.end.row() == range.end.row();
 256                                if !is_same_row {
 257                                    anchors.push(start..start);
 258                                    continue;
 259                                }
 260                            }
 261                            let mut chars_and_offset = display_map
 262                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 263                                .peekable();
 264                            while let Some((ch, offset)) = chars_and_offset.next() {
 265                                if ch.to_string() == will_replace_pair.start {
 266                                    let mut open_str = pair.start.clone();
 267                                    let start = offset;
 268                                    let mut end = start + 1;
 269                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 270                                        // If the next position is already a space or line break,
 271                                        // we don't need to splice another space even under around
 272                                        if surround && !next_ch.is_whitespace() {
 273                                            open_str.push(' ');
 274                                        } else if !surround && next_ch.to_string() == " " {
 275                                            end += 1;
 276                                        }
 277                                    }
 278                                    edits.push((start..end, open_str));
 279                                    anchors.push(start..start);
 280                                    break;
 281                                }
 282                            }
 283
 284                            let mut reverse_chars_and_offsets = display_map
 285                                .reverse_buffer_chars_at(
 286                                    range.end.to_offset(&display_map, Bias::Left),
 287                                )
 288                                .peekable();
 289                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 290                                if ch.to_string() == will_replace_pair.end {
 291                                    let mut close_str = pair.end.clone();
 292                                    let mut start = offset;
 293                                    let end = start + 1;
 294                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 295                                        if surround && !next_ch.is_whitespace() {
 296                                            close_str.insert(0, ' ')
 297                                        } else if !surround && next_ch.to_string() == " " {
 298                                            start -= 1;
 299                                        }
 300                                    }
 301                                    edits.push((start..end, close_str));
 302                                    break;
 303                                }
 304                            }
 305                        } else {
 306                            anchors.push(start..start);
 307                        }
 308                    }
 309
 310                    let stable_anchors = editor
 311                        .selections
 312                        .disjoint_anchors()
 313                        .iter()
 314                        .map(|selection| {
 315                            let start = selection.start.bias_left(&display_map.buffer_snapshot);
 316                            start..start
 317                        })
 318                        .collect::<Vec<_>>();
 319                    edits.sort_by_key(|(range, _)| range.start);
 320                    editor.edit(edits, cx);
 321                    editor.set_clip_at_line_ends(true, cx);
 322                    editor.change_selections(Default::default(), window, cx, |s| {
 323                        s.select_anchor_ranges(stable_anchors);
 324                    });
 325                });
 326            });
 327        }
 328    }
 329
 330    /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
 331    ///
 332    /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
 333    /// A pair of brackets is considered valid if it is well-formed and properly closed.
 334    ///
 335    /// 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.
 336    /// If no valid pair of brackets is found for any cursor, the method returns `false`.
 337    pub fn check_and_move_to_valid_bracket_pair(
 338        &mut self,
 339        object: Object,
 340        window: &mut Window,
 341        cx: &mut Context<Self>,
 342    ) -> bool {
 343        let mut valid = false;
 344        if let Some(pair) = object_to_bracket_pair(object) {
 345            self.update_editor(cx, |_, editor, cx| {
 346                editor.transact(window, cx, |editor, window, cx| {
 347                    editor.set_clip_at_line_ends(false, cx);
 348                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 349                    let mut anchors = Vec::new();
 350
 351                    for selection in &selections {
 352                        let start = selection.start.to_offset(&display_map, Bias::Left);
 353                        if let Some(range) =
 354                            object.range(&display_map, selection.clone(), true, None)
 355                        {
 356                            // If the current parenthesis object is single-line,
 357                            // then we need to filter whether it is the current line or not
 358                            if object.is_multiline()
 359                                || (!object.is_multiline()
 360                                    && selection.start.row() == range.start.row()
 361                                    && selection.end.row() == range.end.row())
 362                            {
 363                                valid = true;
 364                                let chars_and_offset = display_map
 365                                    .buffer_chars_at(
 366                                        range.start.to_offset(&display_map, Bias::Left),
 367                                    )
 368                                    .peekable();
 369                                for (ch, offset) in chars_and_offset {
 370                                    if ch.to_string() == pair.start {
 371                                        anchors.push(offset..offset);
 372                                        break;
 373                                    }
 374                                }
 375                            } else {
 376                                anchors.push(start..start)
 377                            }
 378                        } else {
 379                            anchors.push(start..start)
 380                        }
 381                    }
 382                    editor.change_selections(Default::default(), window, cx, |s| {
 383                        s.select_ranges(anchors);
 384                    });
 385                    editor.set_clip_at_line_ends(true, cx);
 386                });
 387            });
 388        }
 389        valid
 390    }
 391}
 392
 393fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 394    pairs
 395        .iter()
 396        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
 397}
 398
 399fn surround_alias(ch: &str) -> &str {
 400    match ch {
 401        "b" => ")",
 402        "B" => "}",
 403        "a" => ">",
 404        "r" => "]",
 405        _ => ch,
 406    }
 407}
 408
 409fn all_support_surround_pair() -> Vec<BracketPair> {
 410    vec![
 411        BracketPair {
 412            start: "{".into(),
 413            end: "}".into(),
 414            close: true,
 415            surround: true,
 416            newline: false,
 417        },
 418        BracketPair {
 419            start: "'".into(),
 420            end: "'".into(),
 421            close: true,
 422            surround: true,
 423            newline: false,
 424        },
 425        BracketPair {
 426            start: "`".into(),
 427            end: "`".into(),
 428            close: true,
 429            surround: true,
 430            newline: false,
 431        },
 432        BracketPair {
 433            start: "\"".into(),
 434            end: "\"".into(),
 435            close: true,
 436            surround: true,
 437            newline: false,
 438        },
 439        BracketPair {
 440            start: "(".into(),
 441            end: ")".into(),
 442            close: true,
 443            surround: true,
 444            newline: false,
 445        },
 446        BracketPair {
 447            start: "|".into(),
 448            end: "|".into(),
 449            close: true,
 450            surround: true,
 451            newline: false,
 452        },
 453        BracketPair {
 454            start: "[".into(),
 455            end: "]".into(),
 456            close: true,
 457            surround: true,
 458            newline: false,
 459        },
 460        BracketPair {
 461            start: "{".into(),
 462            end: "}".into(),
 463            close: true,
 464            surround: true,
 465            newline: false,
 466        },
 467        BracketPair {
 468            start: "<".into(),
 469            end: ">".into(),
 470            close: true,
 471            surround: true,
 472            newline: false,
 473        },
 474    ]
 475}
 476
 477fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 478    match pair.start.as_str() {
 479        "'" => Some(Object::Quotes),
 480        "`" => Some(Object::BackQuotes),
 481        "\"" => Some(Object::DoubleQuotes),
 482        "|" => Some(Object::VerticalBars),
 483        "(" => Some(Object::Parentheses),
 484        "[" => Some(Object::SquareBrackets),
 485        "{" => Some(Object::CurlyBrackets),
 486        "<" => Some(Object::AngleBrackets),
 487        _ => None,
 488    }
 489}
 490
 491fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
 492    match object {
 493        Object::Quotes => Some(BracketPair {
 494            start: "'".to_string(),
 495            end: "'".to_string(),
 496            close: true,
 497            surround: true,
 498            newline: false,
 499        }),
 500        Object::BackQuotes => Some(BracketPair {
 501            start: "`".to_string(),
 502            end: "`".to_string(),
 503            close: true,
 504            surround: true,
 505            newline: false,
 506        }),
 507        Object::DoubleQuotes => Some(BracketPair {
 508            start: "\"".to_string(),
 509            end: "\"".to_string(),
 510            close: true,
 511            surround: true,
 512            newline: false,
 513        }),
 514        Object::VerticalBars => Some(BracketPair {
 515            start: "|".to_string(),
 516            end: "|".to_string(),
 517            close: true,
 518            surround: true,
 519            newline: false,
 520        }),
 521        Object::Parentheses => Some(BracketPair {
 522            start: "(".to_string(),
 523            end: ")".to_string(),
 524            close: true,
 525            surround: true,
 526            newline: false,
 527        }),
 528        Object::SquareBrackets => Some(BracketPair {
 529            start: "[".to_string(),
 530            end: "]".to_string(),
 531            close: true,
 532            surround: true,
 533            newline: false,
 534        }),
 535        Object::CurlyBrackets => Some(BracketPair {
 536            start: "{".to_string(),
 537            end: "}".to_string(),
 538            close: true,
 539            surround: true,
 540            newline: false,
 541        }),
 542        Object::AngleBrackets => Some(BracketPair {
 543            start: "<".to_string(),
 544            end: ">".to_string(),
 545            close: true,
 546            surround: true,
 547            newline: false,
 548        }),
 549        _ => None,
 550    }
 551}
 552
 553#[cfg(test)]
 554mod test {
 555    use gpui::KeyBinding;
 556    use indoc::indoc;
 557
 558    use crate::{PushAddSurrounds, state::Mode, test::VimTestContext};
 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                PushAddSurrounds {},
 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}