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