object.rs

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