object.rs

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