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