object.rs

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