Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:31:15.048Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

object.rs

4003 lines · 132.5 KB · rust
1use std::ops::Range;
2
3use crate::{
4    Vim,
5    motion::{is_subword_end, is_subword_start, right},
6    state::{Mode, Operator},
7    surrounds::{BRACKET_PAIRS, QUOTE_PAIRS, SurroundPair},
8};
9use editor::{
10    Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset,
11    display_map::{DisplaySnapshot, ToDisplayPoint},
12    movement::{self, FindRange},
13};
14use gpui::{Action, Window, actions};
15use itertools::Itertools;
16use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
17use multi_buffer::MultiBufferRow;
18use schemars::JsonSchema;
19use serde::Deserialize;
20use ui::Context;
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum Object {
25    Word { ignore_punctuation: bool },
26    Subword { ignore_punctuation: bool },
27    Sentence,
28    Paragraph,
29    Quotes,
30    BackQuotes,
31    AnyQuotes,
32    MiniQuotes,
33    DoubleQuotes,
34    VerticalBars,
35    AnyBrackets,
36    MiniBrackets,
37    Parentheses,
38    SquareBrackets,
39    CurlyBrackets,
40    AngleBrackets,
41    Argument,
42    IndentObj { include_below: bool },
43    Tag,
44    Method,
45    Class,
46    Comment,
47    EntireFile,
48}
49
50/// Selects a word text object.
51#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
52#[action(namespace = vim)]
53#[serde(deny_unknown_fields)]
54struct Word {
55    #[serde(default)]
56    ignore_punctuation: bool,
57}
58
59/// Selects a subword text object.
60#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
61#[action(namespace = vim)]
62#[serde(deny_unknown_fields)]
63struct Subword {
64    #[serde(default)]
65    ignore_punctuation: bool,
66}
67/// Selects text at the same indentation level.
68#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
69#[action(namespace = vim)]
70#[serde(deny_unknown_fields)]
71struct IndentObj {
72    #[serde(default)]
73    include_below: bool,
74}
75
76#[derive(Debug, Clone)]
77pub struct CandidateRange {
78    pub start: DisplayPoint,
79    pub end: DisplayPoint,
80}
81
82#[derive(Debug, Clone)]
83pub struct CandidateWithRanges {
84    candidate: CandidateRange,
85    open_range: Range<MultiBufferOffset>,
86    close_range: Range<MultiBufferOffset>,
87}
88
89/// Operates on text within or around parentheses `()`.
90#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
91#[action(namespace = vim)]
92#[serde(deny_unknown_fields)]
93struct Parentheses {
94    #[serde(default)]
95    opening: bool,
96}
97
98/// Operates on text within or around square brackets `[]`.
99#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
100#[action(namespace = vim)]
101#[serde(deny_unknown_fields)]
102struct SquareBrackets {
103    #[serde(default)]
104    opening: bool,
105}
106
107/// Operates on text within or around angle brackets `<>`.
108#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
109#[action(namespace = vim)]
110#[serde(deny_unknown_fields)]
111struct AngleBrackets {
112    #[serde(default)]
113    opening: bool,
114}
115
116/// Operates on text within or around curly brackets `{}`.
117#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
118#[action(namespace = vim)]
119#[serde(deny_unknown_fields)]
120struct CurlyBrackets {
121    #[serde(default)]
122    opening: bool,
123}
124
125fn cover_or_next<I: Iterator<Item = (Range<MultiBufferOffset>, Range<MultiBufferOffset>)>>(
126    candidates: Option<I>,
127    caret: DisplayPoint,
128    map: &DisplaySnapshot,
129) -> Option<CandidateWithRanges> {
130    let caret_offset = caret.to_offset(map, Bias::Left);
131    let mut covering = vec![];
132    let mut next_ones = vec![];
133    let snapshot = map.buffer_snapshot();
134
135    if let Some(ranges) = candidates {
136        for (open_range, close_range) in ranges {
137            let start_off = open_range.start;
138            let end_off = close_range.end;
139            let candidate = CandidateWithRanges {
140                candidate: CandidateRange {
141                    start: start_off.to_display_point(map),
142                    end: end_off.to_display_point(map),
143                },
144                open_range: open_range.clone(),
145                close_range: close_range.clone(),
146            };
147
148            if open_range
149                .start
150                .to_offset(snapshot)
151                .to_display_point(map)
152                .row()
153                == caret_offset.to_display_point(map).row()
154            {
155                if start_off <= caret_offset && caret_offset < end_off {
156                    covering.push(candidate);
157                } else if start_off >= caret_offset {
158                    next_ones.push(candidate);
159                }
160            }
161        }
162    }
163
164    // 1) covering -> smallest width
165    if !covering.is_empty() {
166        return covering.into_iter().min_by_key(|r| {
167            r.candidate.end.to_offset(map, Bias::Right)
168                - r.candidate.start.to_offset(map, Bias::Left)
169        });
170    }
171
172    // 2) next -> closest by start
173    if !next_ones.is_empty() {
174        return next_ones.into_iter().min_by_key(|r| {
175            let start = r.candidate.start.to_offset(map, Bias::Left);
176            (start.0 as isize - caret_offset.0 as isize).abs()
177        });
178    }
179
180    None
181}
182
183type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool;
184
185struct DelimiterRange {
186    open: Range<MultiBufferOffset>,
187    close: Range<MultiBufferOffset>,
188}
189
190impl DelimiterRange {
191    fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range<DisplayPoint> {
192        if around {
193            self.open.start.to_display_point(map)..self.close.end.to_display_point(map)
194        } else {
195            self.open.end.to_display_point(map)..self.close.start.to_display_point(map)
196        }
197    }
198}
199
200fn find_mini_delimiters(
201    map: &DisplaySnapshot,
202    display_point: DisplayPoint,
203    around: bool,
204    is_valid_delimiter: &DelimiterPredicate,
205) -> Option<Range<DisplayPoint>> {
206    let point = map.clip_at_line_end(display_point).to_point(map);
207    let offset = map.buffer_snapshot().point_to_offset(point);
208
209    let line_range = get_line_range(map, point);
210    let visible_line_range = get_visible_line_range(&line_range);
211
212    let snapshot = &map.buffer_snapshot();
213
214    let ranges = map
215        .buffer_snapshot()
216        .bracket_ranges(visible_line_range)
217        .map(|ranges| {
218            ranges.filter_map(|(open, close)| {
219                let (buffer, buffer_open) =
220                    snapshot.range_to_buffer_range::<MultiBufferOffset>(open.clone())?;
221                let (_, buffer_close) =
222                    snapshot.range_to_buffer_range::<MultiBufferOffset>(close.clone())?;
223
224                if is_valid_delimiter(buffer, buffer_open.start, buffer_close.start) {
225                    Some((open, close))
226                } else {
227                    None
228                }
229            })
230        });
231
232    if let Some(candidate) = cover_or_next(ranges, display_point, map) {
233        return Some(
234            DelimiterRange {
235                open: candidate.open_range,
236                close: candidate.close_range,
237            }
238            .to_display_range(map, around),
239        );
240    }
241
242    let results = snapshot.map_excerpt_ranges(offset..offset, |buffer, _, input_range| {
243        let buffer_offset = input_range.start.0;
244        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
245            is_valid_delimiter(buffer, open.start, close.start)
246        };
247        let Some((open, close)) = buffer.innermost_enclosing_bracket_ranges(
248            buffer_offset..buffer_offset,
249            Some(&bracket_filter),
250        ) else {
251            return vec![];
252        };
253        vec![
254            (BufferOffset(open.start)..BufferOffset(open.end), ()),
255            (BufferOffset(close.start)..BufferOffset(close.end), ()),
256        ]
257    })?;
258
259    if results.len() < 2 {
260        return None;
261    }
262
263    Some(
264        DelimiterRange {
265            open: results[0].0.clone(),
266            close: results[1].0.clone(),
267        }
268        .to_display_range(map, around),
269    )
270}
271
272fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range<Point> {
273    let (start, mut end) = (
274        map.prev_line_boundary(point).0,
275        map.next_line_boundary(point).0,
276    );
277
278    if end == point {
279        end = map.max_point().to_point(map);
280    }
281
282    start..end
283}
284
285fn get_visible_line_range(line_range: &Range<Point>) -> Range<Point> {
286    let end_column = line_range.end.column.saturating_sub(1);
287    line_range.start..Point::new(line_range.end.row, end_column)
288}
289
290fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool {
291    matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`'))
292}
293
294fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool {
295    matches!(
296        buffer.chars_at(start).next(),
297        Some('(' | '[' | '{' | '<' | '|')
298    )
299}
300
301fn find_mini_quotes(
302    map: &DisplaySnapshot,
303    display_point: DisplayPoint,
304    around: bool,
305) -> Option<Range<DisplayPoint>> {
306    find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
307}
308
309fn find_mini_brackets(
310    map: &DisplaySnapshot,
311    display_point: DisplayPoint,
312    around: bool,
313) -> Option<Range<DisplayPoint>> {
314    find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
315}
316
317actions!(
318    vim,
319    [
320        /// Selects a sentence text object.
321        Sentence,
322        /// Selects a paragraph text object.
323        Paragraph,
324        /// Selects text within single quotes.
325        Quotes,
326        /// Selects text within backticks.
327        BackQuotes,
328        /// Selects text within the nearest quotes (single or double).
329        MiniQuotes,
330        /// Selects text within any type of quotes.
331        AnyQuotes,
332        /// Selects text within double quotes.
333        DoubleQuotes,
334        /// Selects text within vertical bars (pipes).
335        VerticalBars,
336        /// Selects text within the nearest brackets.
337        MiniBrackets,
338        /// Selects text within any type of brackets.
339        AnyBrackets,
340        /// Selects a function argument.
341        Argument,
342        /// Selects an HTML/XML tag.
343        Tag,
344        /// Selects a method or function.
345        Method,
346        /// Selects a class definition.
347        Class,
348        /// Selects a comment block.
349        Comment,
350        /// Selects the entire file.
351        EntireFile
352    ]
353);
354
355pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
356    Vim::action(
357        editor,
358        cx,
359        |vim, &Word { ignore_punctuation }: &Word, window, cx| {
360            vim.object(Object::Word { ignore_punctuation }, window, cx)
361        },
362    );
363    Vim::action(
364        editor,
365        cx,
366        |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
367            vim.object(Object::Subword { ignore_punctuation }, window, cx)
368        },
369    );
370    Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
371        vim.object(Object::Tag, window, cx)
372    });
373    Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
374        vim.object(Object::Sentence, window, cx)
375    });
376    Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
377        vim.object(Object::Paragraph, window, cx)
378    });
379    Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
380        vim.object(Object::Quotes, window, cx)
381    });
382    Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
383        vim.object(Object::BackQuotes, window, cx)
384    });
385    Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| {
386        vim.object(Object::MiniQuotes, window, cx)
387    });
388    Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| {
389        vim.object(Object::MiniBrackets, window, cx)
390    });
391    Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
392        vim.object(Object::AnyQuotes, window, cx)
393    });
394    Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
395        vim.object(Object::AnyBrackets, window, cx)
396    });
397    Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
398        vim.object(Object::BackQuotes, window, cx)
399    });
400    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
401        vim.object(Object::DoubleQuotes, window, cx)
402    });
403    Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| {
404        vim.object_impl(Object::Parentheses, action.opening, window, cx)
405    });
406    Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
407        vim.object_impl(Object::SquareBrackets, action.opening, window, cx)
408    });
409    Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
410        vim.object_impl(Object::CurlyBrackets, action.opening, window, cx)
411    });
412    Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
413        vim.object_impl(Object::AngleBrackets, action.opening, window, cx)
414    });
415    Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
416        vim.object(Object::VerticalBars, window, cx)
417    });
418    Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
419        vim.object(Object::Argument, window, cx)
420    });
421    Vim::action(editor, cx, |vim, _: &Method, window, cx| {
422        vim.object(Object::Method, window, cx)
423    });
424    Vim::action(editor, cx, |vim, _: &Class, window, cx| {
425        vim.object(Object::Class, window, cx)
426    });
427    Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
428        vim.object(Object::EntireFile, window, cx)
429    });
430    Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
431        if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
432            vim.push_operator(Operator::Object { around: true }, window, cx);
433        }
434        vim.object(Object::Comment, window, cx)
435    });
436    Vim::action(
437        editor,
438        cx,
439        |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
440            vim.object(Object::IndentObj { include_below }, window, cx)
441        },
442    );
443}
444
445impl Vim {
446    fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
447        self.object_impl(object, false, window, cx);
448    }
449
450    fn object_impl(
451        &mut self,
452        object: Object,
453        opening: bool,
454        window: &mut Window,
455        cx: &mut Context<Self>,
456    ) {
457        let count = Self::take_count(cx);
458
459        match self.mode {
460            Mode::Normal | Mode::HelixNormal => {
461                self.normal_object(object, count, opening, window, cx)
462            }
463            Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
464                self.visual_object(object, count, window, cx)
465            }
466            Mode::Insert | Mode::Replace => {
467                // Shouldn't execute a text object in insert mode. Ignoring
468            }
469        }
470    }
471}
472
473impl Object {
474    pub fn is_multiline(self) -> bool {
475        match self {
476            Object::Word { .. }
477            | Object::Subword { .. }
478            | Object::Quotes
479            | Object::BackQuotes
480            | Object::AnyQuotes
481            | Object::MiniQuotes
482            | Object::VerticalBars
483            | Object::DoubleQuotes => false,
484            Object::Sentence
485            | Object::Paragraph
486            | Object::AnyBrackets
487            | Object::MiniBrackets
488            | Object::Parentheses
489            | Object::Tag
490            | Object::AngleBrackets
491            | Object::CurlyBrackets
492            | Object::SquareBrackets
493            | Object::Argument
494            | Object::Method
495            | Object::Class
496            | Object::EntireFile
497            | Object::Comment
498            | Object::IndentObj { .. } => true,
499        }
500    }
501
502    pub fn always_expands_both_ways(self) -> bool {
503        match self {
504            Object::Word { .. }
505            | Object::Subword { .. }
506            | Object::Sentence
507            | Object::Paragraph
508            | Object::Argument
509            | Object::IndentObj { .. } => false,
510            Object::Quotes
511            | Object::BackQuotes
512            | Object::AnyQuotes
513            | Object::MiniQuotes
514            | Object::DoubleQuotes
515            | Object::VerticalBars
516            | Object::AnyBrackets
517            | Object::MiniBrackets
518            | Object::Parentheses
519            | Object::SquareBrackets
520            | Object::Tag
521            | Object::Method
522            | Object::Class
523            | Object::Comment
524            | Object::EntireFile
525            | Object::CurlyBrackets
526            | Object::AngleBrackets => true,
527        }
528    }
529
530    pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
531        match self {
532            Object::Word { .. }
533            | Object::Subword { .. }
534            | Object::Sentence
535            | Object::Quotes
536            | Object::AnyQuotes
537            | Object::MiniQuotes
538            | Object::BackQuotes
539            | Object::DoubleQuotes => {
540                if current_mode == Mode::VisualBlock {
541                    Mode::VisualBlock
542                } else {
543                    Mode::Visual
544                }
545            }
546            Object::Parentheses
547            | Object::AnyBrackets
548            | Object::MiniBrackets
549            | Object::SquareBrackets
550            | Object::CurlyBrackets
551            | Object::AngleBrackets
552            | Object::VerticalBars
553            | Object::Tag
554            | Object::Comment
555            | Object::Argument
556            | Object::IndentObj { .. } => Mode::Visual,
557            Object::Method | Object::Class => {
558                if around {
559                    Mode::VisualLine
560                } else {
561                    Mode::Visual
562                }
563            }
564            Object::Paragraph | Object::EntireFile => Mode::VisualLine,
565        }
566    }
567
568    pub fn range(
569        self,
570        map: &DisplaySnapshot,
571        selection: Selection<DisplayPoint>,
572        around: bool,
573        times: Option<usize>,
574    ) -> Option<Range<DisplayPoint>> {
575        let relative_to = selection.head();
576        match self {
577            Object::Word { ignore_punctuation } => {
578                let count = times.unwrap_or(1);
579                if around {
580                    around_word(map, relative_to, ignore_punctuation, count)
581                } else {
582                    in_word(map, relative_to, ignore_punctuation, count).map(|range| {
583                        // For iw with count > 1, vim includes trailing whitespace
584                        if count > 1 {
585                            let spans_multiple_lines = range.start.row() != range.end.row();
586                            expand_to_include_whitespace(map, range, !spans_multiple_lines)
587                        } else {
588                            range
589                        }
590                    })
591                }
592            }
593            Object::Subword { ignore_punctuation } => {
594                if around {
595                    around_subword(map, relative_to, ignore_punctuation)
596                } else {
597                    in_subword(map, relative_to, ignore_punctuation)
598                }
599            }
600            Object::Sentence => sentence(map, relative_to, around),
601            //change others later
602            Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
603            Object::Quotes => {
604                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
605            }
606            Object::BackQuotes => {
607                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
608            }
609            Object::AnyQuotes => {
610                let cursor_offset = relative_to.to_offset(map, Bias::Left);
611
612                // Find innermost range directly without collecting all ranges
613                let mut innermost = None;
614                let mut min_size = usize::MAX;
615
616                // First pass: find innermost enclosing range
617                for &SurroundPair { open, close } in QUOTE_PAIRS {
618                    if let Some(range) = surrounding_markers(
619                        map,
620                        relative_to,
621                        around,
622                        self.is_multiline(),
623                        open,
624                        close,
625                    ) {
626                        let start_offset = range.start.to_offset(map, Bias::Left);
627                        let end_offset = range.end.to_offset(map, Bias::Right);
628
629                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
630                            let size = end_offset - start_offset;
631                            if size < min_size {
632                                min_size = size;
633                                innermost = Some(range);
634                            }
635                        }
636                    }
637                }
638
639                if let Some(range) = innermost {
640                    return Some(range);
641                }
642
643                // Fallback: find nearest pair if not inside any quotes
644                QUOTE_PAIRS
645                    .iter()
646                    .flat_map(|&SurroundPair { open, close }| {
647                        surrounding_markers(
648                            map,
649                            relative_to,
650                            around,
651                            self.is_multiline(),
652                            open,
653                            close,
654                        )
655                    })
656                    .min_by_key(|range| {
657                        let start_offset = range.start.to_offset(map, Bias::Left);
658                        let end_offset = range.end.to_offset(map, Bias::Right);
659                        if cursor_offset < start_offset {
660                            (start_offset - cursor_offset) as isize
661                        } else if cursor_offset > end_offset {
662                            (cursor_offset - end_offset) as isize
663                        } else {
664                            0
665                        }
666                    })
667            }
668            Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
669            Object::DoubleQuotes => {
670                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
671            }
672            Object::VerticalBars => {
673                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
674            }
675            Object::Parentheses => {
676                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
677            }
678            Object::Tag => {
679                let head = selection.head();
680                let range = selection.range();
681                surrounding_html_tag(map, head, range, around)
682            }
683            Object::AnyBrackets => {
684                let cursor_offset = relative_to.to_offset(map, Bias::Left);
685
686                // Find innermost enclosing bracket range
687                let mut innermost = None;
688                let mut min_size = usize::MAX;
689
690                for &SurroundPair { open, close } in BRACKET_PAIRS {
691                    if let Some(range) = surrounding_markers(
692                        map,
693                        relative_to,
694                        around,
695                        self.is_multiline(),
696                        open,
697                        close,
698                    ) {
699                        let start_offset = range.start.to_offset(map, Bias::Left);
700                        let end_offset = range.end.to_offset(map, Bias::Right);
701
702                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
703                            let size = end_offset - start_offset;
704                            if size < min_size {
705                                min_size = size;
706                                innermost = Some(range);
707                            }
708                        }
709                    }
710                }
711
712                if let Some(range) = innermost {
713                    return Some(range);
714                }
715
716                // Fallback: find nearest bracket pair if not inside any
717                BRACKET_PAIRS
718                    .iter()
719                    .flat_map(|&SurroundPair { open, close }| {
720                        surrounding_markers(
721                            map,
722                            relative_to,
723                            around,
724                            self.is_multiline(),
725                            open,
726                            close,
727                        )
728                    })
729                    .min_by_key(|range| {
730                        let start_offset = range.start.to_offset(map, Bias::Left);
731                        let end_offset = range.end.to_offset(map, Bias::Right);
732                        if cursor_offset < start_offset {
733                            (start_offset - cursor_offset) as isize
734                        } else if cursor_offset > end_offset {
735                            (cursor_offset - end_offset) as isize
736                        } else {
737                            0
738                        }
739                    })
740            }
741            Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
742            Object::SquareBrackets => {
743                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
744            }
745            Object::CurlyBrackets => {
746                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
747            }
748            Object::AngleBrackets => {
749                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
750            }
751            Object::Method => text_object(
752                map,
753                relative_to,
754                if around {
755                    TextObject::AroundFunction
756                } else {
757                    TextObject::InsideFunction
758                },
759            ),
760            Object::Comment => text_object(
761                map,
762                relative_to,
763                if around {
764                    TextObject::AroundComment
765                } else {
766                    TextObject::InsideComment
767                },
768            ),
769            Object::Class => text_object(
770                map,
771                relative_to,
772                if around {
773                    TextObject::AroundClass
774                } else {
775                    TextObject::InsideClass
776                },
777            ),
778            Object::Argument => argument(map, relative_to, around),
779            Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
780            Object::EntireFile => entire_file(map),
781        }
782    }
783
784    pub fn expand_selection(
785        self,
786        map: &DisplaySnapshot,
787        selection: &mut Selection<DisplayPoint>,
788        around: bool,
789        times: Option<usize>,
790    ) -> bool {
791        if let Some(range) = self.range(map, selection.clone(), around, times) {
792            selection.start = range.start;
793            selection.end = range.end;
794            true
795        } else {
796            false
797        }
798    }
799}
800
801/// Returns a range that surrounds the word `relative_to` is in.
802///
803/// If `relative_to` is at the start of a word, return the word.
804/// If `relative_to` is between words, return the space between.
805/// If `times` > 1, extend to include additional words.
806fn in_word(
807    map: &DisplaySnapshot,
808    relative_to: DisplayPoint,
809    ignore_punctuation: bool,
810    times: usize,
811) -> Option<Range<DisplayPoint>> {
812    // Use motion::right so that we consider the character under the cursor when looking for the start
813    let classifier = map
814        .buffer_snapshot()
815        .char_classifier_at(relative_to.to_point(map))
816        .ignore_punctuation(ignore_punctuation);
817    let start = movement::find_preceding_boundary_display_point(
818        map,
819        right(map, relative_to, 1),
820        movement::FindRange::SingleLine,
821        &mut |left, right| classifier.kind(left) != classifier.kind(right),
822    );
823
824    let mut end = movement::find_boundary(
825        map,
826        relative_to,
827        FindRange::SingleLine,
828        &mut |left, right| classifier.kind(left) != classifier.kind(right),
829    );
830
831    let mut is_boundary = |left: char, right: char| classifier.kind(left) != classifier.kind(right);
832
833    for _ in 1..times {
834        let kind_at_end = map
835            .buffer_chars_at(end.to_offset(map, Bias::Right))
836            .next()
837            .map(|(c, _)| classifier.kind(c));
838
839        // Skip whitespace but not punctuation (punctuation is its own word unit).
840        let next_end = if kind_at_end == Some(CharKind::Whitespace) {
841            let after_whitespace =
842                movement::find_boundary(map, end, FindRange::MultiLine, &mut is_boundary);
843            movement::find_boundary(
844                map,
845                after_whitespace,
846                FindRange::MultiLine,
847                &mut is_boundary,
848            )
849        } else {
850            movement::find_boundary(map, end, FindRange::MultiLine, &mut is_boundary)
851        };
852        if next_end == end {
853            break;
854        }
855        end = next_end;
856    }
857
858    Some(start..end)
859}
860
861fn in_subword(
862    map: &DisplaySnapshot,
863    relative_to: DisplayPoint,
864    ignore_punctuation: bool,
865) -> Option<Range<DisplayPoint>> {
866    let offset = relative_to.to_offset(map, Bias::Left);
867    // Use motion::right so that we consider the character under the cursor when looking for the start
868    let classifier = map
869        .buffer_snapshot()
870        .char_classifier_at(relative_to.to_point(map))
871        .ignore_punctuation(ignore_punctuation);
872    let in_subword = map
873        .buffer_chars_at(offset)
874        .next()
875        .map(|(c, _)| {
876            let is_separator = "._-".contains(c);
877            !classifier.is_whitespace(c) && !is_separator
878        })
879        .unwrap_or(false);
880
881    let start = if in_subword {
882        movement::find_preceding_boundary_display_point(
883            map,
884            right(map, relative_to, 1),
885            movement::FindRange::SingleLine,
886            &mut |left, right| {
887                let is_word_start = classifier.kind(left) != classifier.kind(right);
888                is_word_start || is_subword_start(left, right, "._-")
889            },
890        )
891    } else {
892        movement::find_boundary(
893            map,
894            relative_to,
895            FindRange::SingleLine,
896            &mut |left, right| {
897                let is_word_start = classifier.kind(left) != classifier.kind(right);
898                is_word_start || is_subword_start(left, right, "._-")
899            },
900        )
901    };
902
903    let end = movement::find_boundary(
904        map,
905        relative_to,
906        FindRange::SingleLine,
907        &mut |left, right| {
908            let is_word_end = classifier.kind(left) != classifier.kind(right);
909            is_word_end || is_subword_end(left, right, "._-")
910        },
911    );
912
913    Some(start..end)
914}
915
916pub fn surrounding_html_tag(
917    map: &DisplaySnapshot,
918    head: DisplayPoint,
919    range: Range<DisplayPoint>,
920    around: bool,
921) -> Option<Range<DisplayPoint>> {
922    fn read_tag(chars: impl Iterator<Item = char>) -> String {
923        chars
924            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
925            .collect()
926    }
927    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
928        if Some('<') != chars.next() {
929            return None;
930        }
931        Some(read_tag(chars))
932    }
933    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
934        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
935            return None;
936        }
937        Some(read_tag(chars))
938    }
939
940    let snapshot = &map.buffer_snapshot();
941    let head_offset = head.to_offset(map, Bias::Left);
942    let range_start = range.start.to_offset(map, Bias::Left);
943    let range_end = range.end.to_offset(map, Bias::Left);
944    let head_is_start = head_offset <= range_start;
945
946    let results = snapshot.map_excerpt_ranges(
947        range_start..range_end,
948        |buffer, _excerpt_range, input_buffer_range| {
949            let buffer_offset = if head_is_start {
950                input_buffer_range.start
951            } else {
952                input_buffer_range.end
953            };
954
955            let Some(layer) = buffer.syntax_layer_at(buffer_offset) else {
956                return Vec::new();
957            };
958            let mut cursor = layer.node().walk();
959            let mut last_child_node = cursor.node();
960            while cursor.goto_first_child_for_byte(buffer_offset.0).is_some() {
961                last_child_node = cursor.node();
962            }
963
964            let mut last_child_node = Some(last_child_node);
965            while let Some(cur_node) = last_child_node {
966                if cur_node.child_count() >= 2 {
967                    let first_child = cur_node.child(0);
968                    let last_child = cur_node.child(cur_node.child_count() as u32 - 1);
969                    if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
970                        let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
971                        let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
972                        let is_valid = if range_end.saturating_sub(range_start) <= 1 {
973                            buffer_offset.0 <= last_child.end_byte()
974                        } else {
975                            input_buffer_range.start.0 >= first_child.start_byte()
976                                && input_buffer_range.end.0 <= last_child.start_byte() + 1
977                        };
978                        if open_tag.is_some() && open_tag == close_tag && is_valid {
979                            let buffer_range = if around {
980                                first_child.byte_range().start..last_child.byte_range().end
981                            } else {
982                                first_child.byte_range().end..last_child.byte_range().start
983                            };
984                            return vec![(
985                                BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end),
986                                (),
987                            )];
988                        }
989                    }
990                }
991                last_child_node = cur_node.parent();
992            }
993            Vec::new()
994        },
995    )?;
996
997    let (result, ()) = results.into_iter().next()?;
998    Some(result.start.to_display_point(map)..result.end.to_display_point(map))
999}
1000
1001/// Returns a range that surrounds the word and following whitespace
1002/// relative_to is in.
1003///
1004/// If `relative_to` is at the start of a word, return the word and following whitespace.
1005/// If `relative_to` is between words, return the whitespace back and the following word.
1006///
1007/// if in word
1008///   delete that word
1009///   if there is whitespace following the word, delete that as well
1010///   otherwise, delete any preceding whitespace
1011/// otherwise
1012///   delete whitespace around cursor
1013///   delete word following the cursor
1014/// If `times` > 1, extend to include additional words.
1015fn around_word(
1016    map: &DisplaySnapshot,
1017    relative_to: DisplayPoint,
1018    ignore_punctuation: bool,
1019    times: usize,
1020) -> Option<Range<DisplayPoint>> {
1021    let offset = relative_to.to_offset(map, Bias::Left);
1022    let classifier = map
1023        .buffer_snapshot()
1024        .char_classifier_at(offset)
1025        .ignore_punctuation(ignore_punctuation);
1026    let in_word = map
1027        .buffer_chars_at(offset)
1028        .next()
1029        .map(|(c, _)| !classifier.is_whitespace(c))
1030        .unwrap_or(false);
1031
1032    if in_word {
1033        around_containing_word(map, relative_to, ignore_punctuation, times)
1034    } else {
1035        around_next_word(map, relative_to, ignore_punctuation, times)
1036    }
1037}
1038
1039fn around_subword(
1040    map: &DisplaySnapshot,
1041    relative_to: DisplayPoint,
1042    ignore_punctuation: bool,
1043) -> Option<Range<DisplayPoint>> {
1044    // Use motion::right so that we consider the character under the cursor when looking for the start
1045    let classifier = map
1046        .buffer_snapshot()
1047        .char_classifier_at(relative_to.to_point(map))
1048        .ignore_punctuation(ignore_punctuation);
1049    let start = movement::find_preceding_boundary_display_point(
1050        map,
1051        right(map, relative_to, 1),
1052        movement::FindRange::SingleLine,
1053        &mut |left, right| {
1054            let is_separator = |c: char| "._-".contains(c);
1055            let is_word_start =
1056                classifier.kind(left) != classifier.kind(right) && !is_separator(left);
1057            is_word_start || is_subword_start(left, right, "._-")
1058        },
1059    );
1060
1061    let end = movement::find_boundary(
1062        map,
1063        relative_to,
1064        FindRange::SingleLine,
1065        &mut |left, right| {
1066            let is_separator = |c: char| "._-".contains(c);
1067            let is_word_end =
1068                classifier.kind(left) != classifier.kind(right) && !is_separator(right);
1069            is_word_end || is_subword_end(left, right, "._-")
1070        },
1071    );
1072
1073    Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
1074}
1075
1076fn around_containing_word(
1077    map: &DisplaySnapshot,
1078    relative_to: DisplayPoint,
1079    ignore_punctuation: bool,
1080    times: usize,
1081) -> Option<Range<DisplayPoint>> {
1082    in_word(map, relative_to, ignore_punctuation, times).map(|range| {
1083        let spans_multiple_lines = range.start.row() != range.end.row();
1084        let stop_at_newline = !spans_multiple_lines;
1085
1086        let line_start = DisplayPoint::new(range.start.row(), 0);
1087        let is_first_word = map
1088            .buffer_chars_at(line_start.to_offset(map, Bias::Left))
1089            .take_while(|(ch, offset)| {
1090                offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace()
1091            })
1092            .count()
1093            > 0;
1094
1095        if is_first_word {
1096            // For first word on line, trim indentation
1097            let mut expanded = expand_to_include_whitespace(map, range.clone(), stop_at_newline);
1098            expanded.start = range.start;
1099            expanded
1100        } else {
1101            expand_to_include_whitespace(map, range, stop_at_newline)
1102        }
1103    })
1104}
1105
1106fn around_next_word(
1107    map: &DisplaySnapshot,
1108    relative_to: DisplayPoint,
1109    ignore_punctuation: bool,
1110    times: usize,
1111) -> Option<Range<DisplayPoint>> {
1112    let classifier = map
1113        .buffer_snapshot()
1114        .char_classifier_at(relative_to.to_point(map))
1115        .ignore_punctuation(ignore_punctuation);
1116    let start = movement::find_preceding_boundary_display_point(
1117        map,
1118        right(map, relative_to, 1),
1119        FindRange::SingleLine,
1120        &mut |left, right| classifier.kind(left) != classifier.kind(right),
1121    );
1122
1123    let mut word_found = false;
1124    let mut end = movement::find_boundary(
1125        map,
1126        relative_to,
1127        FindRange::MultiLine,
1128        &mut |left, right| {
1129            let left_kind = classifier.kind(left);
1130            let right_kind = classifier.kind(right);
1131
1132            let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
1133
1134            if right_kind != CharKind::Whitespace {
1135                word_found = true;
1136            }
1137
1138            found
1139        },
1140    );
1141
1142    for _ in 1..times {
1143        let next_end =
1144            movement::find_boundary(map, end, FindRange::MultiLine, &mut |left, right| {
1145                let left_kind = classifier.kind(left);
1146                let right_kind = classifier.kind(right);
1147
1148                let in_word_unit = left_kind != CharKind::Whitespace;
1149                (in_word_unit && left_kind != right_kind) || right == '\n' && left == '\n'
1150            });
1151        if next_end == end {
1152            break;
1153        }
1154        end = next_end;
1155    }
1156
1157    Some(start..end)
1158}
1159
1160fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
1161    Some(DisplayPoint::zero()..map.max_point())
1162}
1163
1164fn text_object(
1165    map: &DisplaySnapshot,
1166    relative_to: DisplayPoint,
1167    target: TextObject,
1168) -> Option<Range<DisplayPoint>> {
1169    let snapshot = &map.buffer_snapshot();
1170    let offset = relative_to.to_offset(map, Bias::Left);
1171
1172    let results =
1173        snapshot.map_excerpt_ranges(offset..offset, |buffer, _excerpt_range, buffer_range| {
1174            let buffer_offset = buffer_range.start;
1175
1176            let mut matches: Vec<Range<usize>> = buffer
1177                .text_object_ranges(buffer_offset..buffer_offset, TreeSitterOptions::default())
1178                .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1179                .collect();
1180            matches.sort_by_key(|r| r.end - r.start);
1181            if let Some(buffer_range) = matches.first() {
1182                return vec![(
1183                    BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end),
1184                    (),
1185                )];
1186            }
1187
1188            let Some(around) = target.around() else {
1189                return vec![];
1190            };
1191            let mut matches: Vec<Range<usize>> = buffer
1192                .text_object_ranges(buffer_offset..buffer_offset, TreeSitterOptions::default())
1193                .filter_map(|(r, m)| if m == around { Some(r) } else { None })
1194                .collect();
1195            matches.sort_by_key(|r| r.end - r.start);
1196            let Some(around_range) = matches.first() else {
1197                return vec![];
1198            };
1199
1200            let mut matches: Vec<Range<usize>> = buffer
1201                .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
1202                .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1203                .collect();
1204            matches.sort_by_key(|r| r.start);
1205            if let Some(buffer_range) = matches.first()
1206                && !buffer_range.is_empty()
1207            {
1208                return vec![(
1209                    BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end),
1210                    (),
1211                )];
1212            }
1213            vec![(
1214                BufferOffset(around_range.start)..BufferOffset(around_range.end),
1215                (),
1216            )]
1217        })?;
1218
1219    let (range, ()) = results.into_iter().next()?;
1220    Some(range.start.to_display_point(map)..range.end.to_display_point(map))
1221}
1222
1223fn argument(
1224    map: &DisplaySnapshot,
1225    relative_to: DisplayPoint,
1226    around: bool,
1227) -> Option<Range<DisplayPoint>> {
1228    let snapshot = &map.buffer_snapshot();
1229    let offset = relative_to.to_offset(map, Bias::Left);
1230
1231    fn comma_delimited_range_at(
1232        buffer: &BufferSnapshot,
1233        mut offset: BufferOffset,
1234        include_comma: bool,
1235    ) -> Option<Range<BufferOffset>> {
1236        offset += buffer
1237            .chars_at(offset)
1238            .take_while(|c| c.is_whitespace())
1239            .map(char::len_utf8)
1240            .sum::<usize>();
1241
1242        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
1243            if open.end == close.start {
1244                return false;
1245            }
1246
1247            if open.start == offset.0 || close.end == offset.0 {
1248                return false;
1249            }
1250
1251            matches!(
1252                buffer.chars_at(open.start).next(),
1253                Some('(' | '[' | '{' | '<' | '|')
1254            )
1255        };
1256
1257        let (open_bracket, close_bracket) =
1258            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
1259
1260        let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start);
1261
1262        let layer = buffer.syntax_layer_at(offset)?;
1263        let node = layer.node();
1264        let mut cursor = node.walk();
1265
1266        let mut parent_covers_bracket_range = false;
1267        loop {
1268            let node = cursor.node();
1269            let range = node.byte_range();
1270            let covers_bracket_range =
1271                range.start == open_bracket.start && range.end == close_bracket.end;
1272            if parent_covers_bracket_range && !covers_bracket_range {
1273                break;
1274            }
1275            parent_covers_bracket_range = covers_bracket_range;
1276
1277            cursor.goto_first_child_for_byte(offset.0)?;
1278        }
1279
1280        let mut argument_node = cursor.node();
1281
1282        if argument_node.byte_range() == open_bracket {
1283            if !cursor.goto_next_sibling() {
1284                return Some(inner_bracket_range);
1285            }
1286            argument_node = cursor.node();
1287        }
1288        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
1289            if !cursor.goto_previous_sibling() {
1290                return Some(inner_bracket_range);
1291            }
1292            argument_node = cursor.node();
1293            if argument_node.byte_range() == open_bracket {
1294                return Some(inner_bracket_range);
1295            }
1296        }
1297
1298        let mut start = argument_node.start_byte();
1299        let mut end = argument_node.end_byte();
1300
1301        let mut needs_surrounding_comma = include_comma;
1302
1303        while cursor.goto_previous_sibling() {
1304            let prev = cursor.node();
1305
1306            if prev.start_byte() < open_bracket.end {
1307                start = open_bracket.end;
1308                break;
1309            } else if prev.kind() == "," {
1310                if needs_surrounding_comma {
1311                    start = prev.start_byte();
1312                    needs_surrounding_comma = false;
1313                }
1314                break;
1315            } else if prev.start_byte() < start {
1316                start = prev.start_byte();
1317            }
1318        }
1319
1320        while cursor.goto_next_sibling() {
1321            let next = cursor.node();
1322
1323            if next.end_byte() > close_bracket.start {
1324                end = close_bracket.start;
1325                break;
1326            } else if next.kind() == "," {
1327                if needs_surrounding_comma {
1328                    if let Some(next_arg) = next.next_sibling() {
1329                        end = next_arg.start_byte();
1330                    } else {
1331                        end = next.end_byte();
1332                    }
1333                }
1334                break;
1335            } else if next.end_byte() > end {
1336                end = next.end_byte();
1337            }
1338        }
1339
1340        Some(BufferOffset(start)..BufferOffset(end))
1341    }
1342
1343    let results =
1344        snapshot.map_excerpt_ranges(offset..offset, |buffer, _excerpt_range, buffer_range| {
1345            let buffer_offset = buffer_range.start;
1346            match comma_delimited_range_at(buffer, buffer_offset, around) {
1347                Some(result) => vec![(result, ())],
1348                None => vec![],
1349            }
1350        })?;
1351
1352    let (range, ()) = results.into_iter().next()?;
1353    Some(range.start.to_display_point(map)..range.end.to_display_point(map))
1354}
1355
1356fn indent(
1357    map: &DisplaySnapshot,
1358    relative_to: DisplayPoint,
1359    around: bool,
1360    include_below: bool,
1361) -> Option<Range<DisplayPoint>> {
1362    let point = relative_to.to_point(map);
1363    let row = point.row;
1364
1365    let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
1366
1367    // Loop backwards until we find a non-blank line with less indent
1368    let mut start_row = row;
1369    for prev_row in (0..row).rev() {
1370        let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
1371        if indent.is_line_empty() {
1372            continue;
1373        }
1374        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1375            if around {
1376                // When around is true, include the first line with less indent
1377                start_row = prev_row;
1378            }
1379            break;
1380        }
1381        start_row = prev_row;
1382    }
1383
1384    // Loop forwards until we find a non-blank line with less indent
1385    let mut end_row = row;
1386    let max_rows = map.buffer_snapshot().max_row().0;
1387    for next_row in (row + 1)..=max_rows {
1388        let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
1389        if indent.is_line_empty() {
1390            continue;
1391        }
1392        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1393            if around && include_below {
1394                // When around is true and including below, include this line
1395                end_row = next_row;
1396            }
1397            break;
1398        }
1399        end_row = next_row;
1400    }
1401
1402    let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1403    let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
1404    let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
1405    Some(start..end)
1406}
1407
1408fn sentence(
1409    map: &DisplaySnapshot,
1410    relative_to: DisplayPoint,
1411    around: bool,
1412) -> Option<Range<DisplayPoint>> {
1413    let mut start = None;
1414    let relative_offset = relative_to.to_offset(map, Bias::Left);
1415    let mut previous_end = relative_offset;
1416
1417    let mut chars = map.buffer_chars_at(previous_end).peekable();
1418
1419    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
1420    for (char, offset) in chars
1421        .peek()
1422        .cloned()
1423        .into_iter()
1424        .chain(map.reverse_buffer_chars_at(previous_end))
1425    {
1426        if is_sentence_end(map, offset) {
1427            break;
1428        }
1429
1430        if is_possible_sentence_start(char) {
1431            start = Some(offset);
1432        }
1433
1434        previous_end = offset;
1435    }
1436
1437    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1438    let mut end = relative_offset;
1439    for (char, offset) in chars {
1440        if start.is_none() && is_possible_sentence_start(char) {
1441            if around {
1442                start = Some(offset);
1443                continue;
1444            } else {
1445                end = offset;
1446                break;
1447            }
1448        }
1449
1450        if char != '\n' {
1451            end = offset + char.len_utf8();
1452        }
1453
1454        if is_sentence_end(map, end) {
1455            break;
1456        }
1457    }
1458
1459    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1460    if around {
1461        range = expand_to_include_whitespace(map, range, false);
1462    }
1463
1464    Some(range)
1465}
1466
1467fn is_possible_sentence_start(character: char) -> bool {
1468    !character.is_whitespace() && character != '.'
1469}
1470
1471const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1472const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1473const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1474fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool {
1475    let mut next_chars = map.buffer_chars_at(offset).peekable();
1476    if let Some((char, _)) = next_chars.next() {
1477        // We are at a double newline. This position is a sentence end.
1478        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1479            return true;
1480        }
1481
1482        // The next text is not a valid whitespace. This is not a sentence end
1483        if !SENTENCE_END_WHITESPACE.contains(&char) {
1484            return false;
1485        }
1486    }
1487
1488    for (char, _) in map.reverse_buffer_chars_at(offset) {
1489        if SENTENCE_END_PUNCTUATION.contains(&char) {
1490            return true;
1491        }
1492
1493        if !SENTENCE_END_FILLERS.contains(&char) {
1494            return false;
1495        }
1496    }
1497
1498    false
1499}
1500
1501/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1502/// whitespace to the end first and falls back to the start if there was none.
1503pub fn expand_to_include_whitespace(
1504    map: &DisplaySnapshot,
1505    range: Range<DisplayPoint>,
1506    stop_at_newline: bool,
1507) -> Range<DisplayPoint> {
1508    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1509    let mut whitespace_included = false;
1510
1511    let chars = map.buffer_chars_at(range.end).peekable();
1512    for (char, offset) in chars {
1513        if char == '\n' && stop_at_newline {
1514            break;
1515        }
1516
1517        if char.is_whitespace() {
1518            if char != '\n' || !stop_at_newline {
1519                range.end = offset + char.len_utf8();
1520                whitespace_included = true;
1521            }
1522        } else {
1523            // Found non whitespace. Quit out.
1524            break;
1525        }
1526    }
1527
1528    if !whitespace_included {
1529        for (char, point) in map.reverse_buffer_chars_at(range.start) {
1530            if char == '\n' && stop_at_newline {
1531                break;
1532            }
1533
1534            if !char.is_whitespace() {
1535                break;
1536            }
1537
1538            range.start = point;
1539        }
1540    }
1541
1542    range.start.to_display_point(map)..range.end.to_display_point(map)
1543}
1544
1545/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1546/// where `relative_to` is in. If `around`, principally returns the range ending
1547/// at the end of the next paragraph.
1548///
1549/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1550/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1551/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1552/// the trailing newline is not subject to subsequent operations).
1553///
1554/// Edge cases:
1555/// - If `around` and if the current paragraph is the last paragraph of the
1556///   file and is blank, then the selection results in an error.
1557/// - If `around` and if the current paragraph is the last paragraph of the
1558///   file and is not blank, then the returned range starts at the start of the
1559///   previous paragraph, if it exists.
1560fn paragraph(
1561    map: &DisplaySnapshot,
1562    relative_to: DisplayPoint,
1563    around: bool,
1564    times: usize,
1565) -> Option<Range<DisplayPoint>> {
1566    let mut paragraph_start = start_of_paragraph(map, relative_to);
1567    let mut paragraph_end = end_of_paragraph(map, relative_to);
1568
1569    for i in 0..times {
1570        let paragraph_end_row = paragraph_end.row();
1571        let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1572        let point = relative_to.to_point(map);
1573        let current_line_is_empty = map
1574            .buffer_snapshot()
1575            .is_line_blank(MultiBufferRow(point.row));
1576
1577        if around {
1578            if paragraph_ends_with_eof {
1579                if current_line_is_empty {
1580                    return None;
1581                }
1582
1583                let paragraph_start_buffer_point = paragraph_start.to_point(map);
1584                if paragraph_start_buffer_point.row != 0 {
1585                    let previous_paragraph_last_line_start =
1586                        Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
1587                    paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1588                }
1589            } else {
1590                let paragraph_end_buffer_point = paragraph_end.to_point(map);
1591                let mut start_row = paragraph_end_buffer_point.row + 1;
1592                if i > 0 {
1593                    start_row += 1;
1594                }
1595                let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1596                paragraph_end = end_of_paragraph(map, next_paragraph_start);
1597            }
1598        }
1599    }
1600
1601    let range = paragraph_start..paragraph_end;
1602    Some(range)
1603}
1604
1605/// Returns a position of the start of the current paragraph, where a paragraph
1606/// is defined as a run of non-blank lines or a run of blank lines.
1607pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1608    let point = display_point.to_point(map);
1609    if point.row == 0 {
1610        return DisplayPoint::zero();
1611    }
1612
1613    let is_current_line_blank = map
1614        .buffer_snapshot()
1615        .is_line_blank(MultiBufferRow(point.row));
1616
1617    for row in (0..point.row).rev() {
1618        let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1619        if blank != is_current_line_blank {
1620            return Point::new(row + 1, 0).to_display_point(map);
1621        }
1622    }
1623
1624    DisplayPoint::zero()
1625}
1626
1627/// Returns a position of the end of the current paragraph, where a paragraph
1628/// is defined as a run of non-blank lines or a run of blank lines.
1629/// The trailing newline is excluded from the paragraph.
1630pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1631    let point = display_point.to_point(map);
1632    if point.row == map.buffer_snapshot().max_row().0 {
1633        return map.max_point();
1634    }
1635
1636    let is_current_line_blank = map
1637        .buffer_snapshot()
1638        .is_line_blank(MultiBufferRow(point.row));
1639
1640    for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 {
1641        let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1642        if blank != is_current_line_blank {
1643            let previous_row = row - 1;
1644            return Point::new(
1645                previous_row,
1646                map.buffer_snapshot().line_len(MultiBufferRow(previous_row)),
1647            )
1648            .to_display_point(map);
1649        }
1650    }
1651
1652    map.max_point()
1653}
1654
1655pub fn surrounding_markers(
1656    map: &DisplaySnapshot,
1657    relative_to: DisplayPoint,
1658    around: bool,
1659    search_across_lines: bool,
1660    open_marker: char,
1661    close_marker: char,
1662) -> Option<Range<DisplayPoint>> {
1663    let point = relative_to.to_offset(map, Bias::Left);
1664
1665    let mut matched_closes = 0;
1666    let mut opening = None;
1667
1668    let mut before_ch = match movement::chars_before(map, point).next() {
1669        Some((ch, _)) => ch,
1670        _ => '\0',
1671    };
1672    if let Some((ch, range)) = movement::chars_after(map, point).next()
1673        && ch == open_marker
1674        && before_ch != '\\'
1675    {
1676        if open_marker == close_marker {
1677            let mut total = 0;
1678            for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
1679                if ch == '\n' {
1680                    break;
1681                }
1682                if ch == open_marker && before_ch != '\\' {
1683                    total += 1;
1684                }
1685            }
1686            if total % 2 == 0 {
1687                opening = Some(range)
1688            }
1689        } else {
1690            opening = Some(range)
1691        }
1692    }
1693
1694    if opening.is_none() {
1695        let mut chars_before = movement::chars_before(map, point).peekable();
1696        while let Some((ch, range)) = chars_before.next() {
1697            if ch == '\n' && !search_across_lines {
1698                break;
1699            }
1700
1701            if let Some((before_ch, _)) = chars_before.peek()
1702                && *before_ch == '\\'
1703            {
1704                continue;
1705            }
1706
1707            if ch == open_marker {
1708                if matched_closes == 0 {
1709                    opening = Some(range);
1710                    break;
1711                }
1712                matched_closes -= 1;
1713            } else if ch == close_marker {
1714                matched_closes += 1
1715            }
1716        }
1717    }
1718    if opening.is_none() {
1719        for (ch, range) in movement::chars_after(map, point) {
1720            if before_ch != '\\' {
1721                if ch == open_marker {
1722                    opening = Some(range);
1723                    break;
1724                } else if ch == close_marker {
1725                    break;
1726                }
1727            }
1728
1729            before_ch = ch;
1730        }
1731    }
1732
1733    let mut opening = opening?;
1734
1735    let mut matched_opens = 0;
1736    let mut closing = None;
1737    before_ch = match movement::chars_before(map, opening.end).next() {
1738        Some((ch, _)) => ch,
1739        _ => '\0',
1740    };
1741    for (ch, range) in movement::chars_after(map, opening.end) {
1742        if ch == '\n' && !search_across_lines {
1743            break;
1744        }
1745
1746        if before_ch != '\\' {
1747            if ch == close_marker {
1748                if matched_opens == 0 {
1749                    closing = Some(range);
1750                    break;
1751                }
1752                matched_opens -= 1;
1753            } else if ch == open_marker {
1754                matched_opens += 1;
1755            }
1756        }
1757
1758        before_ch = ch;
1759    }
1760
1761    let mut closing = closing?;
1762
1763    if around && !search_across_lines {
1764        let mut found = false;
1765
1766        for (ch, range) in movement::chars_after(map, closing.end) {
1767            if ch.is_whitespace() && ch != '\n' {
1768                found = true;
1769                closing.end = range.end;
1770            } else {
1771                break;
1772            }
1773        }
1774
1775        if !found {
1776            for (ch, range) in movement::chars_before(map, opening.start) {
1777                if ch.is_whitespace() && ch != '\n' {
1778                    opening.start = range.start
1779                } else {
1780                    break;
1781                }
1782            }
1783        }
1784    }
1785
1786    // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1787    if !around && open_marker != close_marker {
1788        let start_point = opening.end.to_display_point(map);
1789        let end_point = closing.start.to_display_point(map);
1790        let start_offset = start_point.to_offset(map, Bias::Left);
1791        let end_offset = end_point.to_offset(map, Bias::Left);
1792
1793        if start_point.row() != end_point.row()
1794            && map
1795                .buffer_chars_at(start_offset)
1796                .take_while(|(_, offset)| offset < &end_offset)
1797                .any(|(ch, _)| !ch.is_whitespace())
1798        {
1799            let mut first_non_ws = None;
1800            let mut last_non_ws = None;
1801            for (ch, offset) in map.buffer_chars_at(start_offset) {
1802                if !ch.is_whitespace() {
1803                    first_non_ws = Some(offset);
1804                    break;
1805                }
1806            }
1807            for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1808                if !ch.is_whitespace() {
1809                    last_non_ws = Some(offset + ch.len_utf8());
1810                    break;
1811                }
1812            }
1813            if let Some(start) = first_non_ws {
1814                opening.end = start;
1815            }
1816            if let Some(end) = last_non_ws {
1817                closing.start = end;
1818            }
1819        }
1820    }
1821
1822    let result = if around {
1823        opening.start..closing.end
1824    } else {
1825        opening.end..closing.start
1826    };
1827
1828    Some(
1829        map.clip_point(result.start.to_display_point(map), Bias::Left)
1830            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1831    )
1832}
1833
1834#[cfg(test)]
1835mod test {
1836    use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
1837    use gpui::KeyBinding;
1838    use indoc::indoc;
1839    use text::Point;
1840
1841    use crate::{
1842        object::{AnyBrackets, AnyQuotes, MiniBrackets},
1843        state::Mode,
1844        test::{NeovimBackedTestContext, VimTestContext},
1845    };
1846
1847    const WORD_LOCATIONS: &str = indoc! {"
1848        The quick ˇbrowˇnˇ•••
1849        fox ˇjuˇmpsˇ over
1850        the lazy dogˇ••
1851        ˇ
1852        ˇ
1853        ˇ
1854        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1855        ˇ••
1856        ˇ••
1857        ˇ  fox-jumpˇs over
1858        the lazy dogˇ•
1859        ˇ
1860        "
1861    };
1862
1863    #[gpui::test]
1864    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1865        let mut cx = NeovimBackedTestContext::new(cx).await;
1866
1867        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1868            .await
1869            .assert_matches();
1870        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1871            .await
1872            .assert_matches();
1873        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1874            .await
1875            .assert_matches();
1876        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1877            .await
1878            .assert_matches();
1879    }
1880
1881    #[gpui::test]
1882    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1883        let mut cx = NeovimBackedTestContext::new(cx).await;
1884
1885        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1886            .await
1887            .assert_matches();
1888        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1889            .await
1890            .assert_matches();
1891        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1892            .await
1893            .assert_matches();
1894        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1895            .await
1896            .assert_matches();
1897    }
1898
1899    #[gpui::test]
1900    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1901        let mut cx = NeovimBackedTestContext::new(cx).await;
1902
1903        /*
1904                cx.set_shared_state("The quick ˇbrown\nfox").await;
1905                cx.simulate_shared_keystrokes(["v"]).await;
1906                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1907                cx.simulate_shared_keystrokes(["i", "w"]).await;
1908                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1909        */
1910        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1911        cx.simulate_shared_keystrokes("v").await;
1912        cx.shared_state()
1913            .await
1914            .assert_eq("The quick brown\n«\nˇ»fox");
1915        cx.simulate_shared_keystrokes("i w").await;
1916        cx.shared_state()
1917            .await
1918            .assert_eq("The quick brown\n«\nˇ»fox");
1919
1920        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1921            .await
1922            .assert_matches();
1923        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1924            .await
1925            .assert_matches();
1926    }
1927
1928    #[gpui::test]
1929    async fn test_word_object_with_count(cx: &mut gpui::TestAppContext) {
1930        let mut cx = NeovimBackedTestContext::new(cx).await;
1931
1932        cx.set_shared_state("ˇone two three four").await;
1933        cx.simulate_shared_keystrokes("2 d a w").await;
1934        cx.shared_state().await.assert_matches();
1935
1936        cx.set_shared_state("ˇone two three four").await;
1937        cx.simulate_shared_keystrokes("d 2 a w").await;
1938        cx.shared_state().await.assert_matches();
1939
1940        // WORD (shift-w) ignores punctuation
1941        cx.set_shared_state("ˇone-two three-four five").await;
1942        cx.simulate_shared_keystrokes("2 d a shift-w").await;
1943        cx.shared_state().await.assert_matches();
1944
1945        cx.set_shared_state("ˇone two three four five").await;
1946        cx.simulate_shared_keystrokes("3 d a w").await;
1947        cx.shared_state().await.assert_matches();
1948
1949        // Multiplied counts: 2d2aw deletes 4 words (2*2)
1950        cx.set_shared_state("ˇone two three four five six").await;
1951        cx.simulate_shared_keystrokes("2 d 2 a w").await;
1952        cx.shared_state().await.assert_matches();
1953
1954        cx.set_shared_state("ˇone two three four").await;
1955        cx.simulate_shared_keystrokes("2 c a w").await;
1956        cx.shared_state().await.assert_matches();
1957
1958        cx.set_shared_state("ˇone two three four").await;
1959        cx.simulate_shared_keystrokes("2 y a w p").await;
1960        cx.shared_state().await.assert_matches();
1961
1962        // Punctuation: foo-bar is 3 word units (foo, -, bar), so 2aw selects "foo-"
1963        cx.set_shared_state("  ˇfoo-bar baz").await;
1964        cx.simulate_shared_keystrokes("2 d a w").await;
1965        cx.shared_state().await.assert_matches();
1966
1967        // Trailing whitespace counts as a word unit for iw
1968        cx.set_shared_state("ˇfoo   ").await;
1969        cx.simulate_shared_keystrokes("2 d i w").await;
1970        cx.shared_state().await.assert_matches();
1971
1972        // Multi-line: count > 1 crosses line boundaries
1973        cx.set_shared_state("ˇone\ntwo\nthree").await;
1974        cx.simulate_shared_keystrokes("2 d a w").await;
1975        cx.shared_state().await.assert_matches();
1976
1977        cx.set_shared_state("ˇone\ntwo\nthree\nfour").await;
1978        cx.simulate_shared_keystrokes("3 d a w").await;
1979        cx.shared_state().await.assert_matches();
1980
1981        cx.set_shared_state("ˇone\ntwo\nthree").await;
1982        cx.simulate_shared_keystrokes("2 d i w").await;
1983        cx.shared_state().await.assert_matches();
1984
1985        cx.set_shared_state("one ˇtwo\nthree four").await;
1986        cx.simulate_shared_keystrokes("2 d a w").await;
1987        cx.shared_state().await.assert_matches();
1988    }
1989
1990    const PARAGRAPH_EXAMPLES: &[&str] = &[
1991        // Single line
1992        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1993        // Multiple lines without empty lines
1994        indoc! {"
1995            ˇThe quick brownˇ
1996            ˇfox jumps overˇ
1997            the lazy dog.ˇ
1998        "},
1999        // Heading blank paragraph and trailing normal paragraph
2000        indoc! {"
2001            ˇ
2002            ˇ
2003            ˇThe quick brown fox jumps
2004            ˇover the lazy dog.
2005            ˇ
2006            ˇ
2007            ˇThe quick brown fox jumpsˇ
2008            ˇover the lazy dog.ˇ
2009        "},
2010        // Inserted blank paragraph and trailing blank paragraph
2011        indoc! {"
2012            ˇThe quick brown fox jumps
2013            ˇover the lazy dog.
2014            ˇ
2015            ˇ
2016            ˇ
2017            ˇThe quick brown fox jumpsˇ
2018            ˇover the lazy dog.ˇ
2019            ˇ
2020            ˇ
2021            ˇ
2022        "},
2023        // "Blank" paragraph with whitespace characters
2024        indoc! {"
2025            ˇThe quick brown fox jumps
2026            over the lazy dog.
2027
2028            ˇ \t
2029
2030            ˇThe quick brown fox jumps
2031            over the lazy dog.ˇ
2032            ˇ
2033            ˇ \t
2034            \t \t
2035        "},
2036        // Single line "paragraphs", where selection size might be zero.
2037        indoc! {"
2038            ˇThe quick brown fox jumps over the lazy dog.
2039            ˇ
2040            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
2041            ˇ
2042        "},
2043    ];
2044
2045    #[gpui::test]
2046    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
2047        let mut cx = NeovimBackedTestContext::new(cx).await;
2048
2049        for paragraph_example in PARAGRAPH_EXAMPLES {
2050            cx.simulate_at_each_offset("c i p", paragraph_example)
2051                .await
2052                .assert_matches();
2053            cx.simulate_at_each_offset("c a p", paragraph_example)
2054                .await
2055                .assert_matches();
2056        }
2057    }
2058
2059    #[gpui::test]
2060    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
2061        let mut cx = NeovimBackedTestContext::new(cx).await;
2062
2063        for paragraph_example in PARAGRAPH_EXAMPLES {
2064            cx.simulate_at_each_offset("d i p", paragraph_example)
2065                .await
2066                .assert_matches();
2067            cx.simulate_at_each_offset("d a p", paragraph_example)
2068                .await
2069                .assert_matches();
2070        }
2071    }
2072
2073    #[gpui::test]
2074    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
2075        let mut cx = NeovimBackedTestContext::new(cx).await;
2076
2077        const EXAMPLES: &[&str] = &[
2078            indoc! {"
2079                ˇThe quick brown
2080                fox jumps over
2081                the lazy dog.
2082            "},
2083            indoc! {"
2084                ˇ
2085
2086                ˇThe quick brown fox jumps
2087                over the lazy dog.
2088                ˇ
2089
2090                ˇThe quick brown fox jumps
2091                over the lazy dog.
2092            "},
2093            indoc! {"
2094                ˇThe quick brown fox jumps over the lazy dog.
2095                ˇ
2096                ˇThe quick brown fox jumps over the lazy dog.
2097
2098            "},
2099        ];
2100
2101        for paragraph_example in EXAMPLES {
2102            cx.simulate_at_each_offset("v i p", paragraph_example)
2103                .await
2104                .assert_matches();
2105            cx.simulate_at_each_offset("v a p", paragraph_example)
2106                .await
2107                .assert_matches();
2108        }
2109    }
2110
2111    #[gpui::test]
2112    async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2113        let mut cx = NeovimBackedTestContext::new(cx).await;
2114
2115        const WRAPPING_EXAMPLE: &str = indoc! {"
2116            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2117
2118            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2119
2120            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2121        "};
2122
2123        cx.set_shared_wrap(20).await;
2124
2125        cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
2126            .await
2127            .assert_matches();
2128        cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
2129            .await
2130            .assert_matches();
2131    }
2132
2133    #[gpui::test]
2134    async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2135        let mut cx = NeovimBackedTestContext::new(cx).await;
2136
2137        const WRAPPING_EXAMPLE: &str = indoc! {"
2138            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2139
2140            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2141
2142            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2143        "};
2144
2145        cx.set_shared_wrap(20).await;
2146
2147        cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
2148            .await
2149            .assert_matches();
2150        cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
2151            .await
2152            .assert_matches();
2153    }
2154
2155    #[gpui::test]
2156    async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
2157        let mut cx = NeovimBackedTestContext::new(cx).await;
2158
2159        cx.set_shared_state(indoc! {"
2160            a
2161                   ˇ•
2162            aaaaaaaaaaaaa
2163        "})
2164            .await;
2165
2166        cx.simulate_shared_keystrokes("d i p").await;
2167        cx.shared_state().await.assert_eq(indoc! {"
2168            a
2169            aaaaaaaˇaaaaaa
2170        "});
2171    }
2172
2173    #[gpui::test]
2174    async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2175        let mut cx = NeovimBackedTestContext::new(cx).await;
2176
2177        const WRAPPING_EXAMPLE: &str = indoc! {"
2178            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2179
2180            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2181
2182            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2183        "};
2184
2185        cx.set_shared_wrap(20).await;
2186
2187        cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
2188            .await
2189            .assert_matches();
2190        cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
2191            .await
2192            .assert_matches();
2193    }
2194
2195    // Test string with "`" for opening surrounders and "'" for closing surrounders
2196    const SURROUNDING_MARKER_STRING: &str = indoc! {"
2197        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
2198        'ˇfox juˇmps ov`ˇer
2199        the ˇlazy d'o`ˇg"};
2200
2201    const SURROUNDING_OBJECTS: &[(char, char)] = &[
2202        ('"', '"'), // Double Quote
2203        ('(', ')'), // Parentheses
2204    ];
2205
2206    #[gpui::test]
2207    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2208        let mut cx = NeovimBackedTestContext::new(cx).await;
2209
2210        for (start, end) in SURROUNDING_OBJECTS {
2211            let marked_string = SURROUNDING_MARKER_STRING
2212                .replace('`', &start.to_string())
2213                .replace('\'', &end.to_string());
2214
2215            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
2216                .await
2217                .assert_matches();
2218            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
2219                .await
2220                .assert_matches();
2221            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
2222                .await
2223                .assert_matches();
2224            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
2225                .await
2226                .assert_matches();
2227        }
2228    }
2229    #[gpui::test]
2230    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2231        let mut cx = NeovimBackedTestContext::new(cx).await;
2232        cx.set_shared_wrap(12).await;
2233
2234        cx.set_shared_state(indoc! {
2235            "\"ˇhello world\"!"
2236        })
2237        .await;
2238        cx.simulate_shared_keystrokes("v i \"").await;
2239        cx.shared_state().await.assert_eq(indoc! {
2240            "\"«hello worldˇ»\"!"
2241        });
2242
2243        cx.set_shared_state(indoc! {
2244            "\"hˇello world\"!"
2245        })
2246        .await;
2247        cx.simulate_shared_keystrokes("v i \"").await;
2248        cx.shared_state().await.assert_eq(indoc! {
2249            "\"«hello worldˇ»\"!"
2250        });
2251
2252        cx.set_shared_state(indoc! {
2253            "helˇlo \"world\"!"
2254        })
2255        .await;
2256        cx.simulate_shared_keystrokes("v i \"").await;
2257        cx.shared_state().await.assert_eq(indoc! {
2258            "hello \"«worldˇ»\"!"
2259        });
2260
2261        cx.set_shared_state(indoc! {
2262            "hello \"wˇorld\"!"
2263        })
2264        .await;
2265        cx.simulate_shared_keystrokes("v i \"").await;
2266        cx.shared_state().await.assert_eq(indoc! {
2267            "hello \"«worldˇ»\"!"
2268        });
2269
2270        cx.set_shared_state(indoc! {
2271            "hello \"wˇorld\"!"
2272        })
2273        .await;
2274        cx.simulate_shared_keystrokes("v a \"").await;
2275        cx.shared_state().await.assert_eq(indoc! {
2276            "hello« \"world\"ˇ»!"
2277        });
2278
2279        cx.set_shared_state(indoc! {
2280            "hello \"wˇorld\" !"
2281        })
2282        .await;
2283        cx.simulate_shared_keystrokes("v a \"").await;
2284        cx.shared_state().await.assert_eq(indoc! {
2285            "hello «\"world\" ˇ»!"
2286        });
2287
2288        cx.set_shared_state(indoc! {
2289            "hello \"wˇorld\"
2290            goodbye"
2291        })
2292        .await;
2293        cx.simulate_shared_keystrokes("v a \"").await;
2294        cx.shared_state().await.assert_eq(indoc! {
2295            "hello «\"world\" ˇ»
2296            goodbye"
2297        });
2298    }
2299
2300    #[gpui::test]
2301    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2302        let mut cx = VimTestContext::new(cx, true).await;
2303
2304        cx.set_state(
2305            indoc! {
2306                "func empty(a string) bool {
2307                   if a == \"\" {
2308                      return true
2309                   }
2310                   ˇreturn false
2311                }"
2312            },
2313            Mode::Normal,
2314        );
2315        cx.simulate_keystrokes("v i {");
2316        cx.assert_state(
2317            indoc! {
2318                "func empty(a string) bool {
2319                   «if a == \"\" {
2320                      return true
2321                   }
2322                   return falseˇ»
2323                }"
2324            },
2325            Mode::Visual,
2326        );
2327
2328        cx.set_state(
2329            indoc! {
2330                "func empty(a string) bool {
2331                     if a == \"\" {
2332                         ˇreturn true
2333                     }
2334                     return false
2335                }"
2336            },
2337            Mode::Normal,
2338        );
2339        cx.simulate_keystrokes("v i {");
2340        cx.assert_state(
2341            indoc! {
2342                "func empty(a string) bool {
2343                     if a == \"\" {
2344                         «return trueˇ»
2345                     }
2346                     return false
2347                }"
2348            },
2349            Mode::Visual,
2350        );
2351
2352        cx.set_state(
2353            indoc! {
2354                "func empty(a string) bool {
2355                     if a == \"\" ˇ{
2356                         return true
2357                     }
2358                     return false
2359                }"
2360            },
2361            Mode::Normal,
2362        );
2363        cx.simulate_keystrokes("v i {");
2364        cx.assert_state(
2365            indoc! {
2366                "func empty(a string) bool {
2367                     if a == \"\" {
2368                         «return trueˇ»
2369                     }
2370                     return false
2371                }"
2372            },
2373            Mode::Visual,
2374        );
2375
2376        cx.set_state(
2377            indoc! {
2378                "func empty(a string) bool {
2379                     if a == \"\" {
2380                         return true
2381                     }
2382                     return false
2383                ˇ}"
2384            },
2385            Mode::Normal,
2386        );
2387        cx.simulate_keystrokes("v i {");
2388        cx.assert_state(
2389            indoc! {
2390                "func empty(a string) bool {
2391                     «if a == \"\" {
2392                         return true
2393                     }
2394                     return falseˇ»
2395                }"
2396            },
2397            Mode::Visual,
2398        );
2399
2400        cx.set_state(
2401            indoc! {
2402                "func empty(a string) bool {
2403                             if a == \"\" {
2404                             ˇ
2405
2406                             }"
2407            },
2408            Mode::Normal,
2409        );
2410        cx.simulate_keystrokes("c i {");
2411        cx.assert_state(
2412            indoc! {
2413                "func empty(a string) bool {
2414                             if a == \"\" {ˇ}"
2415            },
2416            Mode::Insert,
2417        );
2418    }
2419
2420    #[gpui::test]
2421    async fn test_singleline_surrounding_character_objects_with_escape(
2422        cx: &mut gpui::TestAppContext,
2423    ) {
2424        let mut cx = NeovimBackedTestContext::new(cx).await;
2425        cx.set_shared_state(indoc! {
2426            "h\"e\\\"lˇlo \\\"world\"!"
2427        })
2428        .await;
2429        cx.simulate_shared_keystrokes("v i \"").await;
2430        cx.shared_state().await.assert_eq(indoc! {
2431            "h\"«e\\\"llo \\\"worldˇ»\"!"
2432        });
2433
2434        cx.set_shared_state(indoc! {
2435            "hello \"teˇst \\\"inside\\\" world\""
2436        })
2437        .await;
2438        cx.simulate_shared_keystrokes("v i \"").await;
2439        cx.shared_state().await.assert_eq(indoc! {
2440            "hello \"«test \\\"inside\\\" worldˇ»\""
2441        });
2442    }
2443
2444    #[gpui::test]
2445    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2446        let mut cx = VimTestContext::new(cx, true).await;
2447        cx.set_state(
2448            indoc! {"
2449            fn boop() {
2450                baz(ˇ|a, b| { bar(|j, k| { })})
2451            }"
2452            },
2453            Mode::Normal,
2454        );
2455        cx.simulate_keystrokes("c i |");
2456        cx.assert_state(
2457            indoc! {"
2458            fn boop() {
2459                baz(|ˇ| { bar(|j, k| { })})
2460            }"
2461            },
2462            Mode::Insert,
2463        );
2464        cx.simulate_keystrokes("escape 1 8 |");
2465        cx.assert_state(
2466            indoc! {"
2467            fn boop() {
2468                baz(|| { bar(ˇ|j, k| { })})
2469            }"
2470            },
2471            Mode::Normal,
2472        );
2473
2474        cx.simulate_keystrokes("v a |");
2475        cx.assert_state(
2476            indoc! {"
2477            fn boop() {
2478                baz(|| { bar(«|j, k| ˇ»{ })})
2479            }"
2480            },
2481            Mode::Visual,
2482        );
2483    }
2484
2485    #[gpui::test]
2486    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2487        let mut cx = VimTestContext::new(cx, true).await;
2488
2489        // Generic arguments
2490        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2491        cx.simulate_keystrokes("v i a");
2492        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2493
2494        // Function arguments
2495        cx.set_state(
2496            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2497            Mode::Normal,
2498        );
2499        cx.simulate_keystrokes("d a a");
2500        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2501
2502        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2503        cx.simulate_keystrokes("v a a");
2504        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2505
2506        // Tuple, vec, and array arguments
2507        cx.set_state(
2508            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2509            Mode::Normal,
2510        );
2511        cx.simulate_keystrokes("c i a");
2512        cx.assert_state(
2513            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2514            Mode::Insert,
2515        );
2516
2517        // TODO regressed with the up-to-date Rust grammar.
2518        // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2519        // cx.simulate_keystrokes("c a a");
2520        // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2521
2522        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2523        cx.simulate_keystrokes("c i a");
2524        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2525
2526        cx.set_state(
2527            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2528            Mode::Normal,
2529        );
2530        cx.simulate_keystrokes("c a a");
2531        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2532
2533        // Cursor immediately before / after brackets
2534        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2535        cx.simulate_keystrokes("v i a");
2536        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2537
2538        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2539        cx.simulate_keystrokes("v i a");
2540        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2541    }
2542
2543    #[gpui::test]
2544    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2545        let mut cx = VimTestContext::new(cx, true).await;
2546
2547        // Base use case
2548        cx.set_state(
2549            indoc! {"
2550                fn boop() {
2551                    // Comment
2552                    baz();ˇ
2553
2554                    loop {
2555                        bar(1);
2556                        bar(2);
2557                    }
2558
2559                    result
2560                }
2561            "},
2562            Mode::Normal,
2563        );
2564        cx.simulate_keystrokes("v i i");
2565        cx.assert_state(
2566            indoc! {"
2567                fn boop() {
2568                «    // Comment
2569                    baz();
2570
2571                    loop {
2572                        bar(1);
2573                        bar(2);
2574                    }
2575
2576                    resultˇ»
2577                }
2578            "},
2579            Mode::Visual,
2580        );
2581
2582        // Around indent (include line above)
2583        cx.set_state(
2584            indoc! {"
2585                const ABOVE: str = true;
2586                fn boop() {
2587
2588                    hello();
2589                    worˇld()
2590                }
2591            "},
2592            Mode::Normal,
2593        );
2594        cx.simulate_keystrokes("v a i");
2595        cx.assert_state(
2596            indoc! {"
2597                const ABOVE: str = true;
2598                «fn boop() {
2599
2600                    hello();
2601                    world()ˇ»
2602                }
2603            "},
2604            Mode::Visual,
2605        );
2606
2607        // Around indent (include line above & below)
2608        cx.set_state(
2609            indoc! {"
2610                const ABOVE: str = true;
2611                fn boop() {
2612                    hellˇo();
2613                    world()
2614
2615                }
2616                const BELOW: str = true;
2617            "},
2618            Mode::Normal,
2619        );
2620        cx.simulate_keystrokes("c a shift-i");
2621        cx.assert_state(
2622            indoc! {"
2623                const ABOVE: str = true;
2624                ˇ
2625                const BELOW: str = true;
2626            "},
2627            Mode::Insert,
2628        );
2629    }
2630
2631    #[gpui::test]
2632    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2633        let mut cx = NeovimBackedTestContext::new(cx).await;
2634
2635        for (start, end) in SURROUNDING_OBJECTS {
2636            let marked_string = SURROUNDING_MARKER_STRING
2637                .replace('`', &start.to_string())
2638                .replace('\'', &end.to_string());
2639
2640            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2641                .await
2642                .assert_matches();
2643            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2644                .await
2645                .assert_matches();
2646            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2647                .await
2648                .assert_matches();
2649            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2650                .await
2651                .assert_matches();
2652        }
2653    }
2654
2655    #[gpui::test]
2656    async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2657        let mut cx = VimTestContext::new(cx, true).await;
2658        cx.update(|_, cx| {
2659            cx.bind_keys([KeyBinding::new(
2660                "q",
2661                AnyQuotes,
2662                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2663            )]);
2664        });
2665
2666        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2667            // the false string in the middle should be considered
2668            (
2669                "c i q",
2670                "'first' false ˇstring 'second'",
2671                "'first'ˇ'second'",
2672                Mode::Insert,
2673            ),
2674            // Single quotes
2675            (
2676                "c i q",
2677                "Thisˇ is a 'quote' example.",
2678                "This is a 'ˇ' example.",
2679                Mode::Insert,
2680            ),
2681            (
2682                "c a q",
2683                "Thisˇ is a 'quote' example.",
2684                "This is a ˇexample.",
2685                Mode::Insert,
2686            ),
2687            (
2688                "c i q",
2689                "This is a \"simple 'qˇuote'\" example.",
2690                "This is a \"simple 'ˇ'\" example.",
2691                Mode::Insert,
2692            ),
2693            (
2694                "c a q",
2695                "This is a \"simple 'qˇuote'\" example.",
2696                "This is a \"simpleˇ\" example.",
2697                Mode::Insert,
2698            ),
2699            (
2700                "c i q",
2701                "This is a 'qˇuote' example.",
2702                "This is a 'ˇ' example.",
2703                Mode::Insert,
2704            ),
2705            (
2706                "c a q",
2707                "This is a 'qˇuote' example.",
2708                "This is a ˇexample.",
2709                Mode::Insert,
2710            ),
2711            (
2712                "d i q",
2713                "This is a 'qˇuote' example.",
2714                "This is a 'ˇ' example.",
2715                Mode::Normal,
2716            ),
2717            (
2718                "d a q",
2719                "This is a 'qˇuote' example.",
2720                "This is a ˇexample.",
2721                Mode::Normal,
2722            ),
2723            // Double quotes
2724            (
2725                "c i q",
2726                "This is a \"qˇuote\" example.",
2727                "This is a \"ˇ\" example.",
2728                Mode::Insert,
2729            ),
2730            (
2731                "c a q",
2732                "This is a \"qˇuote\" example.",
2733                "This is a ˇexample.",
2734                Mode::Insert,
2735            ),
2736            (
2737                "d i q",
2738                "This is a \"qˇuote\" example.",
2739                "This is a \"ˇ\" example.",
2740                Mode::Normal,
2741            ),
2742            (
2743                "d a q",
2744                "This is a \"qˇuote\" example.",
2745                "This is a ˇexample.",
2746                Mode::Normal,
2747            ),
2748            // Back quotes
2749            (
2750                "c i q",
2751                "This is a `qˇuote` example.",
2752                "This is a `ˇ` example.",
2753                Mode::Insert,
2754            ),
2755            (
2756                "c a q",
2757                "This is a `qˇuote` example.",
2758                "This is a ˇexample.",
2759                Mode::Insert,
2760            ),
2761            (
2762                "d i q",
2763                "This is a `qˇuote` example.",
2764                "This is a `ˇ` example.",
2765                Mode::Normal,
2766            ),
2767            (
2768                "d a q",
2769                "This is a `qˇuote` example.",
2770                "This is a ˇexample.",
2771                Mode::Normal,
2772            ),
2773        ];
2774
2775        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2776            cx.set_state(initial_state, Mode::Normal);
2777
2778            cx.simulate_keystrokes(keystrokes);
2779
2780            cx.assert_state(expected_state, *expected_mode);
2781        }
2782
2783        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2784            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2785            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2786            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2787            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2788            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2789            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2790            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2791            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2792            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2793            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2794            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2795            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2796        ];
2797
2798        for (keystrokes, initial_state, mode) in INVALID_CASES {
2799            cx.set_state(initial_state, Mode::Normal);
2800
2801            cx.simulate_keystrokes(keystrokes);
2802
2803            cx.assert_state(initial_state, *mode);
2804        }
2805    }
2806
2807    #[gpui::test]
2808    async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2809        let mut cx = VimTestContext::new_typescript(cx).await;
2810
2811        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2812            // Special cases from mini.ai plugin
2813            // the false string in the middle should not be considered
2814            (
2815                "c i q",
2816                "'first' false ˇstring 'second'",
2817                "'first' false string 'ˇ'",
2818                Mode::Insert,
2819            ),
2820            // Multiline support :)! Same behavior as mini.ai plugin
2821            (
2822                "c i q",
2823                indoc! {"
2824                    `
2825                    first
2826                    middle ˇstring
2827                    second
2828                    `
2829                "},
2830                indoc! {"
2831                    `ˇ`
2832                "},
2833                Mode::Insert,
2834            ),
2835            // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2836            // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2837            // Bug reference: https://github.com/zed-industries/zed/issues/23889
2838            ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2839            // Single quotes
2840            (
2841                "c i q",
2842                "Thisˇ is a 'quote' example.",
2843                "This is a 'ˇ' example.",
2844                Mode::Insert,
2845            ),
2846            (
2847                "c a q",
2848                "Thisˇ is a 'quote' example.",
2849                "This is a ˇ example.", // same mini.ai plugin behavior
2850                Mode::Insert,
2851            ),
2852            (
2853                "c i q",
2854                "This is a \"simple 'qˇuote'\" example.",
2855                "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2856                Mode::Insert,
2857            ),
2858            (
2859                "c a q",
2860                "This is a \"simple 'qˇuote'\" example.",
2861                "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2862                Mode::Insert,
2863            ),
2864            (
2865                "c i q",
2866                "This is a 'qˇuote' example.",
2867                "This is a 'ˇ' example.",
2868                Mode::Insert,
2869            ),
2870            (
2871                "c a q",
2872                "This is a 'qˇuote' example.",
2873                "This is a ˇ example.", // same mini.ai plugin behavior
2874                Mode::Insert,
2875            ),
2876            (
2877                "d i q",
2878                "This is a 'qˇuote' example.",
2879                "This is a 'ˇ' example.",
2880                Mode::Normal,
2881            ),
2882            (
2883                "d a q",
2884                "This is a 'qˇuote' example.",
2885                "This is a ˇ example.", // same mini.ai plugin behavior
2886                Mode::Normal,
2887            ),
2888            // Double quotes
2889            (
2890                "c i q",
2891                "This is a \"qˇuote\" example.",
2892                "This is a \"ˇ\" example.",
2893                Mode::Insert,
2894            ),
2895            (
2896                "c a q",
2897                "This is a \"qˇuote\" example.",
2898                "This is a ˇ example.", // same mini.ai plugin behavior
2899                Mode::Insert,
2900            ),
2901            (
2902                "d i q",
2903                "This is a \"qˇuote\" example.",
2904                "This is a \"ˇ\" example.",
2905                Mode::Normal,
2906            ),
2907            (
2908                "d a q",
2909                "This is a \"qˇuote\" example.",
2910                "This is a ˇ example.", // same mini.ai plugin behavior
2911                Mode::Normal,
2912            ),
2913            // Back quotes
2914            (
2915                "c i q",
2916                "This is a `qˇuote` example.",
2917                "This is a `ˇ` example.",
2918                Mode::Insert,
2919            ),
2920            (
2921                "c a q",
2922                "This is a `qˇuote` example.",
2923                "This is a ˇ example.", // same mini.ai plugin behavior
2924                Mode::Insert,
2925            ),
2926            (
2927                "d i q",
2928                "This is a `qˇuote` example.",
2929                "This is a `ˇ` example.",
2930                Mode::Normal,
2931            ),
2932            (
2933                "d a q",
2934                "This is a `qˇuote` example.",
2935                "This is a ˇ example.", // same mini.ai plugin behavior
2936                Mode::Normal,
2937            ),
2938        ];
2939
2940        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2941            cx.set_state(initial_state, Mode::Normal);
2942            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2943            cx.simulate_keystrokes(keystrokes);
2944            cx.assert_state(expected_state, *expected_mode);
2945        }
2946
2947        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2948            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2949            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2950            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2951            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2952            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2953            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2954            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2955            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2956            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2957            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2958            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2959            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2960        ];
2961
2962        for (keystrokes, initial_state, mode) in INVALID_CASES {
2963            cx.set_state(initial_state, Mode::Normal);
2964            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2965            cx.simulate_keystrokes(keystrokes);
2966            cx.assert_state(initial_state, *mode);
2967        }
2968    }
2969
2970    #[gpui::test]
2971    async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2972        let mut cx = VimTestContext::new(cx, true).await;
2973        cx.update(|_, cx| {
2974            cx.bind_keys([KeyBinding::new(
2975                "b",
2976                AnyBrackets,
2977                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2978            )]);
2979        });
2980
2981        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2982            (
2983                "c i b",
2984                indoc! {"
2985                    {
2986                        {
2987                            ˇprint('hello')
2988                        }
2989                    }
2990                "},
2991                indoc! {"
2992                    {
2993                        {
2994                            ˇ
2995                        }
2996                    }
2997                "},
2998                Mode::Insert,
2999            ),
3000            // Bracket (Parentheses)
3001            (
3002                "c i b",
3003                "Thisˇ is a (simple [quote]) example.",
3004                "This is a (ˇ) example.",
3005                Mode::Insert,
3006            ),
3007            (
3008                "c i b",
3009                "This is a [simple (qˇuote)] example.",
3010                "This is a [simple (ˇ)] example.",
3011                Mode::Insert,
3012            ),
3013            (
3014                "c a b",
3015                "This is a [simple (qˇuote)] example.",
3016                "This is a [simple ˇ] example.",
3017                Mode::Insert,
3018            ),
3019            (
3020                "c a b",
3021                "Thisˇ is a (simple [quote]) example.",
3022                "This is a ˇ example.",
3023                Mode::Insert,
3024            ),
3025            (
3026                "c i b",
3027                "This is a (qˇuote) example.",
3028                "This is a (ˇ) example.",
3029                Mode::Insert,
3030            ),
3031            (
3032                "c a b",
3033                "This is a (qˇuote) example.",
3034                "This is a ˇ example.",
3035                Mode::Insert,
3036            ),
3037            (
3038                "d i b",
3039                "This is a (qˇuote) example.",
3040                "This is a (ˇ) example.",
3041                Mode::Normal,
3042            ),
3043            (
3044                "d a b",
3045                "This is a (qˇuote) example.",
3046                "This is a ˇ example.",
3047                Mode::Normal,
3048            ),
3049            // Square brackets
3050            (
3051                "c i b",
3052                "This is a [qˇuote] example.",
3053                "This is a [ˇ] example.",
3054                Mode::Insert,
3055            ),
3056            (
3057                "c a b",
3058                "This is a [qˇuote] example.",
3059                "This is a ˇ example.",
3060                Mode::Insert,
3061            ),
3062            (
3063                "d i b",
3064                "This is a [qˇuote] example.",
3065                "This is a [ˇ] example.",
3066                Mode::Normal,
3067            ),
3068            (
3069                "d a b",
3070                "This is a [qˇuote] example.",
3071                "This is a ˇ example.",
3072                Mode::Normal,
3073            ),
3074            // Curly brackets
3075            (
3076                "c i b",
3077                "This is a {qˇuote} example.",
3078                "This is a {ˇ} example.",
3079                Mode::Insert,
3080            ),
3081            (
3082                "c a b",
3083                "This is a {qˇuote} example.",
3084                "This is a ˇ example.",
3085                Mode::Insert,
3086            ),
3087            (
3088                "d i b",
3089                "This is a {qˇuote} example.",
3090                "This is a {ˇ} example.",
3091                Mode::Normal,
3092            ),
3093            (
3094                "d a b",
3095                "This is a {qˇuote} example.",
3096                "This is a ˇ example.",
3097                Mode::Normal,
3098            ),
3099        ];
3100
3101        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3102            cx.set_state(initial_state, Mode::Normal);
3103
3104            cx.simulate_keystrokes(keystrokes);
3105
3106            cx.assert_state(expected_state, *expected_mode);
3107        }
3108
3109        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3110            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3111            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3112            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3113            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3114            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3115            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3116            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3117            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3118            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3119            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3120            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3121            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3122        ];
3123
3124        for (keystrokes, initial_state, mode) in INVALID_CASES {
3125            cx.set_state(initial_state, Mode::Normal);
3126
3127            cx.simulate_keystrokes(keystrokes);
3128
3129            cx.assert_state(initial_state, *mode);
3130        }
3131    }
3132
3133    #[gpui::test]
3134    async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
3135        let mut cx = VimTestContext::new(cx, true).await;
3136        cx.update(|_, cx| {
3137            cx.bind_keys([KeyBinding::new(
3138                "b",
3139                MiniBrackets,
3140                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
3141            )]);
3142        });
3143
3144        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
3145            // Special cases from mini.ai plugin
3146            // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
3147            // Same behavior as mini.ai plugin
3148            (
3149                "c i b",
3150                indoc! {"
3151                    {
3152                        {
3153                            ˇprint('hello')
3154                        }
3155                    }
3156                "},
3157                indoc! {"
3158                    {
3159                        {
3160                            print(ˇ)
3161                        }
3162                    }
3163                "},
3164                Mode::Insert,
3165            ),
3166            // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3167            // Same behavior as mini.ai plugin
3168            (
3169                "c i b",
3170                indoc! {"
3171                    {
3172                        {
3173                            ˇ
3174                            print('hello')
3175                        }
3176                    }
3177                "},
3178                indoc! {"
3179                    {
3180                        {ˇ}
3181                    }
3182                "},
3183                Mode::Insert,
3184            ),
3185            // If you are in the open bracket then it has higher priority
3186            (
3187                "c i b",
3188                indoc! {"
3189                    «{ˇ»
3190                        {
3191                            print('hello')
3192                        }
3193                    }
3194                "},
3195                indoc! {"
3196                    {ˇ}
3197                "},
3198                Mode::Insert,
3199            ),
3200            // If you are in the close bracket then it has higher priority
3201            (
3202                "c i b",
3203                indoc! {"
3204                    {
3205                        {
3206                            print('hello')
3207                        }
3208                    «}ˇ»
3209                "},
3210                indoc! {"
3211                    {ˇ}
3212                "},
3213                Mode::Insert,
3214            ),
3215            // Bracket (Parentheses)
3216            (
3217                "c i b",
3218                "Thisˇ is a (simple [quote]) example.",
3219                "This is a (ˇ) example.",
3220                Mode::Insert,
3221            ),
3222            (
3223                "c i b",
3224                "This is a [simple (qˇuote)] example.",
3225                "This is a [simple (ˇ)] example.",
3226                Mode::Insert,
3227            ),
3228            (
3229                "c a b",
3230                "This is a [simple (qˇuote)] example.",
3231                "This is a [simple ˇ] example.",
3232                Mode::Insert,
3233            ),
3234            (
3235                "c a b",
3236                "Thisˇ is a (simple [quote]) example.",
3237                "This is a ˇ example.",
3238                Mode::Insert,
3239            ),
3240            (
3241                "c i b",
3242                "This is a (qˇuote) example.",
3243                "This is a (ˇ) example.",
3244                Mode::Insert,
3245            ),
3246            (
3247                "c a b",
3248                "This is a (qˇuote) example.",
3249                "This is a ˇ example.",
3250                Mode::Insert,
3251            ),
3252            (
3253                "d i b",
3254                "This is a (qˇuote) example.",
3255                "This is a (ˇ) example.",
3256                Mode::Normal,
3257            ),
3258            (
3259                "d a b",
3260                "This is a (qˇuote) example.",
3261                "This is a ˇ example.",
3262                Mode::Normal,
3263            ),
3264            // Square brackets
3265            (
3266                "c i b",
3267                "This is a [qˇuote] example.",
3268                "This is a [ˇ] example.",
3269                Mode::Insert,
3270            ),
3271            (
3272                "c a b",
3273                "This is a [qˇuote] example.",
3274                "This is a ˇ example.",
3275                Mode::Insert,
3276            ),
3277            (
3278                "d i b",
3279                "This is a [qˇuote] example.",
3280                "This is a [ˇ] example.",
3281                Mode::Normal,
3282            ),
3283            (
3284                "d a b",
3285                "This is a [qˇuote] example.",
3286                "This is a ˇ example.",
3287                Mode::Normal,
3288            ),
3289            // Curly brackets
3290            (
3291                "c i b",
3292                "This is a {qˇuote} example.",
3293                "This is a {ˇ} example.",
3294                Mode::Insert,
3295            ),
3296            (
3297                "c a b",
3298                "This is a {qˇuote} example.",
3299                "This is a ˇ example.",
3300                Mode::Insert,
3301            ),
3302            (
3303                "d i b",
3304                "This is a {qˇuote} example.",
3305                "This is a {ˇ} example.",
3306                Mode::Normal,
3307            ),
3308            (
3309                "d a b",
3310                "This is a {qˇuote} example.",
3311                "This is a ˇ example.",
3312                Mode::Normal,
3313            ),
3314        ];
3315
3316        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3317            cx.set_state(initial_state, Mode::Normal);
3318            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3319            cx.simulate_keystrokes(keystrokes);
3320            cx.assert_state(expected_state, *expected_mode);
3321        }
3322
3323        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3324            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3325            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3326            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3327            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3328            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3329            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3330            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3331            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3332            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3333            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3334            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3335            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3336        ];
3337
3338        for (keystrokes, initial_state, mode) in INVALID_CASES {
3339            cx.set_state(initial_state, Mode::Normal);
3340            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3341            cx.simulate_keystrokes(keystrokes);
3342            cx.assert_state(initial_state, *mode);
3343        }
3344    }
3345
3346    #[gpui::test]
3347    async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
3348        // Initialize test context with the TypeScript language loaded, so we
3349        // can actually get brackets definition.
3350        let mut cx = VimTestContext::new(cx, true).await;
3351
3352        // Update `b` to `MiniBrackets` so we can later use it when simulating
3353        // keystrokes.
3354        cx.update(|_, cx| {
3355            cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
3356        });
3357
3358        let (editor, cx) = cx.add_window_view(|window, cx| {
3359            let multi_buffer = MultiBuffer::build_multi(
3360                [
3361                    ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
3362                    ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
3363                ],
3364                cx,
3365            );
3366
3367            // In order for the brackets to actually be found, we need to update
3368            // the language used for the second buffer. This is something that
3369            // is handled automatically when simply using `VimTestContext::new`
3370            // but, since this is being set manually, the language isn't
3371            // automatically set.
3372            let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
3373            let buffer_ids = multi_buffer
3374                .read(cx)
3375                .snapshot(cx)
3376                .excerpts()
3377                .map(|excerpt| excerpt.context.start.buffer_id)
3378                .collect::<Vec<_>>();
3379            if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
3380                buffer.update(cx, |buffer, cx| {
3381                    buffer.set_language(Some(language::rust_lang()), cx);
3382                })
3383            };
3384
3385            editor
3386        });
3387
3388        let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3389
3390        cx.assert_excerpts_with_selections(indoc! {"
3391            [EXCERPT]
3392            ˇ111
3393            222
3394            [EXCERPT]
3395            111
3396            a {bracket} example
3397            "
3398        });
3399
3400        cx.simulate_keystrokes("j j j j f r");
3401        cx.assert_excerpts_with_selections(indoc! {"
3402            [EXCERPT]
3403            111
3404            222
3405            [EXCERPT]
3406            111
3407            a {bˇracket} example
3408            "
3409        });
3410
3411        cx.simulate_keystrokes("d i b");
3412        cx.assert_excerpts_with_selections(indoc! {"
3413            [EXCERPT]
3414            111
3415            222
3416            [EXCERPT]
3417            111
3418            a {ˇ} example
3419            "
3420        });
3421    }
3422
3423    #[gpui::test]
3424    async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3425        let mut cx = NeovimBackedTestContext::new(cx).await;
3426        cx.set_shared_state("(trailingˇ whitespace          )")
3427            .await;
3428        cx.simulate_shared_keystrokes("v i b").await;
3429        cx.shared_state().await.assert_matches();
3430        cx.simulate_shared_keystrokes("escape y i b").await;
3431        cx.shared_clipboard()
3432            .await
3433            .assert_eq("trailing whitespace          ");
3434    }
3435
3436    #[gpui::test]
3437    async fn test_tags(cx: &mut gpui::TestAppContext) {
3438        let mut cx = VimTestContext::new_html(cx).await;
3439
3440        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3441        cx.simulate_keystrokes("v i t");
3442        cx.assert_state(
3443            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3444            Mode::Visual,
3445        );
3446        cx.simulate_keystrokes("a t");
3447        cx.assert_state(
3448            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3449            Mode::Visual,
3450        );
3451        cx.simulate_keystrokes("a t");
3452        cx.assert_state(
3453            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3454            Mode::Visual,
3455        );
3456
3457        // The cursor is before the tag
3458        cx.set_state(
3459            "<html><head></head><body> ˇ  <b>hi!</b></body>",
3460            Mode::Normal,
3461        );
3462        cx.simulate_keystrokes("v i t");
3463        cx.assert_state(
3464            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
3465            Mode::Visual,
3466        );
3467        cx.simulate_keystrokes("a t");
3468        cx.assert_state(
3469            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
3470            Mode::Visual,
3471        );
3472
3473        // The cursor is in the open tag
3474        cx.set_state(
3475            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3476            Mode::Normal,
3477        );
3478        cx.simulate_keystrokes("v a t");
3479        cx.assert_state(
3480            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3481            Mode::Visual,
3482        );
3483        cx.simulate_keystrokes("i t");
3484        cx.assert_state(
3485            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3486            Mode::Visual,
3487        );
3488
3489        // current selection length greater than 1
3490        cx.set_state(
3491            "<html><head></head><body><«b>hi!ˇ»</b></body>",
3492            Mode::Visual,
3493        );
3494        cx.simulate_keystrokes("i t");
3495        cx.assert_state(
3496            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3497            Mode::Visual,
3498        );
3499        cx.simulate_keystrokes("a t");
3500        cx.assert_state(
3501            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3502            Mode::Visual,
3503        );
3504
3505        cx.set_state(
3506            "<html><head></head><body><«b>hi!</ˇ»b></body>",
3507            Mode::Visual,
3508        );
3509        cx.simulate_keystrokes("a t");
3510        cx.assert_state(
3511            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3512            Mode::Visual,
3513        );
3514    }
3515    #[gpui::test]
3516    async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3517        let mut cx = NeovimBackedTestContext::new(cx).await;
3518
3519        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3520            .await;
3521        cx.simulate_shared_keystrokes("v a w").await;
3522        cx.shared_state()
3523            .await
3524            .assert_eq("    «const ˇ»f = (x: unknown) => {");
3525
3526        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3527            .await;
3528        cx.simulate_shared_keystrokes("y a w").await;
3529        cx.shared_clipboard().await.assert_eq("const ");
3530
3531        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3532            .await;
3533        cx.simulate_shared_keystrokes("d a w").await;
3534        cx.shared_state()
3535            .await
3536            .assert_eq("    ˇf = (x: unknown) => {");
3537        cx.shared_clipboard().await.assert_eq("const ");
3538
3539        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3540            .await;
3541        cx.simulate_shared_keystrokes("c a w").await;
3542        cx.shared_state()
3543            .await
3544            .assert_eq("    ˇf = (x: unknown) => {");
3545        cx.shared_clipboard().await.assert_eq("const ");
3546    }
3547
3548    #[gpui::test]
3549    async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
3550        let mut cx = VimTestContext::new_typescript(cx).await;
3551
3552        cx.set_state(
3553            indoc! {"
3554                const foo = () => {
3555                    return ˇ1;
3556                };
3557            "},
3558            Mode::Normal,
3559        );
3560        cx.simulate_keystrokes("v a f");
3561        cx.assert_state(
3562            indoc! {"
3563                «const foo = () => {
3564                    return 1;
3565                };ˇ»
3566            "},
3567            Mode::VisualLine,
3568        );
3569
3570        cx.set_state(
3571            indoc! {"
3572                arr.map(() => {
3573                    return ˇ1;
3574                });
3575            "},
3576            Mode::Normal,
3577        );
3578        cx.simulate_keystrokes("v a f");
3579        cx.assert_state(
3580            indoc! {"
3581                arr.map(«() => {
3582                    return 1;
3583                }ˇ»);
3584            "},
3585            Mode::VisualLine,
3586        );
3587
3588        cx.set_state(
3589            indoc! {"
3590                const foo = () => {
3591                    return ˇ1;
3592                };
3593            "},
3594            Mode::Normal,
3595        );
3596        cx.simulate_keystrokes("v i f");
3597        cx.assert_state(
3598            indoc! {"
3599                const foo = () => {
3600                    «return 1;ˇ»
3601                };
3602            "},
3603            Mode::Visual,
3604        );
3605
3606        cx.set_state(
3607            indoc! {"
3608                (() => {
3609                    console.log(ˇ1);
3610                })();
3611            "},
3612            Mode::Normal,
3613        );
3614        cx.simulate_keystrokes("v a f");
3615        cx.assert_state(
3616            indoc! {"
3617                («() => {
3618                    console.log(1);
3619                }ˇ»)();
3620            "},
3621            Mode::VisualLine,
3622        );
3623
3624        cx.set_state(
3625            indoc! {"
3626                const foo = () => {
3627                    return ˇ1;
3628                };
3629                export { foo };
3630            "},
3631            Mode::Normal,
3632        );
3633        cx.simulate_keystrokes("v a f");
3634        cx.assert_state(
3635            indoc! {"
3636                «const foo = () => {
3637                    return 1;
3638                };ˇ»
3639                export { foo };
3640            "},
3641            Mode::VisualLine,
3642        );
3643
3644        cx.set_state(
3645            indoc! {"
3646                let bar = () => {
3647                    return ˇ2;
3648                };
3649            "},
3650            Mode::Normal,
3651        );
3652        cx.simulate_keystrokes("v a f");
3653        cx.assert_state(
3654            indoc! {"
3655                «let bar = () => {
3656                    return 2;
3657                };ˇ»
3658            "},
3659            Mode::VisualLine,
3660        );
3661
3662        cx.set_state(
3663            indoc! {"
3664                var baz = () => {
3665                    return ˇ3;
3666                };
3667            "},
3668            Mode::Normal,
3669        );
3670        cx.simulate_keystrokes("v a f");
3671        cx.assert_state(
3672            indoc! {"
3673                «var baz = () => {
3674                    return 3;
3675                };ˇ»
3676            "},
3677            Mode::VisualLine,
3678        );
3679
3680        cx.set_state(
3681            indoc! {"
3682                const add = (a, b) => a + ˇb;
3683            "},
3684            Mode::Normal,
3685        );
3686        cx.simulate_keystrokes("v a f");
3687        cx.assert_state(
3688            indoc! {"
3689                «const add = (a, b) => a + b;ˇ»
3690            "},
3691            Mode::VisualLine,
3692        );
3693
3694        cx.set_state(
3695            indoc! {"
3696                const add = ˇ(a, b) => a + b;
3697            "},
3698            Mode::Normal,
3699        );
3700        cx.simulate_keystrokes("v a f");
3701        cx.assert_state(
3702            indoc! {"
3703                «const add = (a, b) => a + b;ˇ»
3704            "},
3705            Mode::VisualLine,
3706        );
3707
3708        cx.set_state(
3709            indoc! {"
3710                const add = (a, b) => a + bˇ;
3711            "},
3712            Mode::Normal,
3713        );
3714        cx.simulate_keystrokes("v a f");
3715        cx.assert_state(
3716            indoc! {"
3717                «const add = (a, b) => a + b;ˇ»
3718            "},
3719            Mode::VisualLine,
3720        );
3721
3722        cx.set_state(
3723            indoc! {"
3724                const add = (a, b) =ˇ> a + b;
3725            "},
3726            Mode::Normal,
3727        );
3728        cx.simulate_keystrokes("v a f");
3729        cx.assert_state(
3730            indoc! {"
3731                «const add = (a, b) => a + b;ˇ»
3732            "},
3733            Mode::VisualLine,
3734        );
3735    }
3736
3737    #[gpui::test]
3738    async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
3739        let mut cx = VimTestContext::new_tsx(cx).await;
3740
3741        cx.set_state(
3742            indoc! {r#"
3743                export const MyComponent = () => {
3744                  return (
3745                    <div>
3746                      <div onClick={() => {
3747                        alert("Hello world!");
3748                        console.log(ˇ"clicked");
3749                      }}>Hello world!</div>
3750                    </div>
3751                  );
3752                };
3753            "#},
3754            Mode::Normal,
3755        );
3756        cx.simulate_keystrokes("v a f");
3757        cx.assert_state(
3758            indoc! {r#"
3759                export const MyComponent = () => {
3760                  return (
3761                    <div>
3762                      <div onClick={«() => {
3763                        alert("Hello world!");
3764                        console.log("clicked");
3765                      }ˇ»}>Hello world!</div>
3766                    </div>
3767                  );
3768                };
3769            "#},
3770            Mode::VisualLine,
3771        );
3772
3773        cx.set_state(
3774            indoc! {r#"
3775                export const MyComponent = () => {
3776                  return (
3777                    <div>
3778                      <div onClick={() => console.log("clickˇed")}>Hello world!</div>
3779                    </div>
3780                  );
3781                };
3782            "#},
3783            Mode::Normal,
3784        );
3785        cx.simulate_keystrokes("v a f");
3786        cx.assert_state(
3787            indoc! {r#"
3788                export const MyComponent = () => {
3789                  return (
3790                    <div>
3791                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3792                    </div>
3793                  );
3794                };
3795            "#},
3796            Mode::VisualLine,
3797        );
3798
3799        cx.set_state(
3800            indoc! {r#"
3801                export const MyComponent = () => {
3802                  return (
3803                    <div>
3804                      <div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
3805                    </div>
3806                  );
3807                };
3808            "#},
3809            Mode::Normal,
3810        );
3811        cx.simulate_keystrokes("v a f");
3812        cx.assert_state(
3813            indoc! {r#"
3814                export const MyComponent = () => {
3815                  return (
3816                    <div>
3817                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3818                    </div>
3819                  );
3820                };
3821            "#},
3822            Mode::VisualLine,
3823        );
3824
3825        cx.set_state(
3826            indoc! {r#"
3827                export const MyComponent = () => {
3828                  return (
3829                    <div>
3830                      <div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
3831                    </div>
3832                  );
3833                };
3834            "#},
3835            Mode::Normal,
3836        );
3837        cx.simulate_keystrokes("v a f");
3838        cx.assert_state(
3839            indoc! {r#"
3840                export const MyComponent = () => {
3841                  return (
3842                    <div>
3843                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3844                    </div>
3845                  );
3846                };
3847            "#},
3848            Mode::VisualLine,
3849        );
3850
3851        cx.set_state(
3852            indoc! {r#"
3853                export const MyComponent = () => {
3854                  return (
3855                    <div>
3856                      <div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
3857                    </div>
3858                  );
3859                };
3860            "#},
3861            Mode::Normal,
3862        );
3863        cx.simulate_keystrokes("v a f");
3864        cx.assert_state(
3865            indoc! {r#"
3866                export const MyComponent = () => {
3867                  return (
3868                    <div>
3869                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3870                    </div>
3871                  );
3872                };
3873            "#},
3874            Mode::VisualLine,
3875        );
3876
3877        cx.set_state(
3878            indoc! {r#"
3879                export const MyComponent = () => {
3880                  return (
3881                    <div>
3882                      <div onClick={() => {
3883                        console.log("cliˇcked");
3884                      }}>Hello world!</div>
3885                    </div>
3886                  );
3887                };
3888            "#},
3889            Mode::Normal,
3890        );
3891        cx.simulate_keystrokes("v a f");
3892        cx.assert_state(
3893            indoc! {r#"
3894                export const MyComponent = () => {
3895                  return (
3896                    <div>
3897                      <div onClick={«() => {
3898                        console.log("clicked");
3899                      }ˇ»}>Hello world!</div>
3900                    </div>
3901                  );
3902                };
3903            "#},
3904            Mode::VisualLine,
3905        );
3906
3907        cx.set_state(
3908            indoc! {r#"
3909                export const MyComponent = () => {
3910                  return (
3911                    <div>
3912                      <div onClick={() => fˇoo()}>Hello world!</div>
3913                    </div>
3914                  );
3915                };
3916            "#},
3917            Mode::Normal,
3918        );
3919        cx.simulate_keystrokes("v a f");
3920        cx.assert_state(
3921            indoc! {r#"
3922                export const MyComponent = () => {
3923                  return (
3924                    <div>
3925                      <div onClick={«() => foo()ˇ»}>Hello world!</div>
3926                    </div>
3927                  );
3928                };
3929            "#},
3930            Mode::VisualLine,
3931        );
3932    }
3933
3934    #[gpui::test]
3935    async fn test_subword_object(cx: &mut gpui::TestAppContext) {
3936        let mut cx = VimTestContext::new(cx, true).await;
3937
3938        // Setup custom keybindings for subword object so we can use the
3939        // bindings in `simulate_keystrokes`.
3940        cx.update(|_window, cx| {
3941            cx.bind_keys([KeyBinding::new(
3942                "w",
3943                super::Subword {
3944                    ignore_punctuation: false,
3945                },
3946                Some("vim_operator"),
3947            )]);
3948        });
3949
3950        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3951        cx.simulate_keystrokes("c i w");
3952        cx.assert_state("foo_ˇ_baz", Mode::Insert);
3953
3954        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
3955        cx.simulate_keystrokes("c i w");
3956        cx.assert_state("ˇ_bar_baz", Mode::Insert);
3957
3958        cx.set_state("foo_bar_baˇz", Mode::Normal);
3959        cx.simulate_keystrokes("c i w");
3960        cx.assert_state("foo_bar_ˇ", Mode::Insert);
3961
3962        cx.set_state("fooˇBarBaz", Mode::Normal);
3963        cx.simulate_keystrokes("c i w");
3964        cx.assert_state("fooˇBaz", Mode::Insert);
3965
3966        cx.set_state("ˇfooBarBaz", Mode::Normal);
3967        cx.simulate_keystrokes("c i w");
3968        cx.assert_state("ˇBarBaz", Mode::Insert);
3969
3970        cx.set_state("fooBarBaˇz", Mode::Normal);
3971        cx.simulate_keystrokes("c i w");
3972        cx.assert_state("fooBarˇ", Mode::Insert);
3973
3974        cx.set_state("foo.ˇbar.baz", Mode::Normal);
3975        cx.simulate_keystrokes("c i w");
3976        cx.assert_state("foo.ˇ.baz", Mode::Insert);
3977
3978        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3979        cx.simulate_keystrokes("d i w");
3980        cx.assert_state("foo_ˇ_baz", Mode::Normal);
3981
3982        cx.set_state("fooˇBarBaz", Mode::Normal);
3983        cx.simulate_keystrokes("d i w");
3984        cx.assert_state("fooˇBaz", Mode::Normal);
3985
3986        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3987        cx.simulate_keystrokes("c a w");
3988        cx.assert_state("foo_ˇ_baz", Mode::Insert);
3989
3990        cx.set_state("fooˇBarBaz", Mode::Normal);
3991        cx.simulate_keystrokes("c a w");
3992        cx.assert_state("fooˇBaz", Mode::Insert);
3993
3994        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3995        cx.simulate_keystrokes("d a w");
3996        cx.assert_state("foo_ˇ_baz", Mode::Normal);
3997
3998        cx.set_state("fooˇBarBaz", Mode::Normal);
3999        cx.simulate_keystrokes("d a w");
4000        cx.assert_state("fooˇBaz", Mode::Normal);
4001    }
4002}
4003
Served at tenant.openagents/omega Member data and write actions are omitted.