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