object.rs

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