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