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