object.rs

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