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