surrounds.rs

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