surrounds.rs

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