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