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