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