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