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