object.rs

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