object.rs

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