object.rs

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