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::{state::Mode, test::VimTestContext, PushAddSurrounds};
 558
 559    #[gpui::test]
 560    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 561        let mut cx = VimTestContext::new(cx, true).await;
 562
 563        // test add surrounds with around
 564        cx.set_state(
 565            indoc! {"
 566            The quˇick brown
 567            fox jumps over
 568            the lazy dog."},
 569            Mode::Normal,
 570        );
 571        cx.simulate_keystrokes("y s i w {");
 572        cx.assert_state(
 573            indoc! {"
 574            The ˇ{ quick } brown
 575            fox jumps over
 576            the lazy dog."},
 577            Mode::Normal,
 578        );
 579
 580        // test add surrounds not with around
 581        cx.set_state(
 582            indoc! {"
 583            The quˇick brown
 584            fox jumps over
 585            the lazy dog."},
 586            Mode::Normal,
 587        );
 588        cx.simulate_keystrokes("y s i w }");
 589        cx.assert_state(
 590            indoc! {"
 591            The ˇ{quick} brown
 592            fox jumps over
 593            the lazy dog."},
 594            Mode::Normal,
 595        );
 596
 597        // test add surrounds with motion
 598        cx.set_state(
 599            indoc! {"
 600            The quˇick brown
 601            fox jumps over
 602            the lazy dog."},
 603            Mode::Normal,
 604        );
 605        cx.simulate_keystrokes("y s $ }");
 606        cx.assert_state(
 607            indoc! {"
 608            The quˇ{ick brown}
 609            fox jumps over
 610            the lazy dog."},
 611            Mode::Normal,
 612        );
 613
 614        // test add surrounds with multi cursor
 615        cx.set_state(
 616            indoc! {"
 617            The quˇick brown
 618            fox jumps over
 619            the laˇzy dog."},
 620            Mode::Normal,
 621        );
 622        cx.simulate_keystrokes("y s i w '");
 623        cx.assert_state(
 624            indoc! {"
 625            The ˇ'quick' brown
 626            fox jumps over
 627            the ˇ'lazy' dog."},
 628            Mode::Normal,
 629        );
 630
 631        // test multi cursor add surrounds with motion
 632        cx.set_state(
 633            indoc! {"
 634            The quˇick brown
 635            fox jumps over
 636            the laˇzy dog."},
 637            Mode::Normal,
 638        );
 639        cx.simulate_keystrokes("y s $ '");
 640        cx.assert_state(
 641            indoc! {"
 642            The quˇ'ick brown'
 643            fox jumps over
 644            the laˇ'zy dog.'"},
 645            Mode::Normal,
 646        );
 647
 648        // test multi cursor add surrounds with motion and custom string
 649        cx.set_state(
 650            indoc! {"
 651            The quˇick brown
 652            fox jumps over
 653            the laˇzy dog."},
 654            Mode::Normal,
 655        );
 656        cx.simulate_keystrokes("y s $ 1");
 657        cx.assert_state(
 658            indoc! {"
 659            The quˇ1ick brown1
 660            fox jumps over
 661            the laˇ1zy dog.1"},
 662            Mode::Normal,
 663        );
 664
 665        // test add surrounds with motion current line
 666        cx.set_state(
 667            indoc! {"
 668            The quˇick brown
 669            fox jumps over
 670            the lazy dog."},
 671            Mode::Normal,
 672        );
 673        cx.simulate_keystrokes("y s s {");
 674        cx.assert_state(
 675            indoc! {"
 676            ˇ{ The quick brown }
 677            fox jumps over
 678            the lazy dog."},
 679            Mode::Normal,
 680        );
 681
 682        cx.set_state(
 683            indoc! {"
 684                The quˇick brown•
 685            fox jumps over
 686            the lazy dog."},
 687            Mode::Normal,
 688        );
 689        cx.simulate_keystrokes("y s s {");
 690        cx.assert_state(
 691            indoc! {"
 692                ˇ{ The quick brown }•
 693            fox jumps over
 694            the lazy dog."},
 695            Mode::Normal,
 696        );
 697        cx.simulate_keystrokes("2 y s s )");
 698        cx.assert_state(
 699            indoc! {"
 700                ˇ({ The quick brown }•
 701            fox jumps over)
 702            the lazy dog."},
 703            Mode::Normal,
 704        );
 705
 706        // test add surrounds around object
 707        cx.set_state(
 708            indoc! {"
 709            The [quˇick] brown
 710            fox jumps over
 711            the lazy dog."},
 712            Mode::Normal,
 713        );
 714        cx.simulate_keystrokes("y s a ] )");
 715        cx.assert_state(
 716            indoc! {"
 717            The ˇ([quick]) brown
 718            fox jumps over
 719            the lazy dog."},
 720            Mode::Normal,
 721        );
 722
 723        // test add surrounds inside object
 724        cx.set_state(
 725            indoc! {"
 726            The [quˇick] brown
 727            fox jumps over
 728            the lazy dog."},
 729            Mode::Normal,
 730        );
 731        cx.simulate_keystrokes("y s i ] )");
 732        cx.assert_state(
 733            indoc! {"
 734            The [ˇ(quick)] brown
 735            fox jumps over
 736            the lazy dog."},
 737            Mode::Normal,
 738        );
 739    }
 740
 741    #[gpui::test]
 742    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 743        let mut cx = VimTestContext::new(cx, true).await;
 744
 745        cx.update(|_, cx| {
 746            cx.bind_keys([KeyBinding::new(
 747                "shift-s",
 748                PushAddSurrounds {},
 749                Some("vim_mode == visual"),
 750            )])
 751        });
 752
 753        // test add surrounds with around
 754        cx.set_state(
 755            indoc! {"
 756            The quˇick brown
 757            fox jumps over
 758            the lazy dog."},
 759            Mode::Normal,
 760        );
 761        cx.simulate_keystrokes("v i w shift-s {");
 762        cx.assert_state(
 763            indoc! {"
 764            The ˇ{ quick } brown
 765            fox jumps over
 766            the lazy dog."},
 767            Mode::Normal,
 768        );
 769
 770        // test add surrounds not with around
 771        cx.set_state(
 772            indoc! {"
 773            The quˇick brown
 774            fox jumps over
 775            the lazy dog."},
 776            Mode::Normal,
 777        );
 778        cx.simulate_keystrokes("v i w shift-s }");
 779        cx.assert_state(
 780            indoc! {"
 781            The ˇ{quick} brown
 782            fox jumps over
 783            the lazy dog."},
 784            Mode::Normal,
 785        );
 786
 787        // test add surrounds with motion
 788        cx.set_state(
 789            indoc! {"
 790            The quˇick brown
 791            fox jumps over
 792            the lazy dog."},
 793            Mode::Normal,
 794        );
 795        cx.simulate_keystrokes("v e shift-s }");
 796        cx.assert_state(
 797            indoc! {"
 798            The quˇ{ick} brown
 799            fox jumps over
 800            the lazy dog."},
 801            Mode::Normal,
 802        );
 803
 804        // test add surrounds with multi cursor
 805        cx.set_state(
 806            indoc! {"
 807            The quˇick brown
 808            fox jumps over
 809            the laˇzy dog."},
 810            Mode::Normal,
 811        );
 812        cx.simulate_keystrokes("v i w shift-s '");
 813        cx.assert_state(
 814            indoc! {"
 815            The ˇ'quick' brown
 816            fox jumps over
 817            the ˇ'lazy' dog."},
 818            Mode::Normal,
 819        );
 820
 821        // test add surrounds with visual block
 822        cx.set_state(
 823            indoc! {"
 824            The quˇick brown
 825            fox jumps over
 826            the lazy dog."},
 827            Mode::Normal,
 828        );
 829        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 830        cx.assert_state(
 831            indoc! {"
 832            The ˇ'quick' brown
 833            fox 'jumps' over
 834            the 'lazy 'dog."},
 835            Mode::Normal,
 836        );
 837
 838        // test add surrounds with visual line
 839        cx.set_state(
 840            indoc! {"
 841            The quˇick brown
 842            fox jumps over
 843            the lazy dog."},
 844            Mode::Normal,
 845        );
 846        cx.simulate_keystrokes("j shift-v shift-s '");
 847        cx.assert_state(
 848            indoc! {"
 849            The quick brown
 850            ˇ'
 851            fox jumps over
 852            '
 853            the lazy dog."},
 854            Mode::Normal,
 855        );
 856    }
 857
 858    #[gpui::test]
 859    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 860        let mut cx = VimTestContext::new(cx, true).await;
 861
 862        // test delete surround
 863        cx.set_state(
 864            indoc! {"
 865            The {quˇick} brown
 866            fox jumps over
 867            the lazy dog."},
 868            Mode::Normal,
 869        );
 870        cx.simulate_keystrokes("d s {");
 871        cx.assert_state(
 872            indoc! {"
 873            The ˇquick brown
 874            fox jumps over
 875            the lazy dog."},
 876            Mode::Normal,
 877        );
 878
 879        // test delete not exist surrounds
 880        cx.set_state(
 881            indoc! {"
 882            The {quˇick} brown
 883            fox jumps over
 884            the lazy dog."},
 885            Mode::Normal,
 886        );
 887        cx.simulate_keystrokes("d s [");
 888        cx.assert_state(
 889            indoc! {"
 890            The {quˇick} brown
 891            fox jumps over
 892            the lazy dog."},
 893            Mode::Normal,
 894        );
 895
 896        // test delete surround forward exist, in the surrounds plugin of other editors,
 897        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 898        cx.set_state(
 899            indoc! {"
 900            The {quick} brˇown
 901            fox jumps over
 902            the lazy dog."},
 903            Mode::Normal,
 904        );
 905        cx.simulate_keystrokes("d s {");
 906        cx.assert_state(
 907            indoc! {"
 908            The {quick} brˇown
 909            fox jumps over
 910            the lazy dog."},
 911            Mode::Normal,
 912        );
 913
 914        // test cursor delete inner surrounds
 915        cx.set_state(
 916            indoc! {"
 917            The { quick brown
 918            fox jumˇps over }
 919            the lazy dog."},
 920            Mode::Normal,
 921        );
 922        cx.simulate_keystrokes("d s {");
 923        cx.assert_state(
 924            indoc! {"
 925            The ˇquick brown
 926            fox jumps over
 927            the lazy dog."},
 928            Mode::Normal,
 929        );
 930
 931        // test multi cursor delete surrounds
 932        cx.set_state(
 933            indoc! {"
 934            The [quˇick] brown
 935            fox jumps over
 936            the [laˇzy] dog."},
 937            Mode::Normal,
 938        );
 939        cx.simulate_keystrokes("d s ]");
 940        cx.assert_state(
 941            indoc! {"
 942            The ˇquick brown
 943            fox jumps over
 944            the ˇlazy dog."},
 945            Mode::Normal,
 946        );
 947
 948        // test multi cursor delete surrounds with around
 949        cx.set_state(
 950            indoc! {"
 951            Tˇhe [ quick ] brown
 952            fox jumps over
 953            the [laˇzy] dog."},
 954            Mode::Normal,
 955        );
 956        cx.simulate_keystrokes("d s [");
 957        cx.assert_state(
 958            indoc! {"
 959            The ˇquick brown
 960            fox jumps over
 961            the ˇlazy dog."},
 962            Mode::Normal,
 963        );
 964
 965        cx.set_state(
 966            indoc! {"
 967            Tˇhe [ quick ] brown
 968            fox jumps over
 969            the [laˇzy ] dog."},
 970            Mode::Normal,
 971        );
 972        cx.simulate_keystrokes("d s [");
 973        cx.assert_state(
 974            indoc! {"
 975            The ˇquick brown
 976            fox jumps over
 977            the ˇlazy dog."},
 978            Mode::Normal,
 979        );
 980
 981        // test multi cursor delete different surrounds
 982        // the pair corresponding to the two cursors is the same,
 983        // so they are combined into one cursor
 984        cx.set_state(
 985            indoc! {"
 986            The [quˇick] brown
 987            fox jumps over
 988            the {laˇzy} dog."},
 989            Mode::Normal,
 990        );
 991        cx.simulate_keystrokes("d s {");
 992        cx.assert_state(
 993            indoc! {"
 994            The [quick] brown
 995            fox jumps over
 996            the ˇlazy dog."},
 997            Mode::Normal,
 998        );
 999
1000        // test delete surround with multi cursor and nest surrounds
1001        cx.set_state(
1002            indoc! {"
1003            fn test_surround() {
1004                ifˇ 2 > 1 {
1005                    ˇprintln!(\"it is fine\");
1006                };
1007            }"},
1008            Mode::Normal,
1009        );
1010        cx.simulate_keystrokes("d s }");
1011        cx.assert_state(
1012            indoc! {"
1013            fn test_surround() ˇ
1014                if 2 > 1 ˇ
1015                    println!(\"it is fine\");
1016                ;
1017            "},
1018            Mode::Normal,
1019        );
1020    }
1021
1022    #[gpui::test]
1023    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1024        let mut cx = VimTestContext::new(cx, true).await;
1025
1026        cx.set_state(
1027            indoc! {"
1028            The {quˇick} brown
1029            fox jumps over
1030            the lazy dog."},
1031            Mode::Normal,
1032        );
1033        cx.simulate_keystrokes("c s { [");
1034        cx.assert_state(
1035            indoc! {"
1036            The ˇ[ quick ] brown
1037            fox jumps over
1038            the lazy dog."},
1039            Mode::Normal,
1040        );
1041
1042        // test multi cursor change surrounds
1043        cx.set_state(
1044            indoc! {"
1045            The {quˇick} brown
1046            fox jumps over
1047            the {laˇzy} dog."},
1048            Mode::Normal,
1049        );
1050        cx.simulate_keystrokes("c s { [");
1051        cx.assert_state(
1052            indoc! {"
1053            The ˇ[ quick ] brown
1054            fox jumps over
1055            the ˇ[ lazy ] dog."},
1056            Mode::Normal,
1057        );
1058
1059        // test multi cursor delete different surrounds with after cursor
1060        cx.set_state(
1061            indoc! {"
1062            Thˇe {quick} brown
1063            fox jumps over
1064            the {laˇzy} dog."},
1065            Mode::Normal,
1066        );
1067        cx.simulate_keystrokes("c s { [");
1068        cx.assert_state(
1069            indoc! {"
1070            The ˇ[ quick ] brown
1071            fox jumps over
1072            the ˇ[ lazy ] dog."},
1073            Mode::Normal,
1074        );
1075
1076        // test multi cursor change surrount with not around
1077        cx.set_state(
1078            indoc! {"
1079            Thˇe { quick } brown
1080            fox jumps over
1081            the {laˇzy} dog."},
1082            Mode::Normal,
1083        );
1084        cx.simulate_keystrokes("c s { ]");
1085        cx.assert_state(
1086            indoc! {"
1087            The ˇ[quick] brown
1088            fox jumps over
1089            the ˇ[lazy] dog."},
1090            Mode::Normal,
1091        );
1092
1093        // test multi cursor change with not exist surround
1094        cx.set_state(
1095            indoc! {"
1096            The {quˇick} brown
1097            fox jumps over
1098            the [laˇzy] dog."},
1099            Mode::Normal,
1100        );
1101        cx.simulate_keystrokes("c s [ '");
1102        cx.assert_state(
1103            indoc! {"
1104            The {quick} brown
1105            fox jumps over
1106            the ˇ'lazy' dog."},
1107            Mode::Normal,
1108        );
1109
1110        // test change nesting surrounds
1111        cx.set_state(
1112            indoc! {"
1113            fn test_surround() {
1114                ifˇ 2 > 1 {
1115                    ˇprintln!(\"it is fine\");
1116                }
1117            };"},
1118            Mode::Normal,
1119        );
1120        cx.simulate_keystrokes("c s { [");
1121        cx.assert_state(
1122            indoc! {"
1123            fn test_surround() ˇ[
1124                if 2 > 1 ˇ[
1125                    println!(\"it is fine\");
1126                ]
1127            ];"},
1128            Mode::Normal,
1129        );
1130    }
1131
1132    #[gpui::test]
1133    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1134        let mut cx = VimTestContext::new(cx, true).await;
1135
1136        cx.set_state(
1137            indoc! {"
1138            The quˇick brown
1139            fox jumps over
1140            the lazy dog."},
1141            Mode::Normal,
1142        );
1143        cx.simulate_keystrokes("y s i w [");
1144        cx.assert_state(
1145            indoc! {"
1146            The ˇ[ quick ] brown
1147            fox jumps over
1148            the lazy dog."},
1149            Mode::Normal,
1150        );
1151
1152        cx.simulate_keystrokes("c s [ }");
1153        cx.assert_state(
1154            indoc! {"
1155            The ˇ{quick} brown
1156            fox jumps over
1157            the lazy dog."},
1158            Mode::Normal,
1159        );
1160
1161        cx.simulate_keystrokes("d s {");
1162        cx.assert_state(
1163            indoc! {"
1164            The ˇquick brown
1165            fox jumps over
1166            the lazy dog."},
1167            Mode::Normal,
1168        );
1169
1170        cx.simulate_keystrokes("u");
1171        cx.assert_state(
1172            indoc! {"
1173            The ˇ{quick} brown
1174            fox jumps over
1175            the lazy dog."},
1176            Mode::Normal,
1177        );
1178    }
1179
1180    #[gpui::test]
1181    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1182        let mut cx = VimTestContext::new(cx, true).await;
1183
1184        // add aliases
1185        cx.set_state(
1186            indoc! {"
1187            The quˇick brown
1188            fox jumps over
1189            the lazy dog."},
1190            Mode::Normal,
1191        );
1192        cx.simulate_keystrokes("y s i w b");
1193        cx.assert_state(
1194            indoc! {"
1195            The ˇ(quick) brown
1196            fox jumps over
1197            the lazy dog."},
1198            Mode::Normal,
1199        );
1200
1201        cx.set_state(
1202            indoc! {"
1203            The quˇick brown
1204            fox jumps over
1205            the lazy dog."},
1206            Mode::Normal,
1207        );
1208        cx.simulate_keystrokes("y s i w B");
1209        cx.assert_state(
1210            indoc! {"
1211            The ˇ{quick} brown
1212            fox jumps over
1213            the lazy dog."},
1214            Mode::Normal,
1215        );
1216
1217        cx.set_state(
1218            indoc! {"
1219            The quˇick brown
1220            fox jumps over
1221            the lazy dog."},
1222            Mode::Normal,
1223        );
1224        cx.simulate_keystrokes("y s i w a");
1225        cx.assert_state(
1226            indoc! {"
1227            The ˇ<quick> brown
1228            fox jumps over
1229            the lazy dog."},
1230            Mode::Normal,
1231        );
1232
1233        cx.set_state(
1234            indoc! {"
1235            The quˇick brown
1236            fox jumps over
1237            the lazy dog."},
1238            Mode::Normal,
1239        );
1240        cx.simulate_keystrokes("y s i w r");
1241        cx.assert_state(
1242            indoc! {"
1243            The ˇ[quick] brown
1244            fox jumps over
1245            the lazy dog."},
1246            Mode::Normal,
1247        );
1248
1249        // change aliases
1250        cx.set_state(
1251            indoc! {"
1252            The {quˇick} brown
1253            fox jumps over
1254            the lazy dog."},
1255            Mode::Normal,
1256        );
1257        cx.simulate_keystrokes("c s { b");
1258        cx.assert_state(
1259            indoc! {"
1260            The ˇ(quick) brown
1261            fox jumps over
1262            the lazy dog."},
1263            Mode::Normal,
1264        );
1265
1266        cx.set_state(
1267            indoc! {"
1268            The (quˇick) brown
1269            fox jumps over
1270            the lazy dog."},
1271            Mode::Normal,
1272        );
1273        cx.simulate_keystrokes("c s ( B");
1274        cx.assert_state(
1275            indoc! {"
1276            The ˇ{quick} brown
1277            fox jumps over
1278            the lazy dog."},
1279            Mode::Normal,
1280        );
1281
1282        cx.set_state(
1283            indoc! {"
1284            The (quˇick) brown
1285            fox jumps over
1286            the lazy dog."},
1287            Mode::Normal,
1288        );
1289        cx.simulate_keystrokes("c s ( a");
1290        cx.assert_state(
1291            indoc! {"
1292            The ˇ<quick> brown
1293            fox jumps over
1294            the lazy dog."},
1295            Mode::Normal,
1296        );
1297
1298        cx.set_state(
1299            indoc! {"
1300            The <quˇick> brown
1301            fox jumps over
1302            the lazy dog."},
1303            Mode::Normal,
1304        );
1305        cx.simulate_keystrokes("c s < b");
1306        cx.assert_state(
1307            indoc! {"
1308            The ˇ(quick) brown
1309            fox jumps over
1310            the lazy dog."},
1311            Mode::Normal,
1312        );
1313
1314        cx.set_state(
1315            indoc! {"
1316            The (quˇick) brown
1317            fox jumps over
1318            the lazy dog."},
1319            Mode::Normal,
1320        );
1321        cx.simulate_keystrokes("c s ( r");
1322        cx.assert_state(
1323            indoc! {"
1324            The ˇ[quick] brown
1325            fox jumps over
1326            the lazy dog."},
1327            Mode::Normal,
1328        );
1329
1330        cx.set_state(
1331            indoc! {"
1332            The [quˇick] brown
1333            fox jumps over
1334            the lazy dog."},
1335            Mode::Normal,
1336        );
1337        cx.simulate_keystrokes("c s [ b");
1338        cx.assert_state(
1339            indoc! {"
1340            The ˇ(quick) brown
1341            fox jumps over
1342            the lazy dog."},
1343            Mode::Normal,
1344        );
1345
1346        // delete alias
1347        cx.set_state(
1348            indoc! {"
1349            The {quˇick} brown
1350            fox jumps over
1351            the lazy dog."},
1352            Mode::Normal,
1353        );
1354        cx.simulate_keystrokes("d s B");
1355        cx.assert_state(
1356            indoc! {"
1357            The ˇquick brown
1358            fox jumps over
1359            the lazy dog."},
1360            Mode::Normal,
1361        );
1362
1363        cx.set_state(
1364            indoc! {"
1365            The (quˇick) brown
1366            fox jumps over
1367            the lazy dog."},
1368            Mode::Normal,
1369        );
1370        cx.simulate_keystrokes("d s b");
1371        cx.assert_state(
1372            indoc! {"
1373            The ˇquick brown
1374            fox jumps over
1375            the lazy dog."},
1376            Mode::Normal,
1377        );
1378
1379        cx.set_state(
1380            indoc! {"
1381            The [quˇick] brown
1382            fox jumps over
1383            the lazy dog."},
1384            Mode::Normal,
1385        );
1386        cx.simulate_keystrokes("d s r");
1387        cx.assert_state(
1388            indoc! {"
1389            The ˇquick brown
1390            fox jumps over
1391            the lazy dog."},
1392            Mode::Normal,
1393        );
1394
1395        cx.set_state(
1396            indoc! {"
1397            The <quˇick> brown
1398            fox jumps over
1399            the lazy dog."},
1400            Mode::Normal,
1401        );
1402        cx.simulate_keystrokes("d s a");
1403        cx.assert_state(
1404            indoc! {"
1405            The ˇquick brown
1406            fox jumps over
1407            the lazy dog."},
1408            Mode::Normal,
1409        );
1410    }
1411}