object.rs

   1use std::ops::Range;
   2
   3use crate::{
   4    Vim,
   5    motion::right,
   6    state::{Mode, Operator},
   7};
   8use editor::{
   9    Bias, DisplayPoint, Editor, ToOffset,
  10    display_map::{DisplaySnapshot, ToDisplayPoint},
  11    movement::{self, FindRange},
  12};
  13use gpui::{Action, Window, actions};
  14use itertools::Itertools;
  15use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
  16use multi_buffer::MultiBufferRow;
  17use schemars::JsonSchema;
  18use serde::Deserialize;
  19use ui::Context;
  20
  21#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
  22#[serde(rename_all = "snake_case")]
  23pub enum Object {
  24    Word { ignore_punctuation: bool },
  25    Subword { ignore_punctuation: bool },
  26    Sentence,
  27    Paragraph,
  28    Quotes,
  29    BackQuotes,
  30    AnyQuotes,
  31    MiniQuotes,
  32    DoubleQuotes,
  33    VerticalBars,
  34    AnyBrackets,
  35    MiniBrackets,
  36    Parentheses,
  37    SquareBrackets,
  38    CurlyBrackets,
  39    AngleBrackets,
  40    Argument,
  41    IndentObj { include_below: bool },
  42    Tag,
  43    Method,
  44    Class,
  45    Comment,
  46    EntireFile,
  47}
  48
  49/// Selects a word text object.
  50#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  51#[action(namespace = vim)]
  52#[serde(deny_unknown_fields)]
  53struct Word {
  54    #[serde(default)]
  55    ignore_punctuation: bool,
  56}
  57
  58/// Selects a subword text object.
  59#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  60#[action(namespace = vim)]
  61#[serde(deny_unknown_fields)]
  62struct Subword {
  63    #[serde(default)]
  64    ignore_punctuation: bool,
  65}
  66/// Selects text at the same indentation level.
  67#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  68#[action(namespace = vim)]
  69#[serde(deny_unknown_fields)]
  70struct IndentObj {
  71    #[serde(default)]
  72    include_below: bool,
  73}
  74
  75#[derive(Debug, Clone)]
  76pub struct CandidateRange {
  77    pub start: DisplayPoint,
  78    pub end: DisplayPoint,
  79}
  80
  81#[derive(Debug, Clone)]
  82pub struct CandidateWithRanges {
  83    candidate: CandidateRange,
  84    open_range: Range<usize>,
  85    close_range: Range<usize>,
  86}
  87
  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.is_opening, window, cx)
 382    });
 383    Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
 384        vim.object_impl(Object::SquareBrackets, action.is_opening, window, cx)
 385    });
 386    Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
 387        vim.object_impl(Object::CurlyBrackets, action.is_opening, window, cx)
 388    });
 389    Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
 390        vim.object_impl(Object::AngleBrackets, action.is_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, window, false, cx);
 425    }
 426
 427    fn object_impl(
 428        &mut self,
 429        object: Object,
 430        window: &mut Window,
 431        is_opening: bool,
 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, is_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(|&quote| {
 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.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1477
1478        if around {
1479            if paragraph_ends_with_eof {
1480                if current_line_is_empty {
1481                    return None;
1482                }
1483
1484                let paragraph_start_buffer_point = paragraph_start.to_point(map);
1485                if paragraph_start_buffer_point.row != 0 {
1486                    let previous_paragraph_last_line_start =
1487                        Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
1488                    paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1489                }
1490            } else {
1491                let paragraph_end_buffer_point = paragraph_end.to_point(map);
1492                let mut start_row = paragraph_end_buffer_point.row + 1;
1493                if i > 0 {
1494                    start_row += 1;
1495                }
1496                let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1497                paragraph_end = end_of_paragraph(map, next_paragraph_start);
1498            }
1499        }
1500    }
1501
1502    let range = paragraph_start..paragraph_end;
1503    Some(range)
1504}
1505
1506/// Returns a position of the start of the current paragraph, where a paragraph
1507/// is defined as a run of non-blank lines or a run of blank lines.
1508pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1509    let point = display_point.to_point(map);
1510    if point.row == 0 {
1511        return DisplayPoint::zero();
1512    }
1513
1514    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1515
1516    for row in (0..point.row).rev() {
1517        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1518        if blank != is_current_line_blank {
1519            return Point::new(row + 1, 0).to_display_point(map);
1520        }
1521    }
1522
1523    DisplayPoint::zero()
1524}
1525
1526/// Returns a position of the end of the current paragraph, where a paragraph
1527/// is defined as a run of non-blank lines or a run of blank lines.
1528/// The trailing newline is excluded from the paragraph.
1529pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1530    let point = display_point.to_point(map);
1531    if point.row == map.buffer_snapshot.max_row().0 {
1532        return map.max_point();
1533    }
1534
1535    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1536
1537    for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
1538        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1539        if blank != is_current_line_blank {
1540            let previous_row = row - 1;
1541            return Point::new(
1542                previous_row,
1543                map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
1544            )
1545            .to_display_point(map);
1546        }
1547    }
1548
1549    map.max_point()
1550}
1551
1552pub fn surrounding_markers(
1553    map: &DisplaySnapshot,
1554    relative_to: DisplayPoint,
1555    around: bool,
1556    search_across_lines: bool,
1557    open_marker: char,
1558    close_marker: char,
1559) -> Option<Range<DisplayPoint>> {
1560    let point = relative_to.to_offset(map, Bias::Left);
1561
1562    let mut matched_closes = 0;
1563    let mut opening = None;
1564
1565    let mut before_ch = match movement::chars_before(map, point).next() {
1566        Some((ch, _)) => ch,
1567        _ => '\0',
1568    };
1569    if let Some((ch, range)) = movement::chars_after(map, point).next()
1570        && ch == open_marker
1571        && before_ch != '\\'
1572    {
1573        if open_marker == close_marker {
1574            let mut total = 0;
1575            for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
1576                if ch == '\n' {
1577                    break;
1578                }
1579                if ch == open_marker && before_ch != '\\' {
1580                    total += 1;
1581                }
1582            }
1583            if total % 2 == 0 {
1584                opening = Some(range)
1585            }
1586        } else {
1587            opening = Some(range)
1588        }
1589    }
1590
1591    if opening.is_none() {
1592        let mut chars_before = movement::chars_before(map, point).peekable();
1593        while let Some((ch, range)) = chars_before.next() {
1594            if ch == '\n' && !search_across_lines {
1595                break;
1596            }
1597
1598            if let Some((before_ch, _)) = chars_before.peek()
1599                && *before_ch == '\\'
1600            {
1601                continue;
1602            }
1603
1604            if ch == open_marker {
1605                if matched_closes == 0 {
1606                    opening = Some(range);
1607                    break;
1608                }
1609                matched_closes -= 1;
1610            } else if ch == close_marker {
1611                matched_closes += 1
1612            }
1613        }
1614    }
1615    if opening.is_none() {
1616        for (ch, range) in movement::chars_after(map, point) {
1617            if before_ch != '\\' {
1618                if ch == open_marker {
1619                    opening = Some(range);
1620                    break;
1621                } else if ch == close_marker {
1622                    break;
1623                }
1624            }
1625
1626            before_ch = ch;
1627        }
1628    }
1629
1630    let mut opening = opening?;
1631
1632    let mut matched_opens = 0;
1633    let mut closing = None;
1634    before_ch = match movement::chars_before(map, opening.end).next() {
1635        Some((ch, _)) => ch,
1636        _ => '\0',
1637    };
1638    for (ch, range) in movement::chars_after(map, opening.end) {
1639        if ch == '\n' && !search_across_lines {
1640            break;
1641        }
1642
1643        if before_ch != '\\' {
1644            if ch == close_marker {
1645                if matched_opens == 0 {
1646                    closing = Some(range);
1647                    break;
1648                }
1649                matched_opens -= 1;
1650            } else if ch == open_marker {
1651                matched_opens += 1;
1652            }
1653        }
1654
1655        before_ch = ch;
1656    }
1657
1658    let mut closing = closing?;
1659
1660    if around && !search_across_lines {
1661        let mut found = false;
1662
1663        for (ch, range) in movement::chars_after(map, closing.end) {
1664            if ch.is_whitespace() && ch != '\n' {
1665                found = true;
1666                closing.end = range.end;
1667            } else {
1668                break;
1669            }
1670        }
1671
1672        if !found {
1673            for (ch, range) in movement::chars_before(map, opening.start) {
1674                if ch.is_whitespace() && ch != '\n' {
1675                    opening.start = range.start
1676                } else {
1677                    break;
1678                }
1679            }
1680        }
1681    }
1682
1683    // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1684    if !around && open_marker != close_marker {
1685        let start_point = opening.end.to_display_point(map);
1686        let end_point = closing.start.to_display_point(map);
1687        let start_offset = start_point.to_offset(map, Bias::Left);
1688        let end_offset = end_point.to_offset(map, Bias::Left);
1689
1690        if start_point.row() != end_point.row()
1691            && map
1692                .buffer_chars_at(start_offset)
1693                .take_while(|(_, offset)| offset < &end_offset)
1694                .any(|(ch, _)| !ch.is_whitespace())
1695        {
1696            let mut first_non_ws = None;
1697            let mut last_non_ws = None;
1698            for (ch, offset) in map.buffer_chars_at(start_offset) {
1699                if !ch.is_whitespace() {
1700                    first_non_ws = Some(offset);
1701                    break;
1702                }
1703            }
1704            for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1705                if !ch.is_whitespace() {
1706                    last_non_ws = Some(offset + ch.len_utf8());
1707                    break;
1708                }
1709            }
1710            if let Some(start) = first_non_ws {
1711                opening.end = start;
1712            }
1713            if let Some(end) = last_non_ws {
1714                closing.start = end;
1715            }
1716        }
1717    }
1718
1719    let result = if around {
1720        opening.start..closing.end
1721    } else {
1722        opening.end..closing.start
1723    };
1724
1725    Some(
1726        map.clip_point(result.start.to_display_point(map), Bias::Left)
1727            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1728    )
1729}
1730
1731#[cfg(test)]
1732mod test {
1733    use gpui::KeyBinding;
1734    use indoc::indoc;
1735
1736    use crate::{
1737        object::{AnyBrackets, AnyQuotes, MiniBrackets},
1738        state::Mode,
1739        test::{NeovimBackedTestContext, VimTestContext},
1740    };
1741
1742    const WORD_LOCATIONS: &str = indoc! {"
1743        The quick ˇbrowˇnˇ•••
1744        fox ˇjuˇmpsˇ over
1745        the lazy dogˇ••
1746        ˇ
1747        ˇ
1748        ˇ
1749        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1750        ˇ••
1751        ˇ••
1752        ˇ  fox-jumpˇs over
1753        the lazy dogˇ•
1754        ˇ
1755        "
1756    };
1757
1758    #[gpui::test]
1759    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1760        let mut cx = NeovimBackedTestContext::new(cx).await;
1761
1762        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1763            .await
1764            .assert_matches();
1765        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1766            .await
1767            .assert_matches();
1768        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1769            .await
1770            .assert_matches();
1771        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1772            .await
1773            .assert_matches();
1774    }
1775
1776    #[gpui::test]
1777    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1778        let mut cx = NeovimBackedTestContext::new(cx).await;
1779
1780        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1781            .await
1782            .assert_matches();
1783        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1784            .await
1785            .assert_matches();
1786        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1787            .await
1788            .assert_matches();
1789        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1790            .await
1791            .assert_matches();
1792    }
1793
1794    #[gpui::test]
1795    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1796        let mut cx = NeovimBackedTestContext::new(cx).await;
1797
1798        /*
1799                cx.set_shared_state("The quick ˇbrown\nfox").await;
1800                cx.simulate_shared_keystrokes(["v"]).await;
1801                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1802                cx.simulate_shared_keystrokes(["i", "w"]).await;
1803                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1804        */
1805        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1806        cx.simulate_shared_keystrokes("v").await;
1807        cx.shared_state()
1808            .await
1809            .assert_eq("The quick brown\n«\nˇ»fox");
1810        cx.simulate_shared_keystrokes("i w").await;
1811        cx.shared_state()
1812            .await
1813            .assert_eq("The quick brown\n«\nˇ»fox");
1814
1815        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1816            .await
1817            .assert_matches();
1818        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1819            .await
1820            .assert_matches();
1821    }
1822
1823    const PARAGRAPH_EXAMPLES: &[&str] = &[
1824        // Single line
1825        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1826        // Multiple lines without empty lines
1827        indoc! {"
1828            ˇThe quick brownˇ
1829            ˇfox jumps overˇ
1830            the lazy dog.ˇ
1831        "},
1832        // Heading blank paragraph and trailing normal paragraph
1833        indoc! {"
1834            ˇ
1835            ˇ
1836            ˇThe quick brown fox jumps
1837            ˇover the lazy dog.
1838            ˇ
1839            ˇ
1840            ˇThe quick brown fox jumpsˇ
1841            ˇover the lazy dog.ˇ
1842        "},
1843        // Inserted blank paragraph and trailing blank paragraph
1844        indoc! {"
1845            ˇThe quick brown fox jumps
1846            ˇover the lazy dog.
1847            ˇ
1848            ˇ
1849            ˇ
1850            ˇThe quick brown fox jumpsˇ
1851            ˇover the lazy dog.ˇ
1852            ˇ
1853            ˇ
1854            ˇ
1855        "},
1856        // "Blank" paragraph with whitespace characters
1857        indoc! {"
1858            ˇThe quick brown fox jumps
1859            over the lazy dog.
1860
1861            ˇ \t
1862
1863            ˇThe quick brown fox jumps
1864            over the lazy dog.ˇ
1865            ˇ
1866            ˇ \t
1867            \t \t
1868        "},
1869        // Single line "paragraphs", where selection size might be zero.
1870        indoc! {"
1871            ˇThe quick brown fox jumps over the lazy dog.
1872            ˇ
1873            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1874            ˇ
1875        "},
1876    ];
1877
1878    #[gpui::test]
1879    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1880        let mut cx = NeovimBackedTestContext::new(cx).await;
1881
1882        for paragraph_example in PARAGRAPH_EXAMPLES {
1883            cx.simulate_at_each_offset("c i p", paragraph_example)
1884                .await
1885                .assert_matches();
1886            cx.simulate_at_each_offset("c a p", paragraph_example)
1887                .await
1888                .assert_matches();
1889        }
1890    }
1891
1892    #[gpui::test]
1893    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1894        let mut cx = NeovimBackedTestContext::new(cx).await;
1895
1896        for paragraph_example in PARAGRAPH_EXAMPLES {
1897            cx.simulate_at_each_offset("d i p", paragraph_example)
1898                .await
1899                .assert_matches();
1900            cx.simulate_at_each_offset("d a p", paragraph_example)
1901                .await
1902                .assert_matches();
1903        }
1904    }
1905
1906    #[gpui::test]
1907    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1908        let mut cx = NeovimBackedTestContext::new(cx).await;
1909
1910        const EXAMPLES: &[&str] = &[
1911            indoc! {"
1912                ˇThe quick brown
1913                fox jumps over
1914                the lazy dog.
1915            "},
1916            indoc! {"
1917                ˇ
1918
1919                ˇThe quick brown fox jumps
1920                over the lazy dog.
1921                ˇ
1922
1923                ˇThe quick brown fox jumps
1924                over the lazy dog.
1925            "},
1926            indoc! {"
1927                ˇThe quick brown fox jumps over the lazy dog.
1928                ˇ
1929                ˇThe quick brown fox jumps over the lazy dog.
1930
1931            "},
1932        ];
1933
1934        for paragraph_example in EXAMPLES {
1935            cx.simulate_at_each_offset("v i p", paragraph_example)
1936                .await
1937                .assert_matches();
1938            cx.simulate_at_each_offset("v a p", paragraph_example)
1939                .await
1940                .assert_matches();
1941        }
1942    }
1943
1944    #[gpui::test]
1945    async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
1946        let mut cx = NeovimBackedTestContext::new(cx).await;
1947
1948        const WRAPPING_EXAMPLE: &str = indoc! {"
1949            ˇ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.
1950
1951            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
1952
1953            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
1954        "};
1955
1956        cx.set_shared_wrap(20).await;
1957
1958        cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
1959            .await
1960            .assert_matches();
1961        cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
1962            .await
1963            .assert_matches();
1964    }
1965
1966    #[gpui::test]
1967    async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
1968        let mut cx = NeovimBackedTestContext::new(cx).await;
1969
1970        const WRAPPING_EXAMPLE: &str = indoc! {"
1971            ˇ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.
1972
1973            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
1974
1975            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
1976        "};
1977
1978        cx.set_shared_wrap(20).await;
1979
1980        cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
1981            .await
1982            .assert_matches();
1983        cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
1984            .await
1985            .assert_matches();
1986    }
1987
1988    #[gpui::test]
1989    async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
1990        let mut cx = NeovimBackedTestContext::new(cx).await;
1991
1992        cx.set_shared_state(indoc! {"
1993            a
1994                   ˇ•
1995            aaaaaaaaaaaaa
1996        "})
1997            .await;
1998
1999        cx.simulate_shared_keystrokes("d i p").await;
2000        cx.shared_state().await.assert_eq(indoc! {"
2001            a
2002            aaaaaaaˇaaaaaa
2003        "});
2004    }
2005
2006    #[gpui::test]
2007    async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2008        let mut cx = NeovimBackedTestContext::new(cx).await;
2009
2010        const WRAPPING_EXAMPLE: &str = indoc! {"
2011            ˇ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.
2012
2013            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2014
2015            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2016        "};
2017
2018        cx.set_shared_wrap(20).await;
2019
2020        cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
2021            .await
2022            .assert_matches();
2023        cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
2024            .await
2025            .assert_matches();
2026    }
2027
2028    // Test string with "`" for opening surrounders and "'" for closing surrounders
2029    const SURROUNDING_MARKER_STRING: &str = indoc! {"
2030        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
2031        'ˇfox juˇmps ov`ˇer
2032        the ˇlazy d'o`ˇg"};
2033
2034    const SURROUNDING_OBJECTS: &[(char, char)] = &[
2035        ('"', '"'), // Double Quote
2036        ('(', ')'), // Parentheses
2037    ];
2038
2039    #[gpui::test]
2040    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2041        let mut cx = NeovimBackedTestContext::new(cx).await;
2042
2043        for (start, end) in SURROUNDING_OBJECTS {
2044            let marked_string = SURROUNDING_MARKER_STRING
2045                .replace('`', &start.to_string())
2046                .replace('\'', &end.to_string());
2047
2048            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
2049                .await
2050                .assert_matches();
2051            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
2052                .await
2053                .assert_matches();
2054            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
2055                .await
2056                .assert_matches();
2057            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
2058                .await
2059                .assert_matches();
2060        }
2061    }
2062    #[gpui::test]
2063    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2064        let mut cx = NeovimBackedTestContext::new(cx).await;
2065        cx.set_shared_wrap(12).await;
2066
2067        cx.set_shared_state(indoc! {
2068            "\"ˇhello world\"!"
2069        })
2070        .await;
2071        cx.simulate_shared_keystrokes("v i \"").await;
2072        cx.shared_state().await.assert_eq(indoc! {
2073            "\"«hello worldˇ»\"!"
2074        });
2075
2076        cx.set_shared_state(indoc! {
2077            "\"hˇello world\"!"
2078        })
2079        .await;
2080        cx.simulate_shared_keystrokes("v i \"").await;
2081        cx.shared_state().await.assert_eq(indoc! {
2082            "\"«hello worldˇ»\"!"
2083        });
2084
2085        cx.set_shared_state(indoc! {
2086            "helˇlo \"world\"!"
2087        })
2088        .await;
2089        cx.simulate_shared_keystrokes("v i \"").await;
2090        cx.shared_state().await.assert_eq(indoc! {
2091            "hello \"«worldˇ»\"!"
2092        });
2093
2094        cx.set_shared_state(indoc! {
2095            "hello \"wˇorld\"!"
2096        })
2097        .await;
2098        cx.simulate_shared_keystrokes("v i \"").await;
2099        cx.shared_state().await.assert_eq(indoc! {
2100            "hello \"«worldˇ»\"!"
2101        });
2102
2103        cx.set_shared_state(indoc! {
2104            "hello \"wˇorld\"!"
2105        })
2106        .await;
2107        cx.simulate_shared_keystrokes("v a \"").await;
2108        cx.shared_state().await.assert_eq(indoc! {
2109            "hello« \"world\"ˇ»!"
2110        });
2111
2112        cx.set_shared_state(indoc! {
2113            "hello \"wˇorld\" !"
2114        })
2115        .await;
2116        cx.simulate_shared_keystrokes("v a \"").await;
2117        cx.shared_state().await.assert_eq(indoc! {
2118            "hello «\"world\" ˇ»!"
2119        });
2120
2121        cx.set_shared_state(indoc! {
2122            "hello \"wˇorld\"2123            goodbye"
2124        })
2125        .await;
2126        cx.simulate_shared_keystrokes("v a \"").await;
2127        cx.shared_state().await.assert_eq(indoc! {
2128            "hello «\"world\" ˇ»
2129            goodbye"
2130        });
2131    }
2132
2133    #[gpui::test]
2134    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2135        let mut cx = VimTestContext::new(cx, true).await;
2136
2137        cx.set_state(
2138            indoc! {
2139                "func empty(a string) bool {
2140                   if a == \"\" {
2141                      return true
2142                   }
2143                   ˇreturn false
2144                }"
2145            },
2146            Mode::Normal,
2147        );
2148        cx.simulate_keystrokes("v i {");
2149        cx.assert_state(
2150            indoc! {
2151                "func empty(a string) bool {
2152                   «if a == \"\" {
2153                      return true
2154                   }
2155                   return falseˇ»
2156                }"
2157            },
2158            Mode::Visual,
2159        );
2160
2161        cx.set_state(
2162            indoc! {
2163                "func empty(a string) bool {
2164                     if a == \"\" {
2165                         ˇreturn true
2166                     }
2167                     return false
2168                }"
2169            },
2170            Mode::Normal,
2171        );
2172        cx.simulate_keystrokes("v i {");
2173        cx.assert_state(
2174            indoc! {
2175                "func empty(a string) bool {
2176                     if a == \"\" {
2177                         «return trueˇ»
2178                     }
2179                     return false
2180                }"
2181            },
2182            Mode::Visual,
2183        );
2184
2185        cx.set_state(
2186            indoc! {
2187                "func empty(a string) bool {
2188                     if a == \"\" ˇ{
2189                         return true
2190                     }
2191                     return false
2192                }"
2193            },
2194            Mode::Normal,
2195        );
2196        cx.simulate_keystrokes("v i {");
2197        cx.assert_state(
2198            indoc! {
2199                "func empty(a string) bool {
2200                     if a == \"\" {
2201                         «return trueˇ»
2202                     }
2203                     return false
2204                }"
2205            },
2206            Mode::Visual,
2207        );
2208
2209        cx.set_state(
2210            indoc! {
2211                "func empty(a string) bool {
2212                     if a == \"\" {
2213                         return true
2214                     }
2215                     return false
2216                ˇ}"
2217            },
2218            Mode::Normal,
2219        );
2220        cx.simulate_keystrokes("v i {");
2221        cx.assert_state(
2222            indoc! {
2223                "func empty(a string) bool {
2224                     «if a == \"\" {
2225                         return true
2226                     }
2227                     return falseˇ»
2228                }"
2229            },
2230            Mode::Visual,
2231        );
2232
2233        cx.set_state(
2234            indoc! {
2235                "func empty(a string) bool {
2236                             if a == \"\" {
2237                             ˇ
2238
2239                             }"
2240            },
2241            Mode::Normal,
2242        );
2243        cx.simulate_keystrokes("c i {");
2244        cx.assert_state(
2245            indoc! {
2246                "func empty(a string) bool {
2247                             if a == \"\" {ˇ}"
2248            },
2249            Mode::Insert,
2250        );
2251    }
2252
2253    #[gpui::test]
2254    async fn test_singleline_surrounding_character_objects_with_escape(
2255        cx: &mut gpui::TestAppContext,
2256    ) {
2257        let mut cx = NeovimBackedTestContext::new(cx).await;
2258        cx.set_shared_state(indoc! {
2259            "h\"e\\\"lˇlo \\\"world\"!"
2260        })
2261        .await;
2262        cx.simulate_shared_keystrokes("v i \"").await;
2263        cx.shared_state().await.assert_eq(indoc! {
2264            "h\"«e\\\"llo \\\"worldˇ»\"!"
2265        });
2266
2267        cx.set_shared_state(indoc! {
2268            "hello \"teˇst \\\"inside\\\" world\""
2269        })
2270        .await;
2271        cx.simulate_shared_keystrokes("v i \"").await;
2272        cx.shared_state().await.assert_eq(indoc! {
2273            "hello \"«test \\\"inside\\\" worldˇ»\""
2274        });
2275    }
2276
2277    #[gpui::test]
2278    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2279        let mut cx = VimTestContext::new(cx, true).await;
2280        cx.set_state(
2281            indoc! {"
2282            fn boop() {
2283                baz(ˇ|a, b| { bar(|j, k| { })})
2284            }"
2285            },
2286            Mode::Normal,
2287        );
2288        cx.simulate_keystrokes("c i |");
2289        cx.assert_state(
2290            indoc! {"
2291            fn boop() {
2292                baz(|ˇ| { bar(|j, k| { })})
2293            }"
2294            },
2295            Mode::Insert,
2296        );
2297        cx.simulate_keystrokes("escape 1 8 |");
2298        cx.assert_state(
2299            indoc! {"
2300            fn boop() {
2301                baz(|| { bar(ˇ|j, k| { })})
2302            }"
2303            },
2304            Mode::Normal,
2305        );
2306
2307        cx.simulate_keystrokes("v a |");
2308        cx.assert_state(
2309            indoc! {"
2310            fn boop() {
2311                baz(|| { bar(«|j, k| ˇ»{ })})
2312            }"
2313            },
2314            Mode::Visual,
2315        );
2316    }
2317
2318    #[gpui::test]
2319    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2320        let mut cx = VimTestContext::new(cx, true).await;
2321
2322        // Generic arguments
2323        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2324        cx.simulate_keystrokes("v i a");
2325        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2326
2327        // Function arguments
2328        cx.set_state(
2329            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2330            Mode::Normal,
2331        );
2332        cx.simulate_keystrokes("d a a");
2333        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2334
2335        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2336        cx.simulate_keystrokes("v a a");
2337        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2338
2339        // Tuple, vec, and array arguments
2340        cx.set_state(
2341            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2342            Mode::Normal,
2343        );
2344        cx.simulate_keystrokes("c i a");
2345        cx.assert_state(
2346            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2347            Mode::Insert,
2348        );
2349
2350        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2351        cx.simulate_keystrokes("c a a");
2352        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2353
2354        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2355        cx.simulate_keystrokes("c i a");
2356        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2357
2358        cx.set_state(
2359            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2360            Mode::Normal,
2361        );
2362        cx.simulate_keystrokes("c a a");
2363        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2364
2365        // Cursor immediately before / after brackets
2366        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2367        cx.simulate_keystrokes("v i a");
2368        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2369
2370        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2371        cx.simulate_keystrokes("v i a");
2372        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2373    }
2374
2375    #[gpui::test]
2376    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2377        let mut cx = VimTestContext::new(cx, true).await;
2378
2379        // Base use case
2380        cx.set_state(
2381            indoc! {"
2382                fn boop() {
2383                    // Comment
2384                    baz();ˇ
2385
2386                    loop {
2387                        bar(1);
2388                        bar(2);
2389                    }
2390
2391                    result
2392                }
2393            "},
2394            Mode::Normal,
2395        );
2396        cx.simulate_keystrokes("v i i");
2397        cx.assert_state(
2398            indoc! {"
2399                fn boop() {
2400                «    // Comment
2401                    baz();
2402
2403                    loop {
2404                        bar(1);
2405                        bar(2);
2406                    }
2407
2408                    resultˇ»
2409                }
2410            "},
2411            Mode::Visual,
2412        );
2413
2414        // Around indent (include line above)
2415        cx.set_state(
2416            indoc! {"
2417                const ABOVE: str = true;
2418                fn boop() {
2419
2420                    hello();
2421                    worˇld()
2422                }
2423            "},
2424            Mode::Normal,
2425        );
2426        cx.simulate_keystrokes("v a i");
2427        cx.assert_state(
2428            indoc! {"
2429                const ABOVE: str = true;
2430                «fn boop() {
2431
2432                    hello();
2433                    world()ˇ»
2434                }
2435            "},
2436            Mode::Visual,
2437        );
2438
2439        // Around indent (include line above & below)
2440        cx.set_state(
2441            indoc! {"
2442                const ABOVE: str = true;
2443                fn boop() {
2444                    hellˇo();
2445                    world()
2446
2447                }
2448                const BELOW: str = true;
2449            "},
2450            Mode::Normal,
2451        );
2452        cx.simulate_keystrokes("c a shift-i");
2453        cx.assert_state(
2454            indoc! {"
2455                const ABOVE: str = true;
2456                ˇ
2457                const BELOW: str = true;
2458            "},
2459            Mode::Insert,
2460        );
2461    }
2462
2463    #[gpui::test]
2464    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2465        let mut cx = NeovimBackedTestContext::new(cx).await;
2466
2467        for (start, end) in SURROUNDING_OBJECTS {
2468            let marked_string = SURROUNDING_MARKER_STRING
2469                .replace('`', &start.to_string())
2470                .replace('\'', &end.to_string());
2471
2472            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2473                .await
2474                .assert_matches();
2475            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2476                .await
2477                .assert_matches();
2478            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2479                .await
2480                .assert_matches();
2481            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2482                .await
2483                .assert_matches();
2484        }
2485    }
2486
2487    #[gpui::test]
2488    async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2489        let mut cx = VimTestContext::new(cx, true).await;
2490        cx.update(|_, cx| {
2491            cx.bind_keys([KeyBinding::new(
2492                "q",
2493                AnyQuotes,
2494                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2495            )]);
2496        });
2497
2498        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2499            // the false string in the middle should be considered
2500            (
2501                "c i q",
2502                "'first' false ˇstring 'second'",
2503                "'first'ˇ'second'",
2504                Mode::Insert,
2505            ),
2506            // Single quotes
2507            (
2508                "c i q",
2509                "Thisˇ is a 'quote' example.",
2510                "This is a 'ˇ' example.",
2511                Mode::Insert,
2512            ),
2513            (
2514                "c a q",
2515                "Thisˇ is a 'quote' example.",
2516                "This is a ˇexample.",
2517                Mode::Insert,
2518            ),
2519            (
2520                "c i q",
2521                "This is a \"simple 'qˇuote'\" example.",
2522                "This is a \"simple 'ˇ'\" example.",
2523                Mode::Insert,
2524            ),
2525            (
2526                "c a q",
2527                "This is a \"simple 'qˇuote'\" example.",
2528                "This is a \"simpleˇ\" example.",
2529                Mode::Insert,
2530            ),
2531            (
2532                "c i q",
2533                "This is a 'qˇuote' example.",
2534                "This is a 'ˇ' example.",
2535                Mode::Insert,
2536            ),
2537            (
2538                "c a q",
2539                "This is a 'qˇuote' example.",
2540                "This is a ˇexample.",
2541                Mode::Insert,
2542            ),
2543            (
2544                "d i q",
2545                "This is a 'qˇuote' example.",
2546                "This is a 'ˇ' example.",
2547                Mode::Normal,
2548            ),
2549            (
2550                "d a q",
2551                "This is a 'qˇuote' example.",
2552                "This is a ˇexample.",
2553                Mode::Normal,
2554            ),
2555            // Double quotes
2556            (
2557                "c i q",
2558                "This is a \"qˇuote\" example.",
2559                "This is a \"ˇ\" example.",
2560                Mode::Insert,
2561            ),
2562            (
2563                "c a q",
2564                "This is a \"qˇuote\" example.",
2565                "This is a ˇexample.",
2566                Mode::Insert,
2567            ),
2568            (
2569                "d i q",
2570                "This is a \"qˇuote\" example.",
2571                "This is a \"ˇ\" example.",
2572                Mode::Normal,
2573            ),
2574            (
2575                "d a q",
2576                "This is a \"qˇuote\" example.",
2577                "This is a ˇexample.",
2578                Mode::Normal,
2579            ),
2580            // Back quotes
2581            (
2582                "c i q",
2583                "This is a `qˇuote` example.",
2584                "This is a `ˇ` example.",
2585                Mode::Insert,
2586            ),
2587            (
2588                "c a q",
2589                "This is a `qˇuote` example.",
2590                "This is a ˇexample.",
2591                Mode::Insert,
2592            ),
2593            (
2594                "d i q",
2595                "This is a `qˇuote` example.",
2596                "This is a `ˇ` example.",
2597                Mode::Normal,
2598            ),
2599            (
2600                "d a q",
2601                "This is a `qˇuote` example.",
2602                "This is a ˇexample.",
2603                Mode::Normal,
2604            ),
2605        ];
2606
2607        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2608            cx.set_state(initial_state, Mode::Normal);
2609
2610            cx.simulate_keystrokes(keystrokes);
2611
2612            cx.assert_state(expected_state, *expected_mode);
2613        }
2614
2615        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2616            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2617            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2618            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2619            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2620            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2621            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2622            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2623            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2624            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2625            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2626            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2627            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2628        ];
2629
2630        for (keystrokes, initial_state, mode) in INVALID_CASES {
2631            cx.set_state(initial_state, Mode::Normal);
2632
2633            cx.simulate_keystrokes(keystrokes);
2634
2635            cx.assert_state(initial_state, *mode);
2636        }
2637    }
2638
2639    #[gpui::test]
2640    async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2641        let mut cx = VimTestContext::new_typescript(cx).await;
2642
2643        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2644            // Special cases from mini.ai plugin
2645            // the false string in the middle should not be considered
2646            (
2647                "c i q",
2648                "'first' false ˇstring 'second'",
2649                "'first' false string 'ˇ'",
2650                Mode::Insert,
2651            ),
2652            // Multiline support :)! Same behavior as mini.ai plugin
2653            (
2654                "c i q",
2655                indoc! {"
2656                    `
2657                    first
2658                    middle ˇstring
2659                    second
2660                    `
2661                "},
2662                indoc! {"
2663                    `ˇ`
2664                "},
2665                Mode::Insert,
2666            ),
2667            // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2668            // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2669            // Bug reference: https://github.com/zed-industries/zed/issues/23889
2670            ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2671            // Single quotes
2672            (
2673                "c i q",
2674                "Thisˇ is a 'quote' example.",
2675                "This is a 'ˇ' example.",
2676                Mode::Insert,
2677            ),
2678            (
2679                "c a q",
2680                "Thisˇ is a 'quote' example.",
2681                "This is a ˇ example.", // same mini.ai plugin behavior
2682                Mode::Insert,
2683            ),
2684            (
2685                "c i q",
2686                "This is a \"simple 'qˇuote'\" example.",
2687                "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2688                Mode::Insert,
2689            ),
2690            (
2691                "c a 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 i q",
2698                "This is a 'qˇuote' example.",
2699                "This is a 'ˇ' example.",
2700                Mode::Insert,
2701            ),
2702            (
2703                "c a q",
2704                "This is a 'qˇuote' example.",
2705                "This is a ˇ example.", // same mini.ai plugin behavior
2706                Mode::Insert,
2707            ),
2708            (
2709                "d i q",
2710                "This is a 'qˇuote' example.",
2711                "This is a 'ˇ' example.",
2712                Mode::Normal,
2713            ),
2714            (
2715                "d a q",
2716                "This is a 'qˇuote' example.",
2717                "This is a ˇ example.", // same mini.ai plugin behavior
2718                Mode::Normal,
2719            ),
2720            // Double quotes
2721            (
2722                "c i q",
2723                "This is a \"qˇuote\" example.",
2724                "This is a \"ˇ\" example.",
2725                Mode::Insert,
2726            ),
2727            (
2728                "c a q",
2729                "This is a \"qˇuote\" example.",
2730                "This is a ˇ example.", // same mini.ai plugin behavior
2731                Mode::Insert,
2732            ),
2733            (
2734                "d i q",
2735                "This is a \"qˇuote\" example.",
2736                "This is a \"ˇ\" example.",
2737                Mode::Normal,
2738            ),
2739            (
2740                "d a q",
2741                "This is a \"qˇuote\" example.",
2742                "This is a ˇ example.", // same mini.ai plugin behavior
2743                Mode::Normal,
2744            ),
2745            // Back quotes
2746            (
2747                "c i q",
2748                "This is a `qˇuote` example.",
2749                "This is a `ˇ` example.",
2750                Mode::Insert,
2751            ),
2752            (
2753                "c a q",
2754                "This is a `qˇuote` example.",
2755                "This is a ˇ example.", // same mini.ai plugin behavior
2756                Mode::Insert,
2757            ),
2758            (
2759                "d i q",
2760                "This is a `qˇuote` example.",
2761                "This is a `ˇ` example.",
2762                Mode::Normal,
2763            ),
2764            (
2765                "d a q",
2766                "This is a `qˇuote` example.",
2767                "This is a ˇ example.", // same mini.ai plugin behavior
2768                Mode::Normal,
2769            ),
2770        ];
2771
2772        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2773            cx.set_state(initial_state, Mode::Normal);
2774
2775            cx.simulate_keystrokes(keystrokes);
2776
2777            cx.assert_state(expected_state, *expected_mode);
2778        }
2779
2780        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2781            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2782            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2783            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2784            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2785            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2786            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2787            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2788            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2789            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2790            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2791            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2792            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2793        ];
2794
2795        for (keystrokes, initial_state, mode) in INVALID_CASES {
2796            cx.set_state(initial_state, Mode::Normal);
2797
2798            cx.simulate_keystrokes(keystrokes);
2799
2800            cx.assert_state(initial_state, *mode);
2801        }
2802    }
2803
2804    #[gpui::test]
2805    async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2806        let mut cx = VimTestContext::new(cx, true).await;
2807        cx.update(|_, cx| {
2808            cx.bind_keys([KeyBinding::new(
2809                "b",
2810                AnyBrackets,
2811                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2812            )]);
2813        });
2814
2815        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2816            (
2817                "c i b",
2818                indoc! {"
2819                    {
2820                        {
2821                            ˇprint('hello')
2822                        }
2823                    }
2824                "},
2825                indoc! {"
2826                    {
2827                        {
2828                            ˇ
2829                        }
2830                    }
2831                "},
2832                Mode::Insert,
2833            ),
2834            // Bracket (Parentheses)
2835            (
2836                "c i b",
2837                "Thisˇ is a (simple [quote]) example.",
2838                "This is a (ˇ) example.",
2839                Mode::Insert,
2840            ),
2841            (
2842                "c i b",
2843                "This is a [simple (qˇuote)] example.",
2844                "This is a [simple (ˇ)] example.",
2845                Mode::Insert,
2846            ),
2847            (
2848                "c a 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 [quote]) example.",
2856                "This is a ˇ example.",
2857                Mode::Insert,
2858            ),
2859            (
2860                "c i b",
2861                "This is a (qˇuote) example.",
2862                "This is a (ˇ) example.",
2863                Mode::Insert,
2864            ),
2865            (
2866                "c a b",
2867                "This is a (qˇuote) example.",
2868                "This is a ˇ example.",
2869                Mode::Insert,
2870            ),
2871            (
2872                "d i b",
2873                "This is a (qˇuote) example.",
2874                "This is a (ˇ) example.",
2875                Mode::Normal,
2876            ),
2877            (
2878                "d a b",
2879                "This is a (qˇuote) example.",
2880                "This is a ˇ example.",
2881                Mode::Normal,
2882            ),
2883            // Square brackets
2884            (
2885                "c i b",
2886                "This is a [qˇuote] example.",
2887                "This is a [ˇ] example.",
2888                Mode::Insert,
2889            ),
2890            (
2891                "c a b",
2892                "This is a [qˇuote] example.",
2893                "This is a ˇ example.",
2894                Mode::Insert,
2895            ),
2896            (
2897                "d i b",
2898                "This is a [qˇuote] example.",
2899                "This is a [ˇ] example.",
2900                Mode::Normal,
2901            ),
2902            (
2903                "d a b",
2904                "This is a [qˇuote] example.",
2905                "This is a ˇ example.",
2906                Mode::Normal,
2907            ),
2908            // Curly brackets
2909            (
2910                "c i b",
2911                "This is a {qˇuote} example.",
2912                "This is a {ˇ} example.",
2913                Mode::Insert,
2914            ),
2915            (
2916                "c a b",
2917                "This is a {qˇuote} example.",
2918                "This is a ˇ example.",
2919                Mode::Insert,
2920            ),
2921            (
2922                "d i b",
2923                "This is a {qˇuote} example.",
2924                "This is a {ˇ} example.",
2925                Mode::Normal,
2926            ),
2927            (
2928                "d a b",
2929                "This is a {qˇuote} example.",
2930                "This is a ˇ example.",
2931                Mode::Normal,
2932            ),
2933        ];
2934
2935        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2936            cx.set_state(initial_state, Mode::Normal);
2937
2938            cx.simulate_keystrokes(keystrokes);
2939
2940            cx.assert_state(expected_state, *expected_mode);
2941        }
2942
2943        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2944            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2945            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2946            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2947            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2948            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2949            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2950            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2951            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2952            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2953            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2954            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2955            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2956        ];
2957
2958        for (keystrokes, initial_state, mode) in INVALID_CASES {
2959            cx.set_state(initial_state, Mode::Normal);
2960
2961            cx.simulate_keystrokes(keystrokes);
2962
2963            cx.assert_state(initial_state, *mode);
2964        }
2965    }
2966
2967    #[gpui::test]
2968    async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
2969        let mut cx = VimTestContext::new(cx, true).await;
2970        cx.update(|_, cx| {
2971            cx.bind_keys([KeyBinding::new(
2972                "b",
2973                MiniBrackets,
2974                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2975            )]);
2976        });
2977
2978        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2979            // Special cases from mini.ai plugin
2980            // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
2981            // Same behavior as mini.ai plugin
2982            (
2983                "c i b",
2984                indoc! {"
2985                    {
2986                        {
2987                            ˇprint('hello')
2988                        }
2989                    }
2990                "},
2991                indoc! {"
2992                    {
2993                        {
2994                            print(ˇ)
2995                        }
2996                    }
2997                "},
2998                Mode::Insert,
2999            ),
3000            // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3001            // Same behavior as mini.ai plugin
3002            (
3003                "c i b",
3004                indoc! {"
3005                    {
3006                        {
3007                            ˇ
3008                            print('hello')
3009                        }
3010                    }
3011                "},
3012                indoc! {"
3013                    {
3014                        {ˇ}
3015                    }
3016                "},
3017                Mode::Insert,
3018            ),
3019            // If you are in the open bracket then it has higher priority
3020            (
3021                "c i b",
3022                indoc! {"
3023                    «{ˇ»
3024                        {
3025                            print('hello')
3026                        }
3027                    }
3028                "},
3029                indoc! {"
3030                    {ˇ}
3031                "},
3032                Mode::Insert,
3033            ),
3034            // If you are in the close bracket then it has higher priority
3035            (
3036                "c i b",
3037                indoc! {"
3038                    {
3039                        {
3040                            print('hello')
3041                        }
3042                    «}ˇ»
3043                "},
3044                indoc! {"
3045                    {ˇ}
3046                "},
3047                Mode::Insert,
3048            ),
3049            // Bracket (Parentheses)
3050            (
3051                "c i b",
3052                "Thisˇ is a (simple [quote]) example.",
3053                "This is a (ˇ) example.",
3054                Mode::Insert,
3055            ),
3056            (
3057                "c i b",
3058                "This is a [simple (qˇuote)] example.",
3059                "This is a [simple (ˇ)] example.",
3060                Mode::Insert,
3061            ),
3062            (
3063                "c a 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 [quote]) example.",
3071                "This is a ˇ example.",
3072                Mode::Insert,
3073            ),
3074            (
3075                "c i b",
3076                "This is a (qˇuote) example.",
3077                "This is a (ˇ) example.",
3078                Mode::Insert,
3079            ),
3080            (
3081                "c a b",
3082                "This is a (qˇuote) example.",
3083                "This is a ˇ example.",
3084                Mode::Insert,
3085            ),
3086            (
3087                "d i b",
3088                "This is a (qˇuote) example.",
3089                "This is a (ˇ) example.",
3090                Mode::Normal,
3091            ),
3092            (
3093                "d a b",
3094                "This is a (qˇuote) example.",
3095                "This is a ˇ example.",
3096                Mode::Normal,
3097            ),
3098            // Square brackets
3099            (
3100                "c i b",
3101                "This is a [qˇuote] example.",
3102                "This is a [ˇ] example.",
3103                Mode::Insert,
3104            ),
3105            (
3106                "c a b",
3107                "This is a [qˇuote] example.",
3108                "This is a ˇ example.",
3109                Mode::Insert,
3110            ),
3111            (
3112                "d i b",
3113                "This is a [qˇuote] example.",
3114                "This is a [ˇ] example.",
3115                Mode::Normal,
3116            ),
3117            (
3118                "d a b",
3119                "This is a [qˇuote] example.",
3120                "This is a ˇ example.",
3121                Mode::Normal,
3122            ),
3123            // Curly brackets
3124            (
3125                "c i b",
3126                "This is a {qˇuote} example.",
3127                "This is a {ˇ} example.",
3128                Mode::Insert,
3129            ),
3130            (
3131                "c a b",
3132                "This is a {qˇuote} example.",
3133                "This is a ˇ example.",
3134                Mode::Insert,
3135            ),
3136            (
3137                "d i b",
3138                "This is a {qˇuote} example.",
3139                "This is a {ˇ} example.",
3140                Mode::Normal,
3141            ),
3142            (
3143                "d a b",
3144                "This is a {qˇuote} example.",
3145                "This is a ˇ example.",
3146                Mode::Normal,
3147            ),
3148        ];
3149
3150        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3151            cx.set_state(initial_state, Mode::Normal);
3152
3153            cx.simulate_keystrokes(keystrokes);
3154
3155            cx.assert_state(expected_state, *expected_mode);
3156        }
3157
3158        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3159            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3160            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3161            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3162            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3163            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3164            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3165            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3166            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3167            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3168            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3169            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3170            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3171        ];
3172
3173        for (keystrokes, initial_state, mode) in INVALID_CASES {
3174            cx.set_state(initial_state, Mode::Normal);
3175
3176            cx.simulate_keystrokes(keystrokes);
3177
3178            cx.assert_state(initial_state, *mode);
3179        }
3180    }
3181
3182    #[gpui::test]
3183    async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3184        let mut cx = NeovimBackedTestContext::new(cx).await;
3185        cx.set_shared_state("(trailingˇ whitespace          )")
3186            .await;
3187        cx.simulate_shared_keystrokes("v i b").await;
3188        cx.shared_state().await.assert_matches();
3189        cx.simulate_shared_keystrokes("escape y i b").await;
3190        cx.shared_clipboard()
3191            .await
3192            .assert_eq("trailing whitespace          ");
3193    }
3194
3195    #[gpui::test]
3196    async fn test_tags(cx: &mut gpui::TestAppContext) {
3197        let mut cx = VimTestContext::new_html(cx).await;
3198
3199        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3200        cx.simulate_keystrokes("v i t");
3201        cx.assert_state(
3202            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3203            Mode::Visual,
3204        );
3205        cx.simulate_keystrokes("a t");
3206        cx.assert_state(
3207            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3208            Mode::Visual,
3209        );
3210        cx.simulate_keystrokes("a t");
3211        cx.assert_state(
3212            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3213            Mode::Visual,
3214        );
3215
3216        // The cursor is before the tag
3217        cx.set_state(
3218            "<html><head></head><body> ˇ  <b>hi!</b></body>",
3219            Mode::Normal,
3220        );
3221        cx.simulate_keystrokes("v i t");
3222        cx.assert_state(
3223            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
3224            Mode::Visual,
3225        );
3226        cx.simulate_keystrokes("a t");
3227        cx.assert_state(
3228            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
3229            Mode::Visual,
3230        );
3231
3232        // The cursor is in the open tag
3233        cx.set_state(
3234            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3235            Mode::Normal,
3236        );
3237        cx.simulate_keystrokes("v a t");
3238        cx.assert_state(
3239            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3240            Mode::Visual,
3241        );
3242        cx.simulate_keystrokes("i t");
3243        cx.assert_state(
3244            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3245            Mode::Visual,
3246        );
3247
3248        // current selection length greater than 1
3249        cx.set_state(
3250            "<html><head></head><body><«b>hi!ˇ»</b></body>",
3251            Mode::Visual,
3252        );
3253        cx.simulate_keystrokes("i t");
3254        cx.assert_state(
3255            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3256            Mode::Visual,
3257        );
3258        cx.simulate_keystrokes("a t");
3259        cx.assert_state(
3260            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3261            Mode::Visual,
3262        );
3263
3264        cx.set_state(
3265            "<html><head></head><body><«b>hi!</ˇ»b></body>",
3266            Mode::Visual,
3267        );
3268        cx.simulate_keystrokes("a t");
3269        cx.assert_state(
3270            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3271            Mode::Visual,
3272        );
3273    }
3274    #[gpui::test]
3275    async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3276        let mut cx = NeovimBackedTestContext::new(cx).await;
3277
3278        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3279            .await;
3280        cx.simulate_shared_keystrokes("v a w").await;
3281        cx.shared_state()
3282            .await
3283            .assert_eq("    «const ˇ»f = (x: unknown) => {");
3284
3285        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3286            .await;
3287        cx.simulate_shared_keystrokes("y a w").await;
3288        cx.shared_clipboard().await.assert_eq("const ");
3289
3290        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3291            .await;
3292        cx.simulate_shared_keystrokes("d a w").await;
3293        cx.shared_state()
3294            .await
3295            .assert_eq("    ˇf = (x: unknown) => {");
3296        cx.shared_clipboard().await.assert_eq("const ");
3297
3298        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3299            .await;
3300        cx.simulate_shared_keystrokes("c a w").await;
3301        cx.shared_state()
3302            .await
3303            .assert_eq("    ˇf = (x: unknown) => {");
3304        cx.shared_clipboard().await.assert_eq("const ");
3305    }
3306}