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