object.rs

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