object.rs

   1use std::ops::Range;
   2
   3use crate::{
   4    motion::{coerce_punctuation, right},
   5    state::Mode,
   6    Vim,
   7};
   8use editor::{
   9    display_map::{DisplaySnapshot, ToDisplayPoint},
  10    movement::{self, FindRange},
  11    Bias, DisplayPoint, Editor,
  12};
  13
  14use itertools::Itertools;
  15
  16use gpui::{actions, impl_actions, ViewContext};
  17use language::{char_kind, BufferSnapshot, CharKind, Point, Selection};
  18use multi_buffer::MultiBufferRow;
  19use serde::Deserialize;
  20
  21#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
  22pub enum Object {
  23    Word { ignore_punctuation: bool },
  24    Sentence,
  25    Paragraph,
  26    Quotes,
  27    BackQuotes,
  28    DoubleQuotes,
  29    VerticalBars,
  30    Parentheses,
  31    SquareBrackets,
  32    CurlyBrackets,
  33    AngleBrackets,
  34    Argument,
  35    Tag,
  36}
  37
  38#[derive(Clone, Deserialize, PartialEq)]
  39#[serde(rename_all = "camelCase")]
  40struct Word {
  41    #[serde(default)]
  42    ignore_punctuation: bool,
  43}
  44
  45impl_actions!(vim, [Word]);
  46
  47actions!(
  48    vim,
  49    [
  50        Sentence,
  51        Paragraph,
  52        Quotes,
  53        BackQuotes,
  54        DoubleQuotes,
  55        VerticalBars,
  56        Parentheses,
  57        SquareBrackets,
  58        CurlyBrackets,
  59        AngleBrackets,
  60        Argument,
  61        Tag
  62    ]
  63);
  64
  65pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
  66    Vim::action(
  67        editor,
  68        cx,
  69        |vim, &Word { ignore_punctuation }: &Word, cx| {
  70            vim.object(Object::Word { ignore_punctuation }, cx)
  71        },
  72    );
  73    Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
  74    Vim::action(editor, cx, |vim, _: &Sentence, cx| {
  75        vim.object(Object::Sentence, cx)
  76    });
  77    Vim::action(editor, cx, |vim, _: &Paragraph, cx| {
  78        vim.object(Object::Paragraph, cx)
  79    });
  80    Vim::action(editor, cx, |vim, _: &Quotes, cx| {
  81        vim.object(Object::Quotes, cx)
  82    });
  83    Vim::action(editor, cx, |vim, _: &BackQuotes, cx| {
  84        vim.object(Object::BackQuotes, cx)
  85    });
  86    Vim::action(editor, cx, |vim, _: &DoubleQuotes, cx| {
  87        vim.object(Object::DoubleQuotes, cx)
  88    });
  89    Vim::action(editor, cx, |vim, _: &Parentheses, cx| {
  90        vim.object(Object::Parentheses, cx)
  91    });
  92    Vim::action(editor, cx, |vim, _: &SquareBrackets, cx| {
  93        vim.object(Object::SquareBrackets, cx)
  94    });
  95    Vim::action(editor, cx, |vim, _: &CurlyBrackets, cx| {
  96        vim.object(Object::CurlyBrackets, cx)
  97    });
  98    Vim::action(editor, cx, |vim, _: &AngleBrackets, cx| {
  99        vim.object(Object::AngleBrackets, cx)
 100    });
 101    Vim::action(editor, cx, |vim, _: &VerticalBars, cx| {
 102        vim.object(Object::VerticalBars, cx)
 103    });
 104    Vim::action(editor, cx, |vim, _: &Argument, cx| {
 105        vim.object(Object::Argument, cx)
 106    });
 107}
 108
 109impl Vim {
 110    fn object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
 111        match self.mode {
 112            Mode::Normal => self.normal_object(object, cx),
 113            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx),
 114            Mode::Insert | Mode::Replace => {
 115                // Shouldn't execute a text object in insert mode. Ignoring
 116            }
 117        }
 118    }
 119}
 120
 121impl Object {
 122    pub fn is_multiline(self) -> bool {
 123        match self {
 124            Object::Word { .. }
 125            | Object::Quotes
 126            | Object::BackQuotes
 127            | Object::VerticalBars
 128            | Object::DoubleQuotes => false,
 129            Object::Sentence
 130            | Object::Paragraph
 131            | Object::Parentheses
 132            | Object::Tag
 133            | Object::AngleBrackets
 134            | Object::CurlyBrackets
 135            | Object::SquareBrackets
 136            | Object::Argument => true,
 137        }
 138    }
 139
 140    pub fn always_expands_both_ways(self) -> bool {
 141        match self {
 142            Object::Word { .. } | Object::Sentence | Object::Paragraph | Object::Argument => false,
 143            Object::Quotes
 144            | Object::BackQuotes
 145            | Object::DoubleQuotes
 146            | Object::VerticalBars
 147            | Object::Parentheses
 148            | Object::SquareBrackets
 149            | Object::Tag
 150            | Object::CurlyBrackets
 151            | Object::AngleBrackets => true,
 152        }
 153    }
 154
 155    pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
 156        match self {
 157            Object::Word { .. }
 158            | Object::Sentence
 159            | Object::Quotes
 160            | Object::BackQuotes
 161            | Object::DoubleQuotes => {
 162                if current_mode == Mode::VisualBlock {
 163                    Mode::VisualBlock
 164                } else {
 165                    Mode::Visual
 166                }
 167            }
 168            Object::Parentheses
 169            | Object::SquareBrackets
 170            | Object::CurlyBrackets
 171            | Object::AngleBrackets
 172            | Object::VerticalBars
 173            | Object::Tag
 174            | Object::Argument => Mode::Visual,
 175            Object::Paragraph => Mode::VisualLine,
 176        }
 177    }
 178
 179    pub fn range(
 180        self,
 181        map: &DisplaySnapshot,
 182        selection: Selection<DisplayPoint>,
 183        around: bool,
 184    ) -> Option<Range<DisplayPoint>> {
 185        let relative_to = selection.head();
 186        match self {
 187            Object::Word { ignore_punctuation } => {
 188                if around {
 189                    around_word(map, relative_to, ignore_punctuation)
 190                } else {
 191                    in_word(map, relative_to, ignore_punctuation)
 192                }
 193            }
 194            Object::Sentence => sentence(map, relative_to, around),
 195            Object::Paragraph => paragraph(map, relative_to, around),
 196            Object::Quotes => {
 197                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 198            }
 199            Object::BackQuotes => {
 200                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 201            }
 202            Object::DoubleQuotes => {
 203                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 204            }
 205            Object::VerticalBars => {
 206                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 207            }
 208            Object::Parentheses => {
 209                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 210            }
 211            Object::Tag => surrounding_html_tag(map, selection, around),
 212            Object::SquareBrackets => {
 213                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 214            }
 215            Object::CurlyBrackets => {
 216                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 217            }
 218            Object::AngleBrackets => {
 219                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 220            }
 221            Object::Argument => argument(map, relative_to, around),
 222        }
 223    }
 224
 225    pub fn expand_selection(
 226        self,
 227        map: &DisplaySnapshot,
 228        selection: &mut Selection<DisplayPoint>,
 229        around: bool,
 230    ) -> bool {
 231        if let Some(range) = self.range(map, selection.clone(), around) {
 232            selection.start = range.start;
 233            selection.end = range.end;
 234            true
 235        } else {
 236            false
 237        }
 238    }
 239}
 240
 241/// Returns a range that surrounds the word `relative_to` is in.
 242///
 243/// If `relative_to` is at the start of a word, return the word.
 244/// If `relative_to` is between words, return the space between.
 245fn in_word(
 246    map: &DisplaySnapshot,
 247    relative_to: DisplayPoint,
 248    ignore_punctuation: bool,
 249) -> Option<Range<DisplayPoint>> {
 250    // Use motion::right so that we consider the character under the cursor when looking for the start
 251    let scope = map
 252        .buffer_snapshot
 253        .language_scope_at(relative_to.to_point(map));
 254    let start = movement::find_preceding_boundary_display_point(
 255        map,
 256        right(map, relative_to, 1),
 257        movement::FindRange::SingleLine,
 258        |left, right| {
 259            coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 260                != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 261        },
 262    );
 263
 264    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 265        coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 266            != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 267    });
 268
 269    Some(start..end)
 270}
 271
 272fn surrounding_html_tag(
 273    map: &DisplaySnapshot,
 274    selection: Selection<DisplayPoint>,
 275    around: bool,
 276) -> Option<Range<DisplayPoint>> {
 277    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 278        chars
 279            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 280            .collect()
 281    }
 282    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 283        if Some('<') != chars.next() {
 284            return None;
 285        }
 286        Some(read_tag(chars))
 287    }
 288    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 289        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 290            return None;
 291        }
 292        Some(read_tag(chars))
 293    }
 294
 295    let snapshot = &map.buffer_snapshot;
 296    let offset = selection.head().to_offset(map, Bias::Left);
 297    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 298    let buffer = excerpt.buffer();
 299    let offset = excerpt.map_offset_to_buffer(offset);
 300
 301    // Find the most closest to current offset
 302    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 303    let mut last_child_node = cursor.node();
 304    while cursor.goto_first_child_for_byte(offset).is_some() {
 305        last_child_node = cursor.node();
 306    }
 307
 308    let mut last_child_node = Some(last_child_node);
 309    while let Some(cur_node) = last_child_node {
 310        if cur_node.child_count() >= 2 {
 311            let first_child = cur_node.child(0);
 312            let last_child = cur_node.child(cur_node.child_count() - 1);
 313            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 314                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 315                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 316                // It needs to be handled differently according to the selection length
 317                let is_valid = if selection.end.to_offset(map, Bias::Left)
 318                    - selection.start.to_offset(map, Bias::Left)
 319                    <= 1
 320                {
 321                    offset <= last_child.end_byte()
 322                } else {
 323                    selection.start.to_offset(map, Bias::Left) >= first_child.start_byte()
 324                        && selection.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
 325                };
 326                if open_tag.is_some() && open_tag == close_tag && is_valid {
 327                    let range = if around {
 328                        first_child.byte_range().start..last_child.byte_range().end
 329                    } else {
 330                        first_child.byte_range().end..last_child.byte_range().start
 331                    };
 332                    if excerpt.contains_buffer_range(range.clone()) {
 333                        let result = excerpt.map_range_from_buffer(range);
 334                        return Some(
 335                            result.start.to_display_point(map)..result.end.to_display_point(map),
 336                        );
 337                    }
 338                }
 339            }
 340        }
 341        last_child_node = cur_node.parent();
 342    }
 343    None
 344}
 345
 346/// Returns a range that surrounds the word and following whitespace
 347/// relative_to is in.
 348///
 349/// If `relative_to` is at the start of a word, return the word and following whitespace.
 350/// If `relative_to` is between words, return the whitespace back and the following word.
 351///
 352/// if in word
 353///   delete that word
 354///   if there is whitespace following the word, delete that as well
 355///   otherwise, delete any preceding whitespace
 356/// otherwise
 357///   delete whitespace around cursor
 358///   delete word following the cursor
 359fn around_word(
 360    map: &DisplaySnapshot,
 361    relative_to: DisplayPoint,
 362    ignore_punctuation: bool,
 363) -> Option<Range<DisplayPoint>> {
 364    let offset = relative_to.to_offset(map, Bias::Left);
 365    let scope = map.buffer_snapshot.language_scope_at(offset);
 366    let in_word = map
 367        .buffer_chars_at(offset)
 368        .next()
 369        .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
 370        .unwrap_or(false);
 371
 372    if in_word {
 373        around_containing_word(map, relative_to, ignore_punctuation)
 374    } else {
 375        around_next_word(map, relative_to, ignore_punctuation)
 376    }
 377}
 378
 379fn around_containing_word(
 380    map: &DisplaySnapshot,
 381    relative_to: DisplayPoint,
 382    ignore_punctuation: bool,
 383) -> Option<Range<DisplayPoint>> {
 384    in_word(map, relative_to, ignore_punctuation)
 385        .map(|range| expand_to_include_whitespace(map, range, true))
 386}
 387
 388fn around_next_word(
 389    map: &DisplaySnapshot,
 390    relative_to: DisplayPoint,
 391    ignore_punctuation: bool,
 392) -> Option<Range<DisplayPoint>> {
 393    let scope = map
 394        .buffer_snapshot
 395        .language_scope_at(relative_to.to_point(map));
 396    // Get the start of the word
 397    let start = movement::find_preceding_boundary_display_point(
 398        map,
 399        right(map, relative_to, 1),
 400        FindRange::SingleLine,
 401        |left, right| {
 402            coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 403                != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 404        },
 405    );
 406
 407    let mut word_found = false;
 408    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 409        let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 410        let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 411
 412        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 413
 414        if right_kind != CharKind::Whitespace {
 415            word_found = true;
 416        }
 417
 418        found
 419    });
 420
 421    Some(start..end)
 422}
 423
 424fn argument(
 425    map: &DisplaySnapshot,
 426    relative_to: DisplayPoint,
 427    around: bool,
 428) -> Option<Range<DisplayPoint>> {
 429    let snapshot = &map.buffer_snapshot;
 430    let offset = relative_to.to_offset(map, Bias::Left);
 431
 432    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 433    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 434    let buffer = excerpt.buffer();
 435
 436    fn comma_delimited_range_at(
 437        buffer: &BufferSnapshot,
 438        mut offset: usize,
 439        include_comma: bool,
 440    ) -> Option<Range<usize>> {
 441        // Seek to the first non-whitespace character
 442        offset += buffer
 443            .chars_at(offset)
 444            .take_while(|c| c.is_whitespace())
 445            .map(char::len_utf8)
 446            .sum::<usize>();
 447
 448        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 449            // Filter out empty ranges
 450            if open.end == close.start {
 451                return false;
 452            }
 453
 454            // If the cursor is outside the brackets, ignore them
 455            if open.start == offset || close.end == offset {
 456                return false;
 457            }
 458
 459            // TODO: Is there any better way to filter out string brackets?
 460            // Used to filter out string brackets
 461            return matches!(
 462                buffer.chars_at(open.start).next(),
 463                Some('(' | '[' | '{' | '<' | '|')
 464            );
 465        };
 466
 467        // Find the brackets containing the cursor
 468        let (open_bracket, close_bracket) =
 469            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 470
 471        let inner_bracket_range = open_bracket.end..close_bracket.start;
 472
 473        let layer = buffer.syntax_layer_at(offset)?;
 474        let node = layer.node();
 475        let mut cursor = node.walk();
 476
 477        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 478        let mut parent_covers_bracket_range = false;
 479        loop {
 480            let node = cursor.node();
 481            let range = node.byte_range();
 482            let covers_bracket_range =
 483                range.start == open_bracket.start && range.end == close_bracket.end;
 484            if parent_covers_bracket_range && !covers_bracket_range {
 485                break;
 486            }
 487            parent_covers_bracket_range = covers_bracket_range;
 488
 489            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 490            if cursor.goto_first_child_for_byte(offset).is_none() {
 491                return None;
 492            }
 493        }
 494
 495        let mut argument_node = cursor.node();
 496
 497        // If the child node is the open bracket, move to the next sibling.
 498        if argument_node.byte_range() == open_bracket {
 499            if !cursor.goto_next_sibling() {
 500                return Some(inner_bracket_range);
 501            }
 502            argument_node = cursor.node();
 503        }
 504        // While the child node is the close bracket or a comma, move to the previous sibling
 505        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 506            if !cursor.goto_previous_sibling() {
 507                return Some(inner_bracket_range);
 508            }
 509            argument_node = cursor.node();
 510            if argument_node.byte_range() == open_bracket {
 511                return Some(inner_bracket_range);
 512            }
 513        }
 514
 515        // The start and end of the argument range, defaulting to the start and end of the argument node
 516        let mut start = argument_node.start_byte();
 517        let mut end = argument_node.end_byte();
 518
 519        let mut needs_surrounding_comma = include_comma;
 520
 521        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 522        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 523        while cursor.goto_previous_sibling() {
 524            let prev = cursor.node();
 525
 526            if prev.start_byte() < open_bracket.end {
 527                start = open_bracket.end;
 528                break;
 529            } else if prev.kind() == "," {
 530                if needs_surrounding_comma {
 531                    start = prev.start_byte();
 532                    needs_surrounding_comma = false;
 533                }
 534                break;
 535            } else if prev.start_byte() < start {
 536                start = prev.start_byte();
 537            }
 538        }
 539
 540        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 541        while cursor.goto_next_sibling() {
 542            let next = cursor.node();
 543
 544            if next.end_byte() > close_bracket.start {
 545                end = close_bracket.start;
 546                break;
 547            } else if next.kind() == "," {
 548                if needs_surrounding_comma {
 549                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 550                    if let Some(next_arg) = next.next_sibling() {
 551                        end = next_arg.start_byte();
 552                    } else {
 553                        end = next.end_byte();
 554                    }
 555                }
 556                break;
 557            } else if next.end_byte() > end {
 558                end = next.end_byte();
 559            }
 560        }
 561
 562        Some(start..end)
 563    }
 564
 565    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 566
 567    if excerpt.contains_buffer_range(result.clone()) {
 568        let result = excerpt.map_range_from_buffer(result);
 569        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 570    } else {
 571        None
 572    }
 573}
 574
 575fn sentence(
 576    map: &DisplaySnapshot,
 577    relative_to: DisplayPoint,
 578    around: bool,
 579) -> Option<Range<DisplayPoint>> {
 580    let mut start = None;
 581    let relative_offset = relative_to.to_offset(map, Bias::Left);
 582    let mut previous_end = relative_offset;
 583
 584    let mut chars = map.buffer_chars_at(previous_end).peekable();
 585
 586    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 587    for (char, offset) in chars
 588        .peek()
 589        .cloned()
 590        .into_iter()
 591        .chain(map.reverse_buffer_chars_at(previous_end))
 592    {
 593        if is_sentence_end(map, offset) {
 594            break;
 595        }
 596
 597        if is_possible_sentence_start(char) {
 598            start = Some(offset);
 599        }
 600
 601        previous_end = offset;
 602    }
 603
 604    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 605    let mut end = relative_offset;
 606    for (char, offset) in chars {
 607        if start.is_none() && is_possible_sentence_start(char) {
 608            if around {
 609                start = Some(offset);
 610                continue;
 611            } else {
 612                end = offset;
 613                break;
 614            }
 615        }
 616
 617        if char != '\n' {
 618            end = offset + char.len_utf8();
 619        }
 620
 621        if is_sentence_end(map, end) {
 622            break;
 623        }
 624    }
 625
 626    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
 627    if around {
 628        range = expand_to_include_whitespace(map, range, false);
 629    }
 630
 631    Some(range)
 632}
 633
 634fn is_possible_sentence_start(character: char) -> bool {
 635    !character.is_whitespace() && character != '.'
 636}
 637
 638const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 639const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 640const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 641fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
 642    let mut next_chars = map.buffer_chars_at(offset).peekable();
 643    if let Some((char, _)) = next_chars.next() {
 644        // We are at a double newline. This position is a sentence end.
 645        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 646            return true;
 647        }
 648
 649        // The next text is not a valid whitespace. This is not a sentence end
 650        if !SENTENCE_END_WHITESPACE.contains(&char) {
 651            return false;
 652        }
 653    }
 654
 655    for (char, _) in map.reverse_buffer_chars_at(offset) {
 656        if SENTENCE_END_PUNCTUATION.contains(&char) {
 657            return true;
 658        }
 659
 660        if !SENTENCE_END_FILLERS.contains(&char) {
 661            return false;
 662        }
 663    }
 664
 665    return false;
 666}
 667
 668/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
 669/// whitespace to the end first and falls back to the start if there was none.
 670fn expand_to_include_whitespace(
 671    map: &DisplaySnapshot,
 672    range: Range<DisplayPoint>,
 673    stop_at_newline: bool,
 674) -> Range<DisplayPoint> {
 675    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
 676    let mut whitespace_included = false;
 677
 678    let mut chars = map.buffer_chars_at(range.end).peekable();
 679    while let Some((char, offset)) = chars.next() {
 680        if char == '\n' && stop_at_newline {
 681            break;
 682        }
 683
 684        if char.is_whitespace() {
 685            if char != '\n' {
 686                range.end = offset + char.len_utf8();
 687                whitespace_included = true;
 688            }
 689        } else {
 690            // Found non whitespace. Quit out.
 691            break;
 692        }
 693    }
 694
 695    if !whitespace_included {
 696        for (char, point) in map.reverse_buffer_chars_at(range.start) {
 697            if char == '\n' && stop_at_newline {
 698                break;
 699            }
 700
 701            if !char.is_whitespace() {
 702                break;
 703            }
 704
 705            range.start = point;
 706        }
 707    }
 708
 709    range.start.to_display_point(map)..range.end.to_display_point(map)
 710}
 711
 712/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
 713/// where `relative_to` is in. If `around`, principally returns the range ending
 714/// at the end of the next paragraph.
 715///
 716/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
 717/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
 718/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
 719/// the trailing newline is not subject to subsequent operations).
 720///
 721/// Edge cases:
 722/// - If `around` and if the current paragraph is the last paragraph of the
 723///   file and is blank, then the selection results in an error.
 724/// - If `around` and if the current paragraph is the last paragraph of the
 725///   file and is not blank, then the returned range starts at the start of the
 726///   previous paragraph, if it exists.
 727fn paragraph(
 728    map: &DisplaySnapshot,
 729    relative_to: DisplayPoint,
 730    around: bool,
 731) -> Option<Range<DisplayPoint>> {
 732    let mut paragraph_start = start_of_paragraph(map, relative_to);
 733    let mut paragraph_end = end_of_paragraph(map, relative_to);
 734
 735    let paragraph_end_row = paragraph_end.row();
 736    let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
 737    let point = relative_to.to_point(map);
 738    let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 739
 740    if around {
 741        if paragraph_ends_with_eof {
 742            if current_line_is_empty {
 743                return None;
 744            }
 745
 746            let paragraph_start_row = paragraph_start.row();
 747            if paragraph_start_row.0 != 0 {
 748                let previous_paragraph_last_line_start =
 749                    Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map);
 750                paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
 751            }
 752        } else {
 753            let next_paragraph_start = Point::new(paragraph_end_row.0 + 1, 0).to_display_point(map);
 754            paragraph_end = end_of_paragraph(map, next_paragraph_start);
 755        }
 756    }
 757
 758    let range = paragraph_start..paragraph_end;
 759    Some(range)
 760}
 761
 762/// Returns a position of the start of the current paragraph, where a paragraph
 763/// is defined as a run of non-blank lines or a run of blank lines.
 764pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 765    let point = display_point.to_point(map);
 766    if point.row == 0 {
 767        return DisplayPoint::zero();
 768    }
 769
 770    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 771
 772    for row in (0..point.row).rev() {
 773        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 774        if blank != is_current_line_blank {
 775            return Point::new(row + 1, 0).to_display_point(map);
 776        }
 777    }
 778
 779    DisplayPoint::zero()
 780}
 781
 782/// Returns a position of the end of the current paragraph, where a paragraph
 783/// is defined as a run of non-blank lines or a run of blank lines.
 784/// The trailing newline is excluded from the paragraph.
 785pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 786    let point = display_point.to_point(map);
 787    if point.row == map.max_buffer_row().0 {
 788        return map.max_point();
 789    }
 790
 791    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 792
 793    for row in point.row + 1..map.max_buffer_row().0 + 1 {
 794        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 795        if blank != is_current_line_blank {
 796            let previous_row = row - 1;
 797            return Point::new(
 798                previous_row,
 799                map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
 800            )
 801            .to_display_point(map);
 802        }
 803    }
 804
 805    map.max_point()
 806}
 807
 808fn surrounding_markers(
 809    map: &DisplaySnapshot,
 810    relative_to: DisplayPoint,
 811    around: bool,
 812    search_across_lines: bool,
 813    open_marker: char,
 814    close_marker: char,
 815) -> Option<Range<DisplayPoint>> {
 816    let point = relative_to.to_offset(map, Bias::Left);
 817
 818    let mut matched_closes = 0;
 819    let mut opening = None;
 820
 821    let mut before_ch = match movement::chars_before(map, point).next() {
 822        Some((ch, _)) => ch,
 823        _ => '\0',
 824    };
 825    if let Some((ch, range)) = movement::chars_after(map, point).next() {
 826        if ch == open_marker && before_ch != '\\' {
 827            if open_marker == close_marker {
 828                let mut total = 0;
 829                for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
 830                {
 831                    if ch == '\n' {
 832                        break;
 833                    }
 834                    if ch == open_marker && before_ch != '\\' {
 835                        total += 1;
 836                    }
 837                }
 838                if total % 2 == 0 {
 839                    opening = Some(range)
 840                }
 841            } else {
 842                opening = Some(range)
 843            }
 844        }
 845    }
 846
 847    if opening.is_none() {
 848        let mut chars_before = movement::chars_before(map, point).peekable();
 849        while let Some((ch, range)) = chars_before.next() {
 850            if ch == '\n' && !search_across_lines {
 851                break;
 852            }
 853
 854            if let Some((before_ch, _)) = chars_before.peek() {
 855                if *before_ch == '\\' {
 856                    continue;
 857                }
 858            }
 859
 860            if ch == open_marker {
 861                if matched_closes == 0 {
 862                    opening = Some(range);
 863                    break;
 864                }
 865                matched_closes -= 1;
 866            } else if ch == close_marker {
 867                matched_closes += 1
 868            }
 869        }
 870    }
 871    if opening.is_none() {
 872        for (ch, range) in movement::chars_after(map, point) {
 873            if before_ch != '\\' {
 874                if ch == open_marker {
 875                    opening = Some(range);
 876                    break;
 877                } else if ch == close_marker {
 878                    break;
 879                }
 880            }
 881
 882            before_ch = ch;
 883        }
 884    }
 885
 886    let Some(mut opening) = opening else {
 887        return None;
 888    };
 889
 890    let mut matched_opens = 0;
 891    let mut closing = None;
 892    before_ch = match movement::chars_before(map, opening.end).next() {
 893        Some((ch, _)) => ch,
 894        _ => '\0',
 895    };
 896    for (ch, range) in movement::chars_after(map, opening.end) {
 897        if ch == '\n' && !search_across_lines {
 898            break;
 899        }
 900
 901        if before_ch != '\\' {
 902            if ch == close_marker {
 903                if matched_opens == 0 {
 904                    closing = Some(range);
 905                    break;
 906                }
 907                matched_opens -= 1;
 908            } else if ch == open_marker {
 909                matched_opens += 1;
 910            }
 911        }
 912
 913        before_ch = ch;
 914    }
 915
 916    let Some(mut closing) = closing else {
 917        return None;
 918    };
 919
 920    if around && !search_across_lines {
 921        let mut found = false;
 922
 923        for (ch, range) in movement::chars_after(map, closing.end) {
 924            if ch.is_whitespace() && ch != '\n' {
 925                found = true;
 926                closing.end = range.end;
 927            } else {
 928                break;
 929            }
 930        }
 931
 932        if !found {
 933            for (ch, range) in movement::chars_before(map, opening.start) {
 934                if ch.is_whitespace() && ch != '\n' {
 935                    opening.start = range.start
 936                } else {
 937                    break;
 938                }
 939            }
 940        }
 941    }
 942
 943    if !around && search_across_lines {
 944        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
 945            if ch == '\n' {
 946                opening.end = range.end
 947            }
 948        }
 949
 950        for (ch, range) in movement::chars_before(map, closing.start) {
 951            if !ch.is_whitespace() {
 952                break;
 953            }
 954            if ch != '\n' {
 955                closing.start = range.start
 956            }
 957        }
 958    }
 959
 960    let result = if around {
 961        opening.start..closing.end
 962    } else {
 963        opening.end..closing.start
 964    };
 965
 966    Some(
 967        map.clip_point(result.start.to_display_point(map), Bias::Left)
 968            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
 969    )
 970}
 971
 972#[cfg(test)]
 973mod test {
 974    use indoc::indoc;
 975
 976    use crate::{
 977        state::Mode,
 978        test::{NeovimBackedTestContext, VimTestContext},
 979    };
 980
 981    const WORD_LOCATIONS: &str = indoc! {"
 982        The quick ˇbrowˇnˇ•••
 983        fox ˇjuˇmpsˇ over
 984        the lazy dogˇ••
 985        ˇ
 986        ˇ
 987        ˇ
 988        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
 989        ˇ••
 990        ˇ••
 991        ˇ  fox-jumpˇs over
 992        the lazy dogˇ•
 993        ˇ
 994        "
 995    };
 996
 997    #[gpui::test]
 998    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
 999        let mut cx = NeovimBackedTestContext::new(cx).await;
1000
1001        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1002            .await
1003            .assert_matches();
1004        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1005            .await
1006            .assert_matches();
1007        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1008            .await
1009            .assert_matches();
1010        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1011            .await
1012            .assert_matches();
1013    }
1014
1015    #[gpui::test]
1016    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1017        let mut cx = NeovimBackedTestContext::new(cx).await;
1018
1019        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1020            .await
1021            .assert_matches();
1022        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1023            .await
1024            .assert_matches();
1025        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1026            .await
1027            .assert_matches();
1028        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1029            .await
1030            .assert_matches();
1031    }
1032
1033    #[gpui::test]
1034    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1035        let mut cx = NeovimBackedTestContext::new(cx).await;
1036
1037        /*
1038                cx.set_shared_state("The quick ˇbrown\nfox").await;
1039                cx.simulate_shared_keystrokes(["v"]).await;
1040                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1041                cx.simulate_shared_keystrokes(["i", "w"]).await;
1042                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1043        */
1044        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1045        cx.simulate_shared_keystrokes("v").await;
1046        cx.shared_state()
1047            .await
1048            .assert_eq("The quick brown\n«\nˇ»fox");
1049        cx.simulate_shared_keystrokes("i w").await;
1050        cx.shared_state()
1051            .await
1052            .assert_eq("The quick brown\n«\nˇ»fox");
1053
1054        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1055            .await
1056            .assert_matches();
1057        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1058            .await
1059            .assert_matches();
1060    }
1061
1062    const PARAGRAPH_EXAMPLES: &[&'static str] = &[
1063        // Single line
1064        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1065        // Multiple lines without empty lines
1066        indoc! {"
1067            ˇThe quick brownˇ
1068            ˇfox jumps overˇ
1069            the lazy dog.ˇ
1070        "},
1071        // Heading blank paragraph and trailing normal paragraph
1072        indoc! {"
1073            ˇ
1074            ˇ
1075            ˇThe quick brown fox jumps
1076            ˇover the lazy dog.
1077            ˇ
1078            ˇ
1079            ˇThe quick brown fox jumpsˇ
1080            ˇover the lazy dog.ˇ
1081        "},
1082        // Inserted blank paragraph and trailing blank paragraph
1083        indoc! {"
1084            ˇThe quick brown fox jumps
1085            ˇover the lazy dog.
1086            ˇ
1087            ˇ
1088            ˇ
1089            ˇThe quick brown fox jumpsˇ
1090            ˇover the lazy dog.ˇ
1091            ˇ
1092            ˇ
1093            ˇ
1094        "},
1095        // "Blank" paragraph with whitespace characters
1096        indoc! {"
1097            ˇThe quick brown fox jumps
1098            over the lazy dog.
1099
1100            ˇ \t
1101
1102            ˇThe quick brown fox jumps
1103            over the lazy dog.ˇ
1104            ˇ
1105            ˇ \t
1106            \t \t
1107        "},
1108        // Single line "paragraphs", where selection size might be zero.
1109        indoc! {"
1110            ˇThe quick brown fox jumps over the lazy dog.
1111            ˇ
1112            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1113            ˇ
1114        "},
1115    ];
1116
1117    #[gpui::test]
1118    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1119        let mut cx = NeovimBackedTestContext::new(cx).await;
1120
1121        for paragraph_example in PARAGRAPH_EXAMPLES {
1122            cx.simulate_at_each_offset("c i p", paragraph_example)
1123                .await
1124                .assert_matches();
1125            cx.simulate_at_each_offset("c a p", paragraph_example)
1126                .await
1127                .assert_matches();
1128        }
1129    }
1130
1131    #[gpui::test]
1132    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1133        let mut cx = NeovimBackedTestContext::new(cx).await;
1134
1135        for paragraph_example in PARAGRAPH_EXAMPLES {
1136            cx.simulate_at_each_offset("d i p", paragraph_example)
1137                .await
1138                .assert_matches();
1139            cx.simulate_at_each_offset("d a p", paragraph_example)
1140                .await
1141                .assert_matches();
1142        }
1143    }
1144
1145    #[gpui::test]
1146    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1147        let mut cx = NeovimBackedTestContext::new(cx).await;
1148
1149        const EXAMPLES: &[&'static str] = &[
1150            indoc! {"
1151                ˇThe quick brown
1152                fox jumps over
1153                the lazy dog.
1154            "},
1155            indoc! {"
1156                ˇ
1157
1158                ˇThe quick brown fox jumps
1159                over the lazy dog.
1160                ˇ
1161
1162                ˇThe quick brown fox jumps
1163                over the lazy dog.
1164            "},
1165            indoc! {"
1166                ˇThe quick brown fox jumps over the lazy dog.
1167                ˇ
1168                ˇThe quick brown fox jumps over the lazy dog.
1169
1170            "},
1171        ];
1172
1173        for paragraph_example in EXAMPLES {
1174            cx.simulate_at_each_offset("v i p", paragraph_example)
1175                .await
1176                .assert_matches();
1177            cx.simulate_at_each_offset("v a p", paragraph_example)
1178                .await
1179                .assert_matches();
1180        }
1181    }
1182
1183    // Test string with "`" for opening surrounders and "'" for closing surrounders
1184    const SURROUNDING_MARKER_STRING: &str = indoc! {"
1185        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1186        'ˇfox juˇmps ov`ˇer
1187        the ˇlazy d'o`ˇg"};
1188
1189    const SURROUNDING_OBJECTS: &[(char, char)] = &[
1190        ('"', '"'), // Double Quote
1191        ('(', ')'), // Parentheses
1192    ];
1193
1194    #[gpui::test]
1195    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1196        let mut cx = NeovimBackedTestContext::new(cx).await;
1197
1198        for (start, end) in SURROUNDING_OBJECTS {
1199            let marked_string = SURROUNDING_MARKER_STRING
1200                .replace('`', &start.to_string())
1201                .replace('\'', &end.to_string());
1202
1203            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1204                .await
1205                .assert_matches();
1206            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1207                .await
1208                .assert_matches();
1209            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1210                .await
1211                .assert_matches();
1212            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1213                .await
1214                .assert_matches();
1215        }
1216    }
1217    #[gpui::test]
1218    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1219        let mut cx = NeovimBackedTestContext::new(cx).await;
1220        cx.set_shared_wrap(12).await;
1221
1222        cx.set_shared_state(indoc! {
1223            "\"ˇhello world\"!"
1224        })
1225        .await;
1226        cx.simulate_shared_keystrokes("v i \"").await;
1227        cx.shared_state().await.assert_eq(indoc! {
1228            "\"«hello worldˇ»\"!"
1229        });
1230
1231        cx.set_shared_state(indoc! {
1232            "\"hˇello world\"!"
1233        })
1234        .await;
1235        cx.simulate_shared_keystrokes("v i \"").await;
1236        cx.shared_state().await.assert_eq(indoc! {
1237            "\"«hello worldˇ»\"!"
1238        });
1239
1240        cx.set_shared_state(indoc! {
1241            "helˇlo \"world\"!"
1242        })
1243        .await;
1244        cx.simulate_shared_keystrokes("v i \"").await;
1245        cx.shared_state().await.assert_eq(indoc! {
1246            "hello \"«worldˇ»\"!"
1247        });
1248
1249        cx.set_shared_state(indoc! {
1250            "hello \"wˇorld\"!"
1251        })
1252        .await;
1253        cx.simulate_shared_keystrokes("v i \"").await;
1254        cx.shared_state().await.assert_eq(indoc! {
1255            "hello \"«worldˇ»\"!"
1256        });
1257
1258        cx.set_shared_state(indoc! {
1259            "hello \"wˇorld\"!"
1260        })
1261        .await;
1262        cx.simulate_shared_keystrokes("v a \"").await;
1263        cx.shared_state().await.assert_eq(indoc! {
1264            "hello« \"world\"ˇ»!"
1265        });
1266
1267        cx.set_shared_state(indoc! {
1268            "hello \"wˇorld\" !"
1269        })
1270        .await;
1271        cx.simulate_shared_keystrokes("v a \"").await;
1272        cx.shared_state().await.assert_eq(indoc! {
1273            "hello «\"world\" ˇ»!"
1274        });
1275
1276        cx.set_shared_state(indoc! {
1277            "hello \"wˇorld\"1278            goodbye"
1279        })
1280        .await;
1281        cx.simulate_shared_keystrokes("v a \"").await;
1282        cx.shared_state().await.assert_eq(indoc! {
1283            "hello «\"world\" ˇ»
1284            goodbye"
1285        });
1286    }
1287
1288    #[gpui::test]
1289    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1290        let mut cx = NeovimBackedTestContext::new(cx).await;
1291
1292        cx.set_shared_state(indoc! {
1293            "func empty(a string) bool {
1294               if a == \"\" {
1295                  return true
1296               }
1297               ˇreturn false
1298            }"
1299        })
1300        .await;
1301        cx.simulate_shared_keystrokes("v i {").await;
1302        cx.shared_state().await.assert_eq(indoc! {"
1303            func empty(a string) bool {
1304            «   if a == \"\" {
1305                  return true
1306               }
1307               return false
1308            ˇ»}"});
1309        cx.set_shared_state(indoc! {
1310            "func empty(a string) bool {
1311                 if a == \"\" {
1312                     ˇreturn true
1313                 }
1314                 return false
1315            }"
1316        })
1317        .await;
1318        cx.simulate_shared_keystrokes("v i {").await;
1319        cx.shared_state().await.assert_eq(indoc! {"
1320            func empty(a string) bool {
1321                 if a == \"\" {
1322            «         return true
1323            ˇ»     }
1324                 return false
1325            }"});
1326
1327        cx.set_shared_state(indoc! {
1328            "func empty(a string) bool {
1329                 if a == \"\" ˇ{
1330                     return true
1331                 }
1332                 return false
1333            }"
1334        })
1335        .await;
1336        cx.simulate_shared_keystrokes("v i {").await;
1337        cx.shared_state().await.assert_eq(indoc! {"
1338            func empty(a string) bool {
1339                 if a == \"\" {
1340            «         return true
1341            ˇ»     }
1342                 return false
1343            }"});
1344    }
1345
1346    #[gpui::test]
1347    async fn test_singleline_surrounding_character_objects_with_escape(
1348        cx: &mut gpui::TestAppContext,
1349    ) {
1350        let mut cx = NeovimBackedTestContext::new(cx).await;
1351        cx.set_shared_state(indoc! {
1352            "h\"e\\\"lˇlo \\\"world\"!"
1353        })
1354        .await;
1355        cx.simulate_shared_keystrokes("v i \"").await;
1356        cx.shared_state().await.assert_eq(indoc! {
1357            "h\"«e\\\"llo \\\"worldˇ»\"!"
1358        });
1359
1360        cx.set_shared_state(indoc! {
1361            "hello \"teˇst \\\"inside\\\" world\""
1362        })
1363        .await;
1364        cx.simulate_shared_keystrokes("v i \"").await;
1365        cx.shared_state().await.assert_eq(indoc! {
1366            "hello \"«test \\\"inside\\\" worldˇ»\""
1367        });
1368    }
1369
1370    #[gpui::test]
1371    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1372        let mut cx = VimTestContext::new(cx, true).await;
1373        cx.set_state(
1374            indoc! {"
1375            fn boop() {
1376                baz(ˇ|a, b| { bar(|j, k| { })})
1377            }"
1378            },
1379            Mode::Normal,
1380        );
1381        cx.simulate_keystrokes("c i |");
1382        cx.assert_state(
1383            indoc! {"
1384            fn boop() {
1385                baz(|ˇ| { bar(|j, k| { })})
1386            }"
1387            },
1388            Mode::Insert,
1389        );
1390        cx.simulate_keystrokes("escape 1 8 |");
1391        cx.assert_state(
1392            indoc! {"
1393            fn boop() {
1394                baz(|| { bar(ˇ|j, k| { })})
1395            }"
1396            },
1397            Mode::Normal,
1398        );
1399
1400        cx.simulate_keystrokes("v a |");
1401        cx.assert_state(
1402            indoc! {"
1403            fn boop() {
1404                baz(|| { bar(«|j, k| ˇ»{ })})
1405            }"
1406            },
1407            Mode::Visual,
1408        );
1409    }
1410
1411    #[gpui::test]
1412    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1413        let mut cx = VimTestContext::new(cx, true).await;
1414
1415        // Generic arguments
1416        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1417        cx.simulate_keystrokes("v i a");
1418        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1419
1420        // Function arguments
1421        cx.set_state(
1422            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1423            Mode::Normal,
1424        );
1425        cx.simulate_keystrokes("d a a");
1426        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1427
1428        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1429        cx.simulate_keystrokes("v a a");
1430        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1431
1432        // Tuple, vec, and array arguments
1433        cx.set_state(
1434            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1435            Mode::Normal,
1436        );
1437        cx.simulate_keystrokes("c i a");
1438        cx.assert_state(
1439            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1440            Mode::Insert,
1441        );
1442
1443        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1444        cx.simulate_keystrokes("c a a");
1445        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1446
1447        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1448        cx.simulate_keystrokes("c i a");
1449        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1450
1451        cx.set_state(
1452            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1453            Mode::Normal,
1454        );
1455        cx.simulate_keystrokes("c a a");
1456        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1457
1458        // Cursor immediately before / after brackets
1459        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1460        cx.simulate_keystrokes("v i a");
1461        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1462
1463        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1464        cx.simulate_keystrokes("v i a");
1465        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1466    }
1467
1468    #[gpui::test]
1469    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1470        let mut cx = NeovimBackedTestContext::new(cx).await;
1471
1472        for (start, end) in SURROUNDING_OBJECTS {
1473            let marked_string = SURROUNDING_MARKER_STRING
1474                .replace('`', &start.to_string())
1475                .replace('\'', &end.to_string());
1476
1477            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1478                .await
1479                .assert_matches();
1480            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1481                .await
1482                .assert_matches();
1483            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1484                .await
1485                .assert_matches();
1486            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1487                .await
1488                .assert_matches();
1489        }
1490    }
1491
1492    #[gpui::test]
1493    async fn test_tags(cx: &mut gpui::TestAppContext) {
1494        let mut cx = VimTestContext::new_html(cx).await;
1495
1496        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1497        cx.simulate_keystrokes("v i t");
1498        cx.assert_state(
1499            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1500            Mode::Visual,
1501        );
1502        cx.simulate_keystrokes("a t");
1503        cx.assert_state(
1504            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1505            Mode::Visual,
1506        );
1507        cx.simulate_keystrokes("a t");
1508        cx.assert_state(
1509            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1510            Mode::Visual,
1511        );
1512
1513        // The cursor is before the tag
1514        cx.set_state(
1515            "<html><head></head><body> ˇ  <b>hi!</b></body>",
1516            Mode::Normal,
1517        );
1518        cx.simulate_keystrokes("v i t");
1519        cx.assert_state(
1520            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
1521            Mode::Visual,
1522        );
1523        cx.simulate_keystrokes("a t");
1524        cx.assert_state(
1525            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
1526            Mode::Visual,
1527        );
1528
1529        // The cursor is in the open tag
1530        cx.set_state(
1531            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1532            Mode::Normal,
1533        );
1534        cx.simulate_keystrokes("v a t");
1535        cx.assert_state(
1536            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1537            Mode::Visual,
1538        );
1539        cx.simulate_keystrokes("i t");
1540        cx.assert_state(
1541            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1542            Mode::Visual,
1543        );
1544
1545        // current selection length greater than 1
1546        cx.set_state(
1547            "<html><head></head><body><«b>hi!ˇ»</b></body>",
1548            Mode::Visual,
1549        );
1550        cx.simulate_keystrokes("i t");
1551        cx.assert_state(
1552            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1553            Mode::Visual,
1554        );
1555        cx.simulate_keystrokes("a t");
1556        cx.assert_state(
1557            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1558            Mode::Visual,
1559        );
1560
1561        cx.set_state(
1562            "<html><head></head><body><«b>hi!</ˇ»b></body>",
1563            Mode::Visual,
1564        );
1565        cx.simulate_keystrokes("a t");
1566        cx.assert_state(
1567            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1568            Mode::Visual,
1569        );
1570    }
1571}