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