object.rs

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