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