surrounds.rs

   1use crate::{
   2    Vim,
   3    motion::{self, Motion},
   4    object::{Object, surrounding_markers},
   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) = self.object_to_bracket_pair(target, cx) {
 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 after
 245                    // and before the surround pairs.
 246                    // Space is only added in the following cases:
 247                    // - new surround is not quote and is opening bracket (({[<)
 248                    // - new surround is quote and original was also quote
 249                    let surround = if pair.start != pair.end {
 250                        pair.end != surround_alias((*text).as_ref())
 251                    } else {
 252                        will_replace_pair.start == will_replace_pair.end
 253                    };
 254
 255                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 256                    let mut edits = Vec::new();
 257                    let mut anchors = Vec::new();
 258
 259                    for selection in &selections {
 260                        let start = selection.start.to_offset(&display_map, Bias::Left);
 261                        if let Some(range) =
 262                            target.range(&display_map, selection.clone(), true, None)
 263                        {
 264                            if !target.is_multiline() {
 265                                let is_same_row = selection.start.row() == range.start.row()
 266                                    && selection.end.row() == range.end.row();
 267                                if !is_same_row {
 268                                    anchors.push(start..start);
 269                                    continue;
 270                                }
 271                            }
 272                            let mut chars_and_offset = display_map
 273                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 274                                .peekable();
 275                            while let Some((ch, offset)) = chars_and_offset.next() {
 276                                if ch.to_string() == will_replace_pair.start {
 277                                    let mut open_str = pair.start.clone();
 278                                    let start = offset;
 279                                    let mut end = start + 1;
 280                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 281                                        // If the next position is already a space or line break,
 282                                        // we don't need to splice another space even under around
 283                                        if surround && !next_ch.is_whitespace() {
 284                                            open_str.push(' ');
 285                                        } else if !surround && next_ch.to_string() == " " {
 286                                            end += 1;
 287                                        }
 288                                    }
 289                                    edits.push((start..end, open_str));
 290                                    anchors.push(start..start);
 291                                    break;
 292                                }
 293                            }
 294
 295                            let mut reverse_chars_and_offsets = display_map
 296                                .reverse_buffer_chars_at(
 297                                    range.end.to_offset(&display_map, Bias::Left),
 298                                )
 299                                .peekable();
 300                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 301                                if ch.to_string() == will_replace_pair.end {
 302                                    let mut close_str = pair.end.clone();
 303                                    let mut start = offset;
 304                                    let end = start + 1;
 305                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 306                                        if surround && !next_ch.is_whitespace() {
 307                                            close_str.insert(0, ' ')
 308                                        } else if !surround && next_ch.to_string() == " " {
 309                                            start -= 1;
 310                                        }
 311                                    }
 312                                    edits.push((start..end, close_str));
 313                                    break;
 314                                }
 315                            }
 316                        } else {
 317                            anchors.push(start..start);
 318                        }
 319                    }
 320
 321                    let stable_anchors = editor
 322                        .selections
 323                        .disjoint_anchors_arc()
 324                        .iter()
 325                        .map(|selection| {
 326                            let start = selection.start.bias_left(&display_map.buffer_snapshot());
 327                            start..start
 328                        })
 329                        .collect::<Vec<_>>();
 330                    edits.sort_by_key(|(range, _)| range.start);
 331                    editor.edit(edits, cx);
 332                    editor.set_clip_at_line_ends(true, cx);
 333                    editor.change_selections(Default::default(), window, cx, |s| {
 334                        s.select_anchor_ranges(stable_anchors);
 335                    });
 336                });
 337            });
 338        }
 339    }
 340
 341    /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
 342    ///
 343    /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
 344    /// A pair of brackets is considered valid if it is well-formed and properly closed.
 345    ///
 346    /// 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.
 347    /// If no valid pair of brackets is found for any cursor, the method returns `false`.
 348    pub fn check_and_move_to_valid_bracket_pair(
 349        &mut self,
 350        object: Object,
 351        window: &mut Window,
 352        cx: &mut Context<Self>,
 353    ) -> bool {
 354        let mut valid = false;
 355        if let Some(pair) = self.object_to_bracket_pair(object, cx) {
 356            self.update_editor(cx, |_, editor, cx| {
 357                editor.transact(window, cx, |editor, window, cx| {
 358                    editor.set_clip_at_line_ends(false, cx);
 359                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 360                    let mut anchors = Vec::new();
 361
 362                    for selection in &selections {
 363                        let start = selection.start.to_offset(&display_map, Bias::Left);
 364                        if let Some(range) =
 365                            object.range(&display_map, selection.clone(), true, None)
 366                        {
 367                            // If the current parenthesis object is single-line,
 368                            // then we need to filter whether it is the current line or not
 369                            if object.is_multiline()
 370                                || (!object.is_multiline()
 371                                    && selection.start.row() == range.start.row()
 372                                    && selection.end.row() == range.end.row())
 373                            {
 374                                valid = true;
 375                                let chars_and_offset = display_map
 376                                    .buffer_chars_at(
 377                                        range.start.to_offset(&display_map, Bias::Left),
 378                                    )
 379                                    .peekable();
 380                                for (ch, offset) in chars_and_offset {
 381                                    if ch.to_string() == pair.start {
 382                                        anchors.push(offset..offset);
 383                                        break;
 384                                    }
 385                                }
 386                            } else {
 387                                anchors.push(start..start)
 388                            }
 389                        } else {
 390                            anchors.push(start..start)
 391                        }
 392                    }
 393                    editor.change_selections(Default::default(), window, cx, |s| {
 394                        s.select_ranges(anchors);
 395                    });
 396                    editor.set_clip_at_line_ends(true, cx);
 397                });
 398            });
 399        }
 400        valid
 401    }
 402
 403    fn object_to_bracket_pair(
 404        &self,
 405        object: Object,
 406        cx: &mut Context<Self>,
 407    ) -> Option<BracketPair> {
 408        match object {
 409            Object::Quotes => Some(BracketPair {
 410                start: "'".to_string(),
 411                end: "'".to_string(),
 412                close: true,
 413                surround: true,
 414                newline: false,
 415            }),
 416            Object::BackQuotes => Some(BracketPair {
 417                start: "`".to_string(),
 418                end: "`".to_string(),
 419                close: true,
 420                surround: true,
 421                newline: false,
 422            }),
 423            Object::DoubleQuotes => Some(BracketPair {
 424                start: "\"".to_string(),
 425                end: "\"".to_string(),
 426                close: true,
 427                surround: true,
 428                newline: false,
 429            }),
 430            Object::VerticalBars => Some(BracketPair {
 431                start: "|".to_string(),
 432                end: "|".to_string(),
 433                close: true,
 434                surround: true,
 435                newline: false,
 436            }),
 437            Object::Parentheses => Some(BracketPair {
 438                start: "(".to_string(),
 439                end: ")".to_string(),
 440                close: true,
 441                surround: true,
 442                newline: false,
 443            }),
 444            Object::SquareBrackets => Some(BracketPair {
 445                start: "[".to_string(),
 446                end: "]".to_string(),
 447                close: true,
 448                surround: true,
 449                newline: false,
 450            }),
 451            Object::CurlyBrackets => Some(BracketPair {
 452                start: "{".to_string(),
 453                end: "}".to_string(),
 454                close: true,
 455                surround: true,
 456                newline: false,
 457            }),
 458            Object::AngleBrackets => Some(BracketPair {
 459                start: "<".to_string(),
 460                end: ">".to_string(),
 461                close: true,
 462                surround: true,
 463                newline: false,
 464            }),
 465            Object::AnyBrackets => {
 466                // If we're dealing with `AnyBrackets`, which can map to multiple
 467                // bracket pairs, we'll need to first determine which `BracketPair` to
 468                // target.
 469                // As such, we keep track of the smallest range size, so
 470                // that in cases like `({ name: "John" })` if the cursor is
 471                // inside the curly brackets, we target the curly brackets
 472                // instead of the parentheses.
 473                let mut bracket_pair = None;
 474                let mut min_range_size = usize::MAX;
 475
 476                let _ = self.editor.update(cx, |editor, cx| {
 477                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 478                    // Even if there's multiple cursors, we'll simply rely on
 479                    // the first one to understand what bracket pair to map to.
 480                    // I believe we could, if worth it, go one step above and
 481                    // have a `BracketPair` per selection, so that `AnyBracket`
 482                    // could work in situations where the transformation below
 483                    // could be done.
 484                    //
 485                    // ```
 486                    // (< name:ˇ'Zed' >)
 487                    // <[ name:ˇ'DeltaDB' ]>
 488                    // ```
 489                    //
 490                    // After using `csb{`:
 491                    //
 492                    // ```
 493                    // (ˇ{ name:'Zed' })
 494                    // <ˇ{ name:'DeltaDB' }>
 495                    // ```
 496                    if let Some(selection) = selections.first() {
 497                        let relative_to = selection.head();
 498                        let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
 499                        let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
 500
 501                        for &(open, close) in bracket_pairs.iter() {
 502                            if let Some(range) = surrounding_markers(
 503                                &display_map,
 504                                relative_to,
 505                                true,
 506                                false,
 507                                open,
 508                                close,
 509                            ) {
 510                                let start_offset = range.start.to_offset(&display_map, Bias::Left);
 511                                let end_offset = range.end.to_offset(&display_map, Bias::Right);
 512
 513                                if cursor_offset >= start_offset && cursor_offset <= end_offset {
 514                                    let size = end_offset - start_offset;
 515                                    if size < min_range_size {
 516                                        min_range_size = size;
 517                                        bracket_pair = Some(BracketPair {
 518                                            start: open.to_string(),
 519                                            end: close.to_string(),
 520                                            close: true,
 521                                            surround: true,
 522                                            newline: false,
 523                                        })
 524                                    }
 525                                }
 526                            }
 527                        }
 528                    }
 529                });
 530
 531                bracket_pair
 532            }
 533            _ => None,
 534        }
 535    }
 536}
 537
 538fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 539    pairs
 540        .iter()
 541        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
 542}
 543
 544fn surround_alias(ch: &str) -> &str {
 545    match ch {
 546        "b" => ")",
 547        "B" => "}",
 548        "a" => ">",
 549        "r" => "]",
 550        _ => ch,
 551    }
 552}
 553
 554fn all_support_surround_pair() -> Vec<BracketPair> {
 555    vec![
 556        BracketPair {
 557            start: "{".into(),
 558            end: "}".into(),
 559            close: true,
 560            surround: true,
 561            newline: false,
 562        },
 563        BracketPair {
 564            start: "'".into(),
 565            end: "'".into(),
 566            close: true,
 567            surround: true,
 568            newline: false,
 569        },
 570        BracketPair {
 571            start: "`".into(),
 572            end: "`".into(),
 573            close: true,
 574            surround: true,
 575            newline: false,
 576        },
 577        BracketPair {
 578            start: "\"".into(),
 579            end: "\"".into(),
 580            close: true,
 581            surround: true,
 582            newline: false,
 583        },
 584        BracketPair {
 585            start: "(".into(),
 586            end: ")".into(),
 587            close: true,
 588            surround: true,
 589            newline: false,
 590        },
 591        BracketPair {
 592            start: "|".into(),
 593            end: "|".into(),
 594            close: true,
 595            surround: true,
 596            newline: false,
 597        },
 598        BracketPair {
 599            start: "[".into(),
 600            end: "]".into(),
 601            close: true,
 602            surround: true,
 603            newline: false,
 604        },
 605        BracketPair {
 606            start: "<".into(),
 607            end: ">".into(),
 608            close: true,
 609            surround: true,
 610            newline: false,
 611        },
 612    ]
 613}
 614
 615fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 616    match pair.start.as_str() {
 617        "'" => Some(Object::Quotes),
 618        "`" => Some(Object::BackQuotes),
 619        "\"" => Some(Object::DoubleQuotes),
 620        "|" => Some(Object::VerticalBars),
 621        "(" => Some(Object::Parentheses),
 622        "[" => Some(Object::SquareBrackets),
 623        "{" => Some(Object::CurlyBrackets),
 624        "<" => Some(Object::AngleBrackets),
 625        _ => None,
 626    }
 627}
 628
 629#[cfg(test)]
 630mod test {
 631    use gpui::KeyBinding;
 632    use indoc::indoc;
 633
 634    use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
 635
 636    #[gpui::test]
 637    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 638        let mut cx = VimTestContext::new(cx, true).await;
 639
 640        // test add surrounds with around
 641        cx.set_state(
 642            indoc! {"
 643            The quˇick brown
 644            fox jumps over
 645            the lazy dog."},
 646            Mode::Normal,
 647        );
 648        cx.simulate_keystrokes("y s i w {");
 649        cx.assert_state(
 650            indoc! {"
 651            The ˇ{ quick } brown
 652            fox jumps over
 653            the lazy dog."},
 654            Mode::Normal,
 655        );
 656
 657        // test add surrounds not with around
 658        cx.set_state(
 659            indoc! {"
 660            The quˇick brown
 661            fox jumps over
 662            the lazy dog."},
 663            Mode::Normal,
 664        );
 665        cx.simulate_keystrokes("y s i w }");
 666        cx.assert_state(
 667            indoc! {"
 668            The ˇ{quick} brown
 669            fox jumps over
 670            the lazy dog."},
 671            Mode::Normal,
 672        );
 673
 674        // test add surrounds with motion
 675        cx.set_state(
 676            indoc! {"
 677            The quˇick brown
 678            fox jumps over
 679            the lazy dog."},
 680            Mode::Normal,
 681        );
 682        cx.simulate_keystrokes("y s $ }");
 683        cx.assert_state(
 684            indoc! {"
 685            The quˇ{ick brown}
 686            fox jumps over
 687            the lazy dog."},
 688            Mode::Normal,
 689        );
 690
 691        // test add surrounds with multi cursor
 692        cx.set_state(
 693            indoc! {"
 694            The quˇick brown
 695            fox jumps over
 696            the laˇzy dog."},
 697            Mode::Normal,
 698        );
 699        cx.simulate_keystrokes("y s i w '");
 700        cx.assert_state(
 701            indoc! {"
 702            The ˇ'quick' brown
 703            fox jumps over
 704            the ˇ'lazy' dog."},
 705            Mode::Normal,
 706        );
 707
 708        // test multi cursor add surrounds with motion
 709        cx.set_state(
 710            indoc! {"
 711            The quˇick brown
 712            fox jumps over
 713            the laˇzy dog."},
 714            Mode::Normal,
 715        );
 716        cx.simulate_keystrokes("y s $ '");
 717        cx.assert_state(
 718            indoc! {"
 719            The quˇ'ick brown'
 720            fox jumps over
 721            the laˇ'zy dog.'"},
 722            Mode::Normal,
 723        );
 724
 725        // test multi cursor add surrounds with motion and custom string
 726        cx.set_state(
 727            indoc! {"
 728            The quˇick brown
 729            fox jumps over
 730            the laˇzy dog."},
 731            Mode::Normal,
 732        );
 733        cx.simulate_keystrokes("y s $ 1");
 734        cx.assert_state(
 735            indoc! {"
 736            The quˇ1ick brown1
 737            fox jumps over
 738            the laˇ1zy dog.1"},
 739            Mode::Normal,
 740        );
 741
 742        // test add surrounds with motion current line
 743        cx.set_state(
 744            indoc! {"
 745            The quˇick brown
 746            fox jumps over
 747            the lazy dog."},
 748            Mode::Normal,
 749        );
 750        cx.simulate_keystrokes("y s s {");
 751        cx.assert_state(
 752            indoc! {"
 753            ˇ{ The quick brown }
 754            fox jumps over
 755            the lazy dog."},
 756            Mode::Normal,
 757        );
 758
 759        cx.set_state(
 760            indoc! {"
 761                The quˇick brown•
 762            fox jumps over
 763            the lazy dog."},
 764            Mode::Normal,
 765        );
 766        cx.simulate_keystrokes("y s s {");
 767        cx.assert_state(
 768            indoc! {"
 769                ˇ{ The quick brown }•
 770            fox jumps over
 771            the lazy dog."},
 772            Mode::Normal,
 773        );
 774        cx.simulate_keystrokes("2 y s 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 around object
 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("y s a ] )");
 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 inside object
 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("y s i ] )");
 809        cx.assert_state(
 810            indoc! {"
 811            The [ˇ(quick)] brown
 812            fox jumps over
 813            the lazy dog."},
 814            Mode::Normal,
 815        );
 816    }
 817
 818    #[gpui::test]
 819    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 820        let mut cx = VimTestContext::new(cx, true).await;
 821
 822        cx.update(|_, cx| {
 823            cx.bind_keys([KeyBinding::new(
 824                "shift-s",
 825                PushAddSurrounds {},
 826                Some("vim_mode == visual"),
 827            )])
 828        });
 829
 830        // test add surrounds with around
 831        cx.set_state(
 832            indoc! {"
 833            The quˇick brown
 834            fox jumps over
 835            the lazy dog."},
 836            Mode::Normal,
 837        );
 838        cx.simulate_keystrokes("v i w shift-s {");
 839        cx.assert_state(
 840            indoc! {"
 841            The ˇ{ quick } brown
 842            fox jumps over
 843            the lazy dog."},
 844            Mode::Normal,
 845        );
 846
 847        // test add surrounds not with around
 848        cx.set_state(
 849            indoc! {"
 850            The quˇick brown
 851            fox jumps over
 852            the lazy dog."},
 853            Mode::Normal,
 854        );
 855        cx.simulate_keystrokes("v i w shift-s }");
 856        cx.assert_state(
 857            indoc! {"
 858            The ˇ{quick} brown
 859            fox jumps over
 860            the lazy dog."},
 861            Mode::Normal,
 862        );
 863
 864        // test add surrounds with motion
 865        cx.set_state(
 866            indoc! {"
 867            The quˇick brown
 868            fox jumps over
 869            the lazy dog."},
 870            Mode::Normal,
 871        );
 872        cx.simulate_keystrokes("v e shift-s }");
 873        cx.assert_state(
 874            indoc! {"
 875            The quˇ{ick} brown
 876            fox jumps over
 877            the lazy dog."},
 878            Mode::Normal,
 879        );
 880
 881        // test add surrounds with multi cursor
 882        cx.set_state(
 883            indoc! {"
 884            The quˇick brown
 885            fox jumps over
 886            the laˇzy dog."},
 887            Mode::Normal,
 888        );
 889        cx.simulate_keystrokes("v i w shift-s '");
 890        cx.assert_state(
 891            indoc! {"
 892            The ˇ'quick' brown
 893            fox jumps over
 894            the ˇ'lazy' dog."},
 895            Mode::Normal,
 896        );
 897
 898        // test add surrounds with visual block
 899        cx.set_state(
 900            indoc! {"
 901            The quˇick brown
 902            fox jumps over
 903            the lazy dog."},
 904            Mode::Normal,
 905        );
 906        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 907        cx.assert_state(
 908            indoc! {"
 909            The ˇ'quick' brown
 910            fox 'jumps' over
 911            the 'lazy 'dog."},
 912            Mode::Normal,
 913        );
 914
 915        // test add surrounds with visual line
 916        cx.set_state(
 917            indoc! {"
 918            The quˇick brown
 919            fox jumps over
 920            the lazy dog."},
 921            Mode::Normal,
 922        );
 923        cx.simulate_keystrokes("j shift-v shift-s '");
 924        cx.assert_state(
 925            indoc! {"
 926            The quick brown
 927            ˇ'
 928            fox jumps over
 929            '
 930            the lazy dog."},
 931            Mode::Normal,
 932        );
 933    }
 934
 935    #[gpui::test]
 936    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 937        let mut cx = VimTestContext::new(cx, true).await;
 938
 939        // test delete surround
 940        cx.set_state(
 941            indoc! {"
 942            The {quˇick} brown
 943            fox jumps over
 944            the lazy dog."},
 945            Mode::Normal,
 946        );
 947        cx.simulate_keystrokes("d s {");
 948        cx.assert_state(
 949            indoc! {"
 950            The ˇquick brown
 951            fox jumps over
 952            the lazy dog."},
 953            Mode::Normal,
 954        );
 955
 956        // test delete not exist surrounds
 957        cx.set_state(
 958            indoc! {"
 959            The {quˇick} brown
 960            fox jumps over
 961            the lazy dog."},
 962            Mode::Normal,
 963        );
 964        cx.simulate_keystrokes("d s [");
 965        cx.assert_state(
 966            indoc! {"
 967            The {quˇick} brown
 968            fox jumps over
 969            the lazy dog."},
 970            Mode::Normal,
 971        );
 972
 973        // test delete surround forward exist, in the surrounds plugin of other editors,
 974        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 975        cx.set_state(
 976            indoc! {"
 977            The {quick} brˇown
 978            fox jumps over
 979            the lazy dog."},
 980            Mode::Normal,
 981        );
 982        cx.simulate_keystrokes("d s {");
 983        cx.assert_state(
 984            indoc! {"
 985            The {quick} brˇown
 986            fox jumps over
 987            the lazy dog."},
 988            Mode::Normal,
 989        );
 990
 991        // test cursor delete inner surrounds
 992        cx.set_state(
 993            indoc! {"
 994            The { quick brown
 995            fox jumˇps over }
 996            the lazy dog."},
 997            Mode::Normal,
 998        );
 999        cx.simulate_keystrokes("d s {");
1000        cx.assert_state(
1001            indoc! {"
1002            The ˇquick brown
1003            fox jumps over
1004            the lazy dog."},
1005            Mode::Normal,
1006        );
1007
1008        // test multi cursor delete surrounds
1009        cx.set_state(
1010            indoc! {"
1011            The [quˇick] brown
1012            fox jumps over
1013            the [laˇzy] dog."},
1014            Mode::Normal,
1015        );
1016        cx.simulate_keystrokes("d s ]");
1017        cx.assert_state(
1018            indoc! {"
1019            The ˇquick brown
1020            fox jumps over
1021            the ˇlazy dog."},
1022            Mode::Normal,
1023        );
1024
1025        // test multi cursor delete surrounds with around
1026        cx.set_state(
1027            indoc! {"
1028            Tˇhe [ quick ] brown
1029            fox jumps over
1030            the [laˇzy] dog."},
1031            Mode::Normal,
1032        );
1033        cx.simulate_keystrokes("d s [");
1034        cx.assert_state(
1035            indoc! {"
1036            The ˇquick brown
1037            fox jumps over
1038            the ˇlazy dog."},
1039            Mode::Normal,
1040        );
1041
1042        cx.set_state(
1043            indoc! {"
1044            Tˇhe [ quick ] brown
1045            fox jumps over
1046            the [laˇzy ] dog."},
1047            Mode::Normal,
1048        );
1049        cx.simulate_keystrokes("d s [");
1050        cx.assert_state(
1051            indoc! {"
1052            The ˇquick brown
1053            fox jumps over
1054            the ˇlazy dog."},
1055            Mode::Normal,
1056        );
1057
1058        // test multi cursor delete different surrounds
1059        // the pair corresponding to the two cursors is the same,
1060        // so they are combined into one cursor
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("d 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 delete surround with multi cursor and nest surrounds
1078        cx.set_state(
1079            indoc! {"
1080            fn test_surround() {
1081                ifˇ 2 > 1 {
1082                    ˇprintln!(\"it is fine\");
1083                };
1084            }"},
1085            Mode::Normal,
1086        );
1087        cx.simulate_keystrokes("d s }");
1088        cx.assert_state(
1089            indoc! {"
1090            fn test_surround() ˇ
1091                if 2 > 1 ˇ
1092                    println!(\"it is fine\");
1093                ;
1094            "},
1095            Mode::Normal,
1096        );
1097    }
1098
1099    #[gpui::test]
1100    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1101        let mut cx = VimTestContext::new(cx, true).await;
1102
1103        cx.set_state(
1104            indoc! {"
1105            The {quˇick} brown
1106            fox jumps over
1107            the lazy dog."},
1108            Mode::Normal,
1109        );
1110        cx.simulate_keystrokes("c s { [");
1111        cx.assert_state(
1112            indoc! {"
1113            The ˇ[ quick ] brown
1114            fox jumps over
1115            the lazy dog."},
1116            Mode::Normal,
1117        );
1118
1119        // test multi cursor change surrounds
1120        cx.set_state(
1121            indoc! {"
1122            The {quˇick} brown
1123            fox jumps over
1124            the {laˇzy} dog."},
1125            Mode::Normal,
1126        );
1127        cx.simulate_keystrokes("c s { [");
1128        cx.assert_state(
1129            indoc! {"
1130            The ˇ[ quick ] brown
1131            fox jumps over
1132            the ˇ[ lazy ] dog."},
1133            Mode::Normal,
1134        );
1135
1136        // test multi cursor delete different surrounds with after cursor
1137        cx.set_state(
1138            indoc! {"
1139            Thˇe {quick} brown
1140            fox jumps over
1141            the {laˇzy} dog."},
1142            Mode::Normal,
1143        );
1144        cx.simulate_keystrokes("c s { [");
1145        cx.assert_state(
1146            indoc! {"
1147            The ˇ[ quick ] brown
1148            fox jumps over
1149            the ˇ[ lazy ] dog."},
1150            Mode::Normal,
1151        );
1152
1153        // test multi cursor change surrount with not around
1154        cx.set_state(
1155            indoc! {"
1156            Thˇe { quick } brown
1157            fox jumps over
1158            the {laˇzy} dog."},
1159            Mode::Normal,
1160        );
1161        cx.simulate_keystrokes("c s { ]");
1162        cx.assert_state(
1163            indoc! {"
1164            The ˇ[quick] brown
1165            fox jumps over
1166            the ˇ[lazy] dog."},
1167            Mode::Normal,
1168        );
1169
1170        // test multi cursor change with not exist surround
1171        cx.set_state(
1172            indoc! {"
1173            The {quˇick} brown
1174            fox jumps over
1175            the [laˇzy] dog."},
1176            Mode::Normal,
1177        );
1178        cx.simulate_keystrokes("c s [ '");
1179        cx.assert_state(
1180            indoc! {"
1181            The {quick} brown
1182            fox jumps over
1183            the ˇ'lazy' dog."},
1184            Mode::Normal,
1185        );
1186
1187        // test change nesting surrounds
1188        cx.set_state(
1189            indoc! {"
1190            fn test_surround() {
1191                ifˇ 2 > 1 {
1192                    ˇprintln!(\"it is fine\");
1193                }
1194            };"},
1195            Mode::Normal,
1196        );
1197        cx.simulate_keystrokes("c s { [");
1198        cx.assert_state(
1199            indoc! {"
1200            fn test_surround() ˇ[
1201                if 2 > 1 ˇ[
1202                    println!(\"it is fine\");
1203                ]
1204            ];"},
1205            Mode::Normal,
1206        );
1207
1208        // test change quotes.
1209        cx.set_state(indoc! {"'  ˇstr  '"}, Mode::Normal);
1210        cx.simulate_keystrokes("c s ' \"");
1211        cx.assert_state(indoc! {"ˇ\"  str  \""}, Mode::Normal);
1212
1213        // test multi cursor change quotes
1214        cx.set_state(
1215            indoc! {"
1216            '  ˇstr  '
1217            some example text here
1218            ˇ'  str  '
1219        "},
1220            Mode::Normal,
1221        );
1222        cx.simulate_keystrokes("c s ' \"");
1223        cx.assert_state(
1224            indoc! {"
1225            ˇ\"  str  \"
1226            some example text here
1227            ˇ\"  str  \"
1228        "},
1229            Mode::Normal,
1230        );
1231
1232        // test quote to bracket spacing.
1233        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1234        cx.simulate_keystrokes("c s ' {");
1235        cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1236
1237        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1238        cx.simulate_keystrokes("c s ' }");
1239        cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1240    }
1241
1242    #[gpui::test]
1243    async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1244        let mut cx = VimTestContext::new(cx, true).await;
1245
1246        // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1247        // action.
1248        cx.update(|_, cx| {
1249            cx.bind_keys([KeyBinding::new(
1250                "b",
1251                AnyBrackets,
1252                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1253            )]);
1254        });
1255
1256        cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1257        cx.simulate_keystrokes("c s b [");
1258        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1259
1260        cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1261        cx.simulate_keystrokes("c s b {");
1262        cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1263
1264        cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1265        cx.simulate_keystrokes("c s b [");
1266        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1267
1268        cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1269        cx.simulate_keystrokes("c s b [");
1270        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1271
1272        cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1273        cx.simulate_keystrokes("c s b {");
1274        cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1275
1276        cx.set_state(
1277            indoc! {"
1278            (< name: ˇ'Zed' >)
1279            (< nˇame: 'DeltaDB' >)
1280        "},
1281            Mode::Normal,
1282        );
1283        cx.simulate_keystrokes("c s b {");
1284        cx.set_state(
1285            indoc! {"
1286            (ˇ{ name: 'Zed' })
1287            (ˇ{ name: 'DeltaDB' })
1288        "},
1289            Mode::Normal,
1290        );
1291    }
1292
1293    #[gpui::test]
1294    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1295        let mut cx = VimTestContext::new(cx, true).await;
1296
1297        cx.set_state(
1298            indoc! {"
1299            The quˇick brown
1300            fox jumps over
1301            the lazy dog."},
1302            Mode::Normal,
1303        );
1304        cx.simulate_keystrokes("y s i w [");
1305        cx.assert_state(
1306            indoc! {"
1307            The ˇ[ quick ] brown
1308            fox jumps over
1309            the lazy dog."},
1310            Mode::Normal,
1311        );
1312
1313        cx.simulate_keystrokes("c s [ }");
1314        cx.assert_state(
1315            indoc! {"
1316            The ˇ{quick} brown
1317            fox jumps over
1318            the lazy dog."},
1319            Mode::Normal,
1320        );
1321
1322        cx.simulate_keystrokes("d s {");
1323        cx.assert_state(
1324            indoc! {"
1325            The ˇquick brown
1326            fox jumps over
1327            the lazy dog."},
1328            Mode::Normal,
1329        );
1330
1331        cx.simulate_keystrokes("u");
1332        cx.assert_state(
1333            indoc! {"
1334            The ˇ{quick} brown
1335            fox jumps over
1336            the lazy dog."},
1337            Mode::Normal,
1338        );
1339    }
1340
1341    #[gpui::test]
1342    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1343        let mut cx = VimTestContext::new(cx, true).await;
1344
1345        // add aliases
1346        cx.set_state(
1347            indoc! {"
1348            The quˇick brown
1349            fox jumps over
1350            the lazy dog."},
1351            Mode::Normal,
1352        );
1353        cx.simulate_keystrokes("y s i w b");
1354        cx.assert_state(
1355            indoc! {"
1356            The ˇ(quick) brown
1357            fox jumps over
1358            the lazy dog."},
1359            Mode::Normal,
1360        );
1361
1362        cx.set_state(
1363            indoc! {"
1364            The quˇick brown
1365            fox jumps over
1366            the lazy dog."},
1367            Mode::Normal,
1368        );
1369        cx.simulate_keystrokes("y s i w B");
1370        cx.assert_state(
1371            indoc! {"
1372            The ˇ{quick} brown
1373            fox jumps over
1374            the lazy dog."},
1375            Mode::Normal,
1376        );
1377
1378        cx.set_state(
1379            indoc! {"
1380            The quˇick brown
1381            fox jumps over
1382            the lazy dog."},
1383            Mode::Normal,
1384        );
1385        cx.simulate_keystrokes("y s i w a");
1386        cx.assert_state(
1387            indoc! {"
1388            The ˇ<quick> brown
1389            fox jumps over
1390            the lazy dog."},
1391            Mode::Normal,
1392        );
1393
1394        cx.set_state(
1395            indoc! {"
1396            The quˇick brown
1397            fox jumps over
1398            the lazy dog."},
1399            Mode::Normal,
1400        );
1401        cx.simulate_keystrokes("y s i w r");
1402        cx.assert_state(
1403            indoc! {"
1404            The ˇ[quick] brown
1405            fox jumps over
1406            the lazy dog."},
1407            Mode::Normal,
1408        );
1409
1410        // change aliases
1411        cx.set_state(
1412            indoc! {"
1413            The {quˇick} brown
1414            fox jumps over
1415            the lazy dog."},
1416            Mode::Normal,
1417        );
1418        cx.simulate_keystrokes("c s { b");
1419        cx.assert_state(
1420            indoc! {"
1421            The ˇ(quick) brown
1422            fox jumps over
1423            the lazy dog."},
1424            Mode::Normal,
1425        );
1426
1427        cx.set_state(
1428            indoc! {"
1429            The (quˇick) brown
1430            fox jumps over
1431            the lazy dog."},
1432            Mode::Normal,
1433        );
1434        cx.simulate_keystrokes("c s ( B");
1435        cx.assert_state(
1436            indoc! {"
1437            The ˇ{quick} brown
1438            fox jumps over
1439            the lazy dog."},
1440            Mode::Normal,
1441        );
1442
1443        cx.set_state(
1444            indoc! {"
1445            The (quˇick) brown
1446            fox jumps over
1447            the lazy dog."},
1448            Mode::Normal,
1449        );
1450        cx.simulate_keystrokes("c s ( a");
1451        cx.assert_state(
1452            indoc! {"
1453            The ˇ<quick> brown
1454            fox jumps over
1455            the lazy dog."},
1456            Mode::Normal,
1457        );
1458
1459        cx.set_state(
1460            indoc! {"
1461            The <quˇick> brown
1462            fox jumps over
1463            the lazy dog."},
1464            Mode::Normal,
1465        );
1466        cx.simulate_keystrokes("c s < b");
1467        cx.assert_state(
1468            indoc! {"
1469            The ˇ(quick) brown
1470            fox jumps over
1471            the lazy dog."},
1472            Mode::Normal,
1473        );
1474
1475        cx.set_state(
1476            indoc! {"
1477            The (quˇick) brown
1478            fox jumps over
1479            the lazy dog."},
1480            Mode::Normal,
1481        );
1482        cx.simulate_keystrokes("c s ( r");
1483        cx.assert_state(
1484            indoc! {"
1485            The ˇ[quick] brown
1486            fox jumps over
1487            the lazy dog."},
1488            Mode::Normal,
1489        );
1490
1491        cx.set_state(
1492            indoc! {"
1493            The [quˇick] brown
1494            fox jumps over
1495            the lazy dog."},
1496            Mode::Normal,
1497        );
1498        cx.simulate_keystrokes("c s [ b");
1499        cx.assert_state(
1500            indoc! {"
1501            The ˇ(quick) brown
1502            fox jumps over
1503            the lazy dog."},
1504            Mode::Normal,
1505        );
1506
1507        // delete alias
1508        cx.set_state(
1509            indoc! {"
1510            The {quˇick} brown
1511            fox jumps over
1512            the lazy dog."},
1513            Mode::Normal,
1514        );
1515        cx.simulate_keystrokes("d s B");
1516        cx.assert_state(
1517            indoc! {"
1518            The ˇquick brown
1519            fox jumps over
1520            the lazy dog."},
1521            Mode::Normal,
1522        );
1523
1524        cx.set_state(
1525            indoc! {"
1526            The (quˇick) brown
1527            fox jumps over
1528            the lazy dog."},
1529            Mode::Normal,
1530        );
1531        cx.simulate_keystrokes("d s b");
1532        cx.assert_state(
1533            indoc! {"
1534            The ˇquick brown
1535            fox jumps over
1536            the lazy dog."},
1537            Mode::Normal,
1538        );
1539
1540        cx.set_state(
1541            indoc! {"
1542            The [quˇick] brown
1543            fox jumps over
1544            the lazy dog."},
1545            Mode::Normal,
1546        );
1547        cx.simulate_keystrokes("d s r");
1548        cx.assert_state(
1549            indoc! {"
1550            The ˇquick brown
1551            fox jumps over
1552            the lazy dog."},
1553            Mode::Normal,
1554        );
1555
1556        cx.set_state(
1557            indoc! {"
1558            The <quˇick> brown
1559            fox jumps over
1560            the lazy dog."},
1561            Mode::Normal,
1562        );
1563        cx.simulate_keystrokes("d s a");
1564        cx.assert_state(
1565            indoc! {"
1566            The ˇquick brown
1567            fox jumps over
1568            the lazy dog."},
1569            Mode::Normal,
1570        );
1571    }
1572}