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