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