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