1use std::ops::Range;
2
3use crate::{
4 motion::right,
5 state::{Mode, Operator},
6 Vim,
7};
8use editor::{
9 display_map::{DisplaySnapshot, ToDisplayPoint},
10 movement::{self, FindRange},
11 Bias, DisplayPoint, Editor,
12};
13use gpui::{actions, impl_actions, Window};
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 DoubleQuotes,
32 VerticalBars,
33 AnyBrackets,
34 Parentheses,
35 SquareBrackets,
36 CurlyBrackets,
37 AngleBrackets,
38 Argument,
39 IndentObj { include_below: bool },
40 Tag,
41 Method,
42 Class,
43 Comment,
44 EntireFile,
45}
46
47#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
48#[serde(deny_unknown_fields)]
49struct Word {
50 #[serde(default)]
51 ignore_punctuation: bool,
52}
53
54#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
55#[serde(deny_unknown_fields)]
56struct Subword {
57 #[serde(default)]
58 ignore_punctuation: bool,
59}
60#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
61#[serde(deny_unknown_fields)]
62struct IndentObj {
63 #[serde(default)]
64 include_below: bool,
65}
66
67impl_actions!(vim, [Word, Subword, IndentObj]);
68
69actions!(
70 vim,
71 [
72 Sentence,
73 Paragraph,
74 Quotes,
75 BackQuotes,
76 AnyQuotes,
77 DoubleQuotes,
78 VerticalBars,
79 Parentheses,
80 AnyBrackets,
81 SquareBrackets,
82 CurlyBrackets,
83 AngleBrackets,
84 Argument,
85 Tag,
86 Method,
87 Class,
88 Comment,
89 EntireFile
90 ]
91);
92
93pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
94 Vim::action(
95 editor,
96 cx,
97 |vim, &Word { ignore_punctuation }: &Word, window, cx| {
98 vim.object(Object::Word { ignore_punctuation }, window, cx)
99 },
100 );
101 Vim::action(
102 editor,
103 cx,
104 |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
105 vim.object(Object::Subword { ignore_punctuation }, window, cx)
106 },
107 );
108 Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
109 vim.object(Object::Tag, window, cx)
110 });
111 Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
112 vim.object(Object::Sentence, window, cx)
113 });
114 Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
115 vim.object(Object::Paragraph, window, cx)
116 });
117 Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
118 vim.object(Object::Quotes, window, cx)
119 });
120 Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
121 vim.object(Object::AnyQuotes, window, cx)
122 });
123 Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
124 vim.object(Object::AnyBrackets, window, cx)
125 });
126 Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
127 vim.object(Object::DoubleQuotes, window, cx)
128 });
129 Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
130 vim.object(Object::DoubleQuotes, window, cx)
131 });
132 Vim::action(editor, cx, |vim, _: &Parentheses, window, cx| {
133 vim.object(Object::Parentheses, window, cx)
134 });
135 Vim::action(editor, cx, |vim, _: &SquareBrackets, window, cx| {
136 vim.object(Object::SquareBrackets, window, cx)
137 });
138 Vim::action(editor, cx, |vim, _: &CurlyBrackets, window, cx| {
139 vim.object(Object::CurlyBrackets, window, cx)
140 });
141 Vim::action(editor, cx, |vim, _: &AngleBrackets, window, cx| {
142 vim.object(Object::AngleBrackets, window, cx)
143 });
144 Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
145 vim.object(Object::VerticalBars, window, cx)
146 });
147 Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
148 vim.object(Object::Argument, window, cx)
149 });
150 Vim::action(editor, cx, |vim, _: &Method, window, cx| {
151 vim.object(Object::Method, window, cx)
152 });
153 Vim::action(editor, cx, |vim, _: &Class, window, cx| {
154 vim.object(Object::Class, window, cx)
155 });
156 Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
157 vim.object(Object::EntireFile, window, cx)
158 });
159 Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
160 if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
161 vim.push_operator(Operator::Object { around: true }, window, cx);
162 }
163 vim.object(Object::Comment, window, cx)
164 });
165 Vim::action(
166 editor,
167 cx,
168 |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
169 vim.object(Object::IndentObj { include_below }, window, cx)
170 },
171 );
172}
173
174impl Vim {
175 fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
176 match self.mode {
177 Mode::Normal => self.normal_object(object, window, cx),
178 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
179 self.visual_object(object, window, cx)
180 }
181 Mode::Insert | Mode::Replace | Mode::HelixNormal => {
182 // Shouldn't execute a text object in insert mode. Ignoring
183 }
184 }
185 }
186}
187
188impl Object {
189 pub fn is_multiline(self) -> bool {
190 match self {
191 Object::Word { .. }
192 | Object::Subword { .. }
193 | Object::Quotes
194 | Object::BackQuotes
195 | Object::AnyQuotes
196 | Object::VerticalBars
197 | Object::DoubleQuotes => false,
198 Object::Sentence
199 | Object::Paragraph
200 | Object::AnyBrackets
201 | Object::Parentheses
202 | Object::Tag
203 | Object::AngleBrackets
204 | Object::CurlyBrackets
205 | Object::SquareBrackets
206 | Object::Argument
207 | Object::Method
208 | Object::Class
209 | Object::EntireFile
210 | Object::Comment
211 | Object::IndentObj { .. } => true,
212 }
213 }
214
215 pub fn always_expands_both_ways(self) -> bool {
216 match self {
217 Object::Word { .. }
218 | Object::Subword { .. }
219 | Object::Sentence
220 | Object::Paragraph
221 | Object::Argument
222 | Object::IndentObj { .. } => false,
223 Object::Quotes
224 | Object::BackQuotes
225 | Object::AnyQuotes
226 | Object::DoubleQuotes
227 | Object::VerticalBars
228 | Object::AnyBrackets
229 | Object::Parentheses
230 | Object::SquareBrackets
231 | Object::Tag
232 | Object::Method
233 | Object::Class
234 | Object::Comment
235 | Object::EntireFile
236 | Object::CurlyBrackets
237 | Object::AngleBrackets => true,
238 }
239 }
240
241 pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
242 match self {
243 Object::Word { .. }
244 | Object::Subword { .. }
245 | Object::Sentence
246 | Object::Quotes
247 | Object::AnyQuotes
248 | Object::BackQuotes
249 | Object::DoubleQuotes => {
250 if current_mode == Mode::VisualBlock {
251 Mode::VisualBlock
252 } else {
253 Mode::Visual
254 }
255 }
256 Object::Parentheses
257 | Object::AnyBrackets
258 | Object::SquareBrackets
259 | Object::CurlyBrackets
260 | Object::AngleBrackets
261 | Object::VerticalBars
262 | Object::Tag
263 | Object::Comment
264 | Object::Argument
265 | Object::IndentObj { .. } => Mode::Visual,
266 Object::Method | Object::Class => {
267 if around {
268 Mode::VisualLine
269 } else {
270 Mode::Visual
271 }
272 }
273 Object::Paragraph | Object::EntireFile => Mode::VisualLine,
274 }
275 }
276
277 pub fn range(
278 self,
279 map: &DisplaySnapshot,
280 selection: Selection<DisplayPoint>,
281 around: bool,
282 ) -> Option<Range<DisplayPoint>> {
283 let relative_to = selection.head();
284 match self {
285 Object::Word { ignore_punctuation } => {
286 if around {
287 around_word(map, relative_to, ignore_punctuation)
288 } else {
289 in_word(map, relative_to, ignore_punctuation)
290 }
291 }
292 Object::Subword { ignore_punctuation } => {
293 if around {
294 around_subword(map, relative_to, ignore_punctuation)
295 } else {
296 in_subword(map, relative_to, ignore_punctuation)
297 }
298 }
299 Object::Sentence => sentence(map, relative_to, around),
300 Object::Paragraph => paragraph(map, relative_to, around),
301 Object::Quotes => {
302 surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
303 }
304 Object::BackQuotes => {
305 surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
306 }
307 Object::AnyQuotes => {
308 let quote_types = ['\'', '"', '`']; // Types of quotes to handle
309 let relative_offset = relative_to.to_offset(map, Bias::Left) as isize;
310
311 // Find the closest matching quote range
312 quote_types
313 .iter()
314 .flat_map(|"e| {
315 // Get ranges for each quote type
316 surrounding_markers(
317 map,
318 relative_to,
319 around,
320 self.is_multiline(),
321 quote,
322 quote,
323 )
324 })
325 .min_by_key(|range| calculate_range_distance(range, relative_offset, map))
326 }
327 Object::DoubleQuotes => {
328 surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
329 }
330 Object::VerticalBars => {
331 surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
332 }
333 Object::Parentheses => {
334 surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
335 }
336 Object::Tag => {
337 let head = selection.head();
338 let range = selection.range();
339 surrounding_html_tag(map, head, range, around)
340 }
341 Object::AnyBrackets => {
342 let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
343 let relative_offset = relative_to.to_offset(map, Bias::Left) as isize;
344
345 bracket_pairs
346 .iter()
347 .flat_map(|&(open_bracket, close_bracket)| {
348 surrounding_markers(
349 map,
350 relative_to,
351 around,
352 self.is_multiline(),
353 open_bracket,
354 close_bracket,
355 )
356 })
357 .min_by_key(|range| calculate_range_distance(range, relative_offset, map))
358 }
359 Object::SquareBrackets => {
360 surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
361 }
362 Object::CurlyBrackets => {
363 surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
364 }
365 Object::AngleBrackets => {
366 surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
367 }
368 Object::Method => text_object(
369 map,
370 relative_to,
371 if around {
372 TextObject::AroundFunction
373 } else {
374 TextObject::InsideFunction
375 },
376 ),
377 Object::Comment => text_object(
378 map,
379 relative_to,
380 if around {
381 TextObject::AroundComment
382 } else {
383 TextObject::InsideComment
384 },
385 ),
386 Object::Class => text_object(
387 map,
388 relative_to,
389 if around {
390 TextObject::AroundClass
391 } else {
392 TextObject::InsideClass
393 },
394 ),
395 Object::Argument => argument(map, relative_to, around),
396 Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
397 Object::EntireFile => entire_file(map),
398 }
399 }
400
401 pub fn expand_selection(
402 self,
403 map: &DisplaySnapshot,
404 selection: &mut Selection<DisplayPoint>,
405 around: bool,
406 ) -> bool {
407 if let Some(range) = self.range(map, selection.clone(), around) {
408 selection.start = range.start;
409 selection.end = range.end;
410 if !around && self.is_multiline() {
411 preserve_indented_newline(map, selection);
412 }
413 true
414 } else {
415 false
416 }
417 }
418}
419
420/// Returns a range without the final newline char.
421///
422/// If the selection spans multiple lines and is preceded by an opening brace (`{`),
423/// this function will trim the selection to exclude the final newline
424/// in order to preserve a properly indented line.
425fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
426 let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map));
427
428 if start_point.row == end_point.row {
429 return;
430 }
431
432 let start_offset = selection.start.to_offset(map, Bias::Left);
433 let mut pos = start_offset;
434
435 while pos > 0 {
436 pos -= 1;
437 let current_char = map.buffer_chars_at(pos).next().map(|(ch, _)| ch);
438
439 match current_char {
440 Some(ch) if !ch.is_whitespace() => break,
441 Some('\n') if pos > 0 => {
442 let prev_char = map.buffer_chars_at(pos - 1).next().map(|(ch, _)| ch);
443 if prev_char == Some('{') {
444 let end_pos = selection.end.to_offset(map, Bias::Left);
445 for (ch, offset) in map.reverse_buffer_chars_at(end_pos) {
446 match ch {
447 '\n' => {
448 selection.end = offset.to_display_point(map);
449 break;
450 }
451 ch if !ch.is_whitespace() => break,
452 _ => continue,
453 }
454 }
455 }
456 break;
457 }
458 _ => continue,
459 }
460 }
461}
462
463/// Returns a range that surrounds the word `relative_to` is in.
464///
465/// If `relative_to` is at the start of a word, return the word.
466/// If `relative_to` is between words, return the space between.
467fn in_word(
468 map: &DisplaySnapshot,
469 relative_to: DisplayPoint,
470 ignore_punctuation: bool,
471) -> Option<Range<DisplayPoint>> {
472 // Use motion::right so that we consider the character under the cursor when looking for the start
473 let classifier = map
474 .buffer_snapshot
475 .char_classifier_at(relative_to.to_point(map))
476 .ignore_punctuation(ignore_punctuation);
477 let start = movement::find_preceding_boundary_display_point(
478 map,
479 right(map, relative_to, 1),
480 movement::FindRange::SingleLine,
481 |left, right| classifier.kind(left) != classifier.kind(right),
482 );
483
484 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
485 classifier.kind(left) != classifier.kind(right)
486 });
487
488 Some(start..end)
489}
490
491fn in_subword(
492 map: &DisplaySnapshot,
493 relative_to: DisplayPoint,
494 ignore_punctuation: bool,
495) -> Option<Range<DisplayPoint>> {
496 let offset = relative_to.to_offset(map, Bias::Left);
497 // Use motion::right so that we consider the character under the cursor when looking for the start
498 let classifier = map
499 .buffer_snapshot
500 .char_classifier_at(relative_to.to_point(map))
501 .ignore_punctuation(ignore_punctuation);
502 let in_subword = map
503 .buffer_chars_at(offset)
504 .next()
505 .map(|(c, _)| {
506 if classifier.is_word('-') {
507 !classifier.is_whitespace(c) && c != '_' && c != '-'
508 } else {
509 !classifier.is_whitespace(c) && c != '_'
510 }
511 })
512 .unwrap_or(false);
513
514 let start = if in_subword {
515 movement::find_preceding_boundary_display_point(
516 map,
517 right(map, relative_to, 1),
518 movement::FindRange::SingleLine,
519 |left, right| {
520 let is_word_start = classifier.kind(left) != classifier.kind(right);
521 let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
522 || left == '_' && right != '_'
523 || left.is_lowercase() && right.is_uppercase();
524 is_word_start || is_subword_start
525 },
526 )
527 } else {
528 movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
529 let is_word_start = classifier.kind(left) != classifier.kind(right);
530 let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
531 || left == '_' && right != '_'
532 || left.is_lowercase() && right.is_uppercase();
533 is_word_start || is_subword_start
534 })
535 };
536
537 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
538 let is_word_end = classifier.kind(left) != classifier.kind(right);
539 let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
540 || left != '_' && right == '_'
541 || left.is_lowercase() && right.is_uppercase();
542 is_word_end || is_subword_end
543 });
544
545 Some(start..end)
546}
547
548pub fn surrounding_html_tag(
549 map: &DisplaySnapshot,
550 head: DisplayPoint,
551 range: Range<DisplayPoint>,
552 around: bool,
553) -> Option<Range<DisplayPoint>> {
554 fn read_tag(chars: impl Iterator<Item = char>) -> String {
555 chars
556 .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
557 .collect()
558 }
559 fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
560 if Some('<') != chars.next() {
561 return None;
562 }
563 Some(read_tag(chars))
564 }
565 fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
566 if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
567 return None;
568 }
569 Some(read_tag(chars))
570 }
571
572 let snapshot = &map.buffer_snapshot;
573 let offset = head.to_offset(map, Bias::Left);
574 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
575 let buffer = excerpt.buffer();
576 let offset = excerpt.map_offset_to_buffer(offset);
577
578 // Find the most closest to current offset
579 let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
580 let mut last_child_node = cursor.node();
581 while cursor.goto_first_child_for_byte(offset).is_some() {
582 last_child_node = cursor.node();
583 }
584
585 let mut last_child_node = Some(last_child_node);
586 while let Some(cur_node) = last_child_node {
587 if cur_node.child_count() >= 2 {
588 let first_child = cur_node.child(0);
589 let last_child = cur_node.child(cur_node.child_count() - 1);
590 if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
591 let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
592 let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
593 // It needs to be handled differently according to the selection length
594 let is_valid = if range.end.to_offset(map, Bias::Left)
595 - range.start.to_offset(map, Bias::Left)
596 <= 1
597 {
598 offset <= last_child.end_byte()
599 } else {
600 range.start.to_offset(map, Bias::Left) >= first_child.start_byte()
601 && range.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
602 };
603 if open_tag.is_some() && open_tag == close_tag && is_valid {
604 let range = if around {
605 first_child.byte_range().start..last_child.byte_range().end
606 } else {
607 first_child.byte_range().end..last_child.byte_range().start
608 };
609 if excerpt.contains_buffer_range(range.clone()) {
610 let result = excerpt.map_range_from_buffer(range);
611 return Some(
612 result.start.to_display_point(map)..result.end.to_display_point(map),
613 );
614 }
615 }
616 }
617 }
618 last_child_node = cur_node.parent();
619 }
620 None
621}
622
623/// Returns a range that surrounds the word and following whitespace
624/// relative_to is in.
625///
626/// If `relative_to` is at the start of a word, return the word and following whitespace.
627/// If `relative_to` is between words, return the whitespace back and the following word.
628///
629/// if in word
630/// delete that word
631/// if there is whitespace following the word, delete that as well
632/// otherwise, delete any preceding whitespace
633/// otherwise
634/// delete whitespace around cursor
635/// delete word following the cursor
636fn around_word(
637 map: &DisplaySnapshot,
638 relative_to: DisplayPoint,
639 ignore_punctuation: bool,
640) -> Option<Range<DisplayPoint>> {
641 let offset = relative_to.to_offset(map, Bias::Left);
642 let classifier = map
643 .buffer_snapshot
644 .char_classifier_at(offset)
645 .ignore_punctuation(ignore_punctuation);
646 let in_word = map
647 .buffer_chars_at(offset)
648 .next()
649 .map(|(c, _)| !classifier.is_whitespace(c))
650 .unwrap_or(false);
651
652 if in_word {
653 around_containing_word(map, relative_to, ignore_punctuation)
654 } else {
655 around_next_word(map, relative_to, ignore_punctuation)
656 }
657}
658
659/// Calculate distance between a range and a cursor position
660///
661/// Returns a score where:
662/// - Lower values indicate better matches
663/// - Range containing cursor gets priority (returns range length)
664/// - For non-containing ranges, uses minimum distance to boundaries as primary factor
665/// - Range length is used as secondary factor for tiebreaking
666fn calculate_range_distance(
667 range: &Range<DisplayPoint>,
668 cursor_offset: isize,
669 map: &DisplaySnapshot,
670) -> isize {
671 let start_offset = range.start.to_offset(map, Bias::Left) as isize;
672 let end_offset = range.end.to_offset(map, Bias::Right) as isize;
673 let range_length = end_offset - start_offset;
674
675 // If cursor is inside the range, return range length
676 if cursor_offset >= start_offset && cursor_offset <= end_offset {
677 return range_length;
678 }
679
680 // Calculate minimum distance to range boundaries
681 let start_distance = (cursor_offset - start_offset).abs();
682 let end_distance = (cursor_offset - end_offset).abs();
683 let min_distance = start_distance.min(end_distance);
684
685 // Use min_distance as primary factor, range_length as secondary
686 // Multiply by large number to ensure distance is primary factor
687 min_distance * 10000 + range_length
688}
689
690fn around_subword(
691 map: &DisplaySnapshot,
692 relative_to: DisplayPoint,
693 ignore_punctuation: bool,
694) -> Option<Range<DisplayPoint>> {
695 // Use motion::right so that we consider the character under the cursor when looking for the start
696 let classifier = map
697 .buffer_snapshot
698 .char_classifier_at(relative_to.to_point(map))
699 .ignore_punctuation(ignore_punctuation);
700 let start = movement::find_preceding_boundary_display_point(
701 map,
702 right(map, relative_to, 1),
703 movement::FindRange::SingleLine,
704 |left, right| {
705 let is_word_start = classifier.kind(left) != classifier.kind(right);
706 let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
707 || left != '_' && right == '_'
708 || left.is_lowercase() && right.is_uppercase();
709 is_word_start || is_subword_start
710 },
711 );
712
713 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
714 let is_word_end = classifier.kind(left) != classifier.kind(right);
715 let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
716 || left != '_' && right == '_'
717 || left.is_lowercase() && right.is_uppercase();
718 is_word_end || is_subword_end
719 });
720
721 Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
722}
723
724fn around_containing_word(
725 map: &DisplaySnapshot,
726 relative_to: DisplayPoint,
727 ignore_punctuation: bool,
728) -> Option<Range<DisplayPoint>> {
729 in_word(map, relative_to, ignore_punctuation)
730 .map(|range| expand_to_include_whitespace(map, range, true))
731}
732
733fn around_next_word(
734 map: &DisplaySnapshot,
735 relative_to: DisplayPoint,
736 ignore_punctuation: bool,
737) -> Option<Range<DisplayPoint>> {
738 let classifier = map
739 .buffer_snapshot
740 .char_classifier_at(relative_to.to_point(map))
741 .ignore_punctuation(ignore_punctuation);
742 // Get the start of the word
743 let start = movement::find_preceding_boundary_display_point(
744 map,
745 right(map, relative_to, 1),
746 FindRange::SingleLine,
747 |left, right| classifier.kind(left) != classifier.kind(right),
748 );
749
750 let mut word_found = false;
751 let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
752 let left_kind = classifier.kind(left);
753 let right_kind = classifier.kind(right);
754
755 let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
756
757 if right_kind != CharKind::Whitespace {
758 word_found = true;
759 }
760
761 found
762 });
763
764 Some(start..end)
765}
766
767fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
768 Some(DisplayPoint::zero()..map.max_point())
769}
770
771fn text_object(
772 map: &DisplaySnapshot,
773 relative_to: DisplayPoint,
774 target: TextObject,
775) -> Option<Range<DisplayPoint>> {
776 let snapshot = &map.buffer_snapshot;
777 let offset = relative_to.to_offset(map, Bias::Left);
778
779 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
780 let buffer = excerpt.buffer();
781 let offset = excerpt.map_offset_to_buffer(offset);
782
783 let mut matches: Vec<Range<usize>> = buffer
784 .text_object_ranges(offset..offset, TreeSitterOptions::default())
785 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
786 .collect();
787 matches.sort_by_key(|r| (r.end - r.start));
788 if let Some(buffer_range) = matches.first() {
789 let range = excerpt.map_range_from_buffer(buffer_range.clone());
790 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
791 }
792
793 let around = target.around()?;
794 let mut matches: Vec<Range<usize>> = buffer
795 .text_object_ranges(offset..offset, TreeSitterOptions::default())
796 .filter_map(|(r, m)| if m == around { Some(r) } else { None })
797 .collect();
798 matches.sort_by_key(|r| (r.end - r.start));
799 let around_range = matches.first()?;
800
801 let mut matches: Vec<Range<usize>> = buffer
802 .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
803 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
804 .collect();
805 matches.sort_by_key(|r| r.start);
806 if let Some(buffer_range) = matches.first() {
807 if !buffer_range.is_empty() {
808 let range = excerpt.map_range_from_buffer(buffer_range.clone());
809 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
810 }
811 }
812 let buffer_range = excerpt.map_range_from_buffer(around_range.clone());
813 return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
814}
815
816fn argument(
817 map: &DisplaySnapshot,
818 relative_to: DisplayPoint,
819 around: bool,
820) -> Option<Range<DisplayPoint>> {
821 let snapshot = &map.buffer_snapshot;
822 let offset = relative_to.to_offset(map, Bias::Left);
823
824 // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
825 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
826 let buffer = excerpt.buffer();
827
828 fn comma_delimited_range_at(
829 buffer: &BufferSnapshot,
830 mut offset: usize,
831 include_comma: bool,
832 ) -> Option<Range<usize>> {
833 // Seek to the first non-whitespace character
834 offset += buffer
835 .chars_at(offset)
836 .take_while(|c| c.is_whitespace())
837 .map(char::len_utf8)
838 .sum::<usize>();
839
840 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
841 // Filter out empty ranges
842 if open.end == close.start {
843 return false;
844 }
845
846 // If the cursor is outside the brackets, ignore them
847 if open.start == offset || close.end == offset {
848 return false;
849 }
850
851 // TODO: Is there any better way to filter out string brackets?
852 // Used to filter out string brackets
853 matches!(
854 buffer.chars_at(open.start).next(),
855 Some('(' | '[' | '{' | '<' | '|')
856 )
857 };
858
859 // Find the brackets containing the cursor
860 let (open_bracket, close_bracket) =
861 buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
862
863 let inner_bracket_range = open_bracket.end..close_bracket.start;
864
865 let layer = buffer.syntax_layer_at(offset)?;
866 let node = layer.node();
867 let mut cursor = node.walk();
868
869 // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
870 let mut parent_covers_bracket_range = false;
871 loop {
872 let node = cursor.node();
873 let range = node.byte_range();
874 let covers_bracket_range =
875 range.start == open_bracket.start && range.end == close_bracket.end;
876 if parent_covers_bracket_range && !covers_bracket_range {
877 break;
878 }
879 parent_covers_bracket_range = covers_bracket_range;
880
881 // Unable to find a child node with a parent that covers the bracket range, so no argument to select
882 cursor.goto_first_child_for_byte(offset)?;
883 }
884
885 let mut argument_node = cursor.node();
886
887 // If the child node is the open bracket, move to the next sibling.
888 if argument_node.byte_range() == open_bracket {
889 if !cursor.goto_next_sibling() {
890 return Some(inner_bracket_range);
891 }
892 argument_node = cursor.node();
893 }
894 // While the child node is the close bracket or a comma, move to the previous sibling
895 while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
896 if !cursor.goto_previous_sibling() {
897 return Some(inner_bracket_range);
898 }
899 argument_node = cursor.node();
900 if argument_node.byte_range() == open_bracket {
901 return Some(inner_bracket_range);
902 }
903 }
904
905 // The start and end of the argument range, defaulting to the start and end of the argument node
906 let mut start = argument_node.start_byte();
907 let mut end = argument_node.end_byte();
908
909 let mut needs_surrounding_comma = include_comma;
910
911 // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
912 // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
913 while cursor.goto_previous_sibling() {
914 let prev = cursor.node();
915
916 if prev.start_byte() < open_bracket.end {
917 start = open_bracket.end;
918 break;
919 } else if prev.kind() == "," {
920 if needs_surrounding_comma {
921 start = prev.start_byte();
922 needs_surrounding_comma = false;
923 }
924 break;
925 } else if prev.start_byte() < start {
926 start = prev.start_byte();
927 }
928 }
929
930 // Do the same for the end of the argument, extending to next comma or the end of the argument list
931 while cursor.goto_next_sibling() {
932 let next = cursor.node();
933
934 if next.end_byte() > close_bracket.start {
935 end = close_bracket.start;
936 break;
937 } else if next.kind() == "," {
938 if needs_surrounding_comma {
939 // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
940 if let Some(next_arg) = next.next_sibling() {
941 end = next_arg.start_byte();
942 } else {
943 end = next.end_byte();
944 }
945 }
946 break;
947 } else if next.end_byte() > end {
948 end = next.end_byte();
949 }
950 }
951
952 Some(start..end)
953 }
954
955 let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
956
957 if excerpt.contains_buffer_range(result.clone()) {
958 let result = excerpt.map_range_from_buffer(result);
959 Some(result.start.to_display_point(map)..result.end.to_display_point(map))
960 } else {
961 None
962 }
963}
964
965fn indent(
966 map: &DisplaySnapshot,
967 relative_to: DisplayPoint,
968 around: bool,
969 include_below: bool,
970) -> Option<Range<DisplayPoint>> {
971 let point = relative_to.to_point(map);
972 let row = point.row;
973
974 let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
975
976 // Loop backwards until we find a non-blank line with less indent
977 let mut start_row = row;
978 for prev_row in (0..row).rev() {
979 let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
980 if indent.is_line_empty() {
981 continue;
982 }
983 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
984 if around {
985 // When around is true, include the first line with less indent
986 start_row = prev_row;
987 }
988 break;
989 }
990 start_row = prev_row;
991 }
992
993 // Loop forwards until we find a non-blank line with less indent
994 let mut end_row = row;
995 let max_rows = map.buffer_snapshot.max_row().0;
996 for next_row in (row + 1)..=max_rows {
997 let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
998 if indent.is_line_empty() {
999 continue;
1000 }
1001 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1002 if around && include_below {
1003 // When around is true and including below, include this line
1004 end_row = next_row;
1005 }
1006 break;
1007 }
1008 end_row = next_row;
1009 }
1010
1011 let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
1012 let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
1013 let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
1014 Some(start..end)
1015}
1016
1017fn sentence(
1018 map: &DisplaySnapshot,
1019 relative_to: DisplayPoint,
1020 around: bool,
1021) -> Option<Range<DisplayPoint>> {
1022 let mut start = None;
1023 let relative_offset = relative_to.to_offset(map, Bias::Left);
1024 let mut previous_end = relative_offset;
1025
1026 let mut chars = map.buffer_chars_at(previous_end).peekable();
1027
1028 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
1029 for (char, offset) in chars
1030 .peek()
1031 .cloned()
1032 .into_iter()
1033 .chain(map.reverse_buffer_chars_at(previous_end))
1034 {
1035 if is_sentence_end(map, offset) {
1036 break;
1037 }
1038
1039 if is_possible_sentence_start(char) {
1040 start = Some(offset);
1041 }
1042
1043 previous_end = offset;
1044 }
1045
1046 // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1047 let mut end = relative_offset;
1048 for (char, offset) in chars {
1049 if start.is_none() && is_possible_sentence_start(char) {
1050 if around {
1051 start = Some(offset);
1052 continue;
1053 } else {
1054 end = offset;
1055 break;
1056 }
1057 }
1058
1059 if char != '\n' {
1060 end = offset + char.len_utf8();
1061 }
1062
1063 if is_sentence_end(map, end) {
1064 break;
1065 }
1066 }
1067
1068 let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1069 if around {
1070 range = expand_to_include_whitespace(map, range, false);
1071 }
1072
1073 Some(range)
1074}
1075
1076fn is_possible_sentence_start(character: char) -> bool {
1077 !character.is_whitespace() && character != '.'
1078}
1079
1080const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1081const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1082const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1083fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
1084 let mut next_chars = map.buffer_chars_at(offset).peekable();
1085 if let Some((char, _)) = next_chars.next() {
1086 // We are at a double newline. This position is a sentence end.
1087 if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1088 return true;
1089 }
1090
1091 // The next text is not a valid whitespace. This is not a sentence end
1092 if !SENTENCE_END_WHITESPACE.contains(&char) {
1093 return false;
1094 }
1095 }
1096
1097 for (char, _) in map.reverse_buffer_chars_at(offset) {
1098 if SENTENCE_END_PUNCTUATION.contains(&char) {
1099 return true;
1100 }
1101
1102 if !SENTENCE_END_FILLERS.contains(&char) {
1103 return false;
1104 }
1105 }
1106
1107 false
1108}
1109
1110/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1111/// whitespace to the end first and falls back to the start if there was none.
1112fn expand_to_include_whitespace(
1113 map: &DisplaySnapshot,
1114 range: Range<DisplayPoint>,
1115 stop_at_newline: bool,
1116) -> Range<DisplayPoint> {
1117 let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1118 let mut whitespace_included = false;
1119
1120 let chars = map.buffer_chars_at(range.end).peekable();
1121 for (char, offset) in chars {
1122 if char == '\n' && stop_at_newline {
1123 break;
1124 }
1125
1126 if char.is_whitespace() {
1127 if char != '\n' {
1128 range.end = offset + char.len_utf8();
1129 whitespace_included = true;
1130 }
1131 } else {
1132 // Found non whitespace. Quit out.
1133 break;
1134 }
1135 }
1136
1137 if !whitespace_included {
1138 for (char, point) in map.reverse_buffer_chars_at(range.start) {
1139 if char == '\n' && stop_at_newline {
1140 break;
1141 }
1142
1143 if !char.is_whitespace() {
1144 break;
1145 }
1146
1147 range.start = point;
1148 }
1149 }
1150
1151 range.start.to_display_point(map)..range.end.to_display_point(map)
1152}
1153
1154/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1155/// where `relative_to` is in. If `around`, principally returns the range ending
1156/// at the end of the next paragraph.
1157///
1158/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1159/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1160/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1161/// the trailing newline is not subject to subsequent operations).
1162///
1163/// Edge cases:
1164/// - If `around` and if the current paragraph is the last paragraph of the
1165/// file and is blank, then the selection results in an error.
1166/// - If `around` and if the current paragraph is the last paragraph of the
1167/// file and is not blank, then the returned range starts at the start of the
1168/// previous paragraph, if it exists.
1169fn paragraph(
1170 map: &DisplaySnapshot,
1171 relative_to: DisplayPoint,
1172 around: bool,
1173) -> Option<Range<DisplayPoint>> {
1174 let mut paragraph_start = start_of_paragraph(map, relative_to);
1175 let mut paragraph_end = end_of_paragraph(map, relative_to);
1176
1177 let paragraph_end_row = paragraph_end.row();
1178 let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1179 let point = relative_to.to_point(map);
1180 let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1181
1182 if around {
1183 if paragraph_ends_with_eof {
1184 if current_line_is_empty {
1185 return None;
1186 }
1187
1188 let paragraph_start_row = paragraph_start.row();
1189 if paragraph_start_row.0 != 0 {
1190 let previous_paragraph_last_line_start =
1191 DisplayPoint::new(paragraph_start_row - 1, 0);
1192 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1193 }
1194 } else {
1195 let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
1196 paragraph_end = end_of_paragraph(map, next_paragraph_start);
1197 }
1198 }
1199
1200 let range = paragraph_start..paragraph_end;
1201 Some(range)
1202}
1203
1204/// Returns a position of the start of the current paragraph, where a paragraph
1205/// is defined as a run of non-blank lines or a run of blank lines.
1206pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1207 let point = display_point.to_point(map);
1208 if point.row == 0 {
1209 return DisplayPoint::zero();
1210 }
1211
1212 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1213
1214 for row in (0..point.row).rev() {
1215 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1216 if blank != is_current_line_blank {
1217 return Point::new(row + 1, 0).to_display_point(map);
1218 }
1219 }
1220
1221 DisplayPoint::zero()
1222}
1223
1224/// Returns a position of the end of the current paragraph, where a paragraph
1225/// is defined as a run of non-blank lines or a run of blank lines.
1226/// The trailing newline is excluded from the paragraph.
1227pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1228 let point = display_point.to_point(map);
1229 if point.row == map.buffer_snapshot.max_row().0 {
1230 return map.max_point();
1231 }
1232
1233 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1234
1235 for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
1236 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1237 if blank != is_current_line_blank {
1238 let previous_row = row - 1;
1239 return Point::new(
1240 previous_row,
1241 map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
1242 )
1243 .to_display_point(map);
1244 }
1245 }
1246
1247 map.max_point()
1248}
1249
1250fn surrounding_markers(
1251 map: &DisplaySnapshot,
1252 relative_to: DisplayPoint,
1253 around: bool,
1254 search_across_lines: bool,
1255 open_marker: char,
1256 close_marker: char,
1257) -> Option<Range<DisplayPoint>> {
1258 let point = relative_to.to_offset(map, Bias::Left);
1259
1260 let mut matched_closes = 0;
1261 let mut opening = None;
1262
1263 let mut before_ch = match movement::chars_before(map, point).next() {
1264 Some((ch, _)) => ch,
1265 _ => '\0',
1266 };
1267 if let Some((ch, range)) = movement::chars_after(map, point).next() {
1268 if ch == open_marker && before_ch != '\\' {
1269 if open_marker == close_marker {
1270 let mut total = 0;
1271 for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
1272 {
1273 if ch == '\n' {
1274 break;
1275 }
1276 if ch == open_marker && before_ch != '\\' {
1277 total += 1;
1278 }
1279 }
1280 if total % 2 == 0 {
1281 opening = Some(range)
1282 }
1283 } else {
1284 opening = Some(range)
1285 }
1286 }
1287 }
1288
1289 if opening.is_none() {
1290 let mut chars_before = movement::chars_before(map, point).peekable();
1291 while let Some((ch, range)) = chars_before.next() {
1292 if ch == '\n' && !search_across_lines {
1293 break;
1294 }
1295
1296 if let Some((before_ch, _)) = chars_before.peek() {
1297 if *before_ch == '\\' {
1298 continue;
1299 }
1300 }
1301
1302 if ch == open_marker {
1303 if matched_closes == 0 {
1304 opening = Some(range);
1305 break;
1306 }
1307 matched_closes -= 1;
1308 } else if ch == close_marker {
1309 matched_closes += 1
1310 }
1311 }
1312 }
1313 if opening.is_none() {
1314 for (ch, range) in movement::chars_after(map, point) {
1315 if before_ch != '\\' {
1316 if ch == open_marker {
1317 opening = Some(range);
1318 break;
1319 } else if ch == close_marker {
1320 break;
1321 }
1322 }
1323
1324 before_ch = ch;
1325 }
1326 }
1327
1328 let mut opening = opening?;
1329
1330 let mut matched_opens = 0;
1331 let mut closing = None;
1332 before_ch = match movement::chars_before(map, opening.end).next() {
1333 Some((ch, _)) => ch,
1334 _ => '\0',
1335 };
1336 for (ch, range) in movement::chars_after(map, opening.end) {
1337 if ch == '\n' && !search_across_lines {
1338 break;
1339 }
1340
1341 if before_ch != '\\' {
1342 if ch == close_marker {
1343 if matched_opens == 0 {
1344 closing = Some(range);
1345 break;
1346 }
1347 matched_opens -= 1;
1348 } else if ch == open_marker {
1349 matched_opens += 1;
1350 }
1351 }
1352
1353 before_ch = ch;
1354 }
1355
1356 let mut closing = closing?;
1357
1358 if around && !search_across_lines {
1359 let mut found = false;
1360
1361 for (ch, range) in movement::chars_after(map, closing.end) {
1362 if ch.is_whitespace() && ch != '\n' {
1363 found = true;
1364 closing.end = range.end;
1365 } else {
1366 break;
1367 }
1368 }
1369
1370 if !found {
1371 for (ch, range) in movement::chars_before(map, opening.start) {
1372 if ch.is_whitespace() && ch != '\n' {
1373 opening.start = range.start
1374 } else {
1375 break;
1376 }
1377 }
1378 }
1379 }
1380
1381 if !around && search_across_lines {
1382 // Handle trailing newline after opening
1383 if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
1384 if ch == '\n' {
1385 opening.end = range.end;
1386
1387 // After newline, skip leading whitespace
1388 let mut chars = movement::chars_after(map, opening.end).peekable();
1389 while let Some((ch, range)) = chars.peek() {
1390 if !ch.is_whitespace() {
1391 break;
1392 }
1393 opening.end = range.end;
1394 chars.next();
1395 }
1396 }
1397 }
1398
1399 // Handle leading whitespace before closing
1400 let mut last_newline_end = None;
1401 for (ch, range) in movement::chars_before(map, closing.start) {
1402 if !ch.is_whitespace() {
1403 break;
1404 }
1405 if ch == '\n' {
1406 last_newline_end = Some(range.end);
1407 break;
1408 }
1409 }
1410 // Adjust closing.start to exclude whitespace after a newline, if present
1411 if let Some(end) = last_newline_end {
1412 closing.start = end;
1413 }
1414 }
1415
1416 let result = if around {
1417 opening.start..closing.end
1418 } else {
1419 opening.end..closing.start
1420 };
1421
1422 Some(
1423 map.clip_point(result.start.to_display_point(map), Bias::Left)
1424 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1425 )
1426}
1427
1428#[cfg(test)]
1429mod test {
1430 use gpui::KeyBinding;
1431 use indoc::indoc;
1432
1433 use crate::{
1434 object::AnyBrackets,
1435 state::Mode,
1436 test::{NeovimBackedTestContext, VimTestContext},
1437 };
1438
1439 const WORD_LOCATIONS: &str = indoc! {"
1440 The quick ˇbrowˇnˇ•••
1441 fox ˇjuˇmpsˇ over
1442 the lazy dogˇ••
1443 ˇ
1444 ˇ
1445 ˇ
1446 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1447 ˇ••
1448 ˇ••
1449 ˇ fox-jumpˇs over
1450 the lazy dogˇ•
1451 ˇ
1452 "
1453 };
1454
1455 #[gpui::test]
1456 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1457 let mut cx = NeovimBackedTestContext::new(cx).await;
1458
1459 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1460 .await
1461 .assert_matches();
1462 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1463 .await
1464 .assert_matches();
1465 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1466 .await
1467 .assert_matches();
1468 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1469 .await
1470 .assert_matches();
1471 }
1472
1473 #[gpui::test]
1474 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1475 let mut cx = NeovimBackedTestContext::new(cx).await;
1476
1477 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1478 .await
1479 .assert_matches();
1480 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1481 .await
1482 .assert_matches();
1483 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1484 .await
1485 .assert_matches();
1486 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1487 .await
1488 .assert_matches();
1489 }
1490
1491 #[gpui::test]
1492 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1493 let mut cx = NeovimBackedTestContext::new(cx).await;
1494
1495 /*
1496 cx.set_shared_state("The quick ˇbrown\nfox").await;
1497 cx.simulate_shared_keystrokes(["v"]).await;
1498 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1499 cx.simulate_shared_keystrokes(["i", "w"]).await;
1500 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1501 */
1502 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1503 cx.simulate_shared_keystrokes("v").await;
1504 cx.shared_state()
1505 .await
1506 .assert_eq("The quick brown\n«\nˇ»fox");
1507 cx.simulate_shared_keystrokes("i w").await;
1508 cx.shared_state()
1509 .await
1510 .assert_eq("The quick brown\n«\nˇ»fox");
1511
1512 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1513 .await
1514 .assert_matches();
1515 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1516 .await
1517 .assert_matches();
1518 }
1519
1520 const PARAGRAPH_EXAMPLES: &[&str] = &[
1521 // Single line
1522 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1523 // Multiple lines without empty lines
1524 indoc! {"
1525 ˇThe quick brownˇ
1526 ˇfox jumps overˇ
1527 the lazy dog.ˇ
1528 "},
1529 // Heading blank paragraph and trailing normal paragraph
1530 indoc! {"
1531 ˇ
1532 ˇ
1533 ˇThe quick brown fox jumps
1534 ˇover the lazy dog.
1535 ˇ
1536 ˇ
1537 ˇThe quick brown fox jumpsˇ
1538 ˇover the lazy dog.ˇ
1539 "},
1540 // Inserted blank paragraph and trailing blank paragraph
1541 indoc! {"
1542 ˇThe quick brown fox jumps
1543 ˇover the lazy dog.
1544 ˇ
1545 ˇ
1546 ˇ
1547 ˇThe quick brown fox jumpsˇ
1548 ˇover the lazy dog.ˇ
1549 ˇ
1550 ˇ
1551 ˇ
1552 "},
1553 // "Blank" paragraph with whitespace characters
1554 indoc! {"
1555 ˇThe quick brown fox jumps
1556 over the lazy dog.
1557
1558 ˇ \t
1559
1560 ˇThe quick brown fox jumps
1561 over the lazy dog.ˇ
1562 ˇ
1563 ˇ \t
1564 \t \t
1565 "},
1566 // Single line "paragraphs", where selection size might be zero.
1567 indoc! {"
1568 ˇThe quick brown fox jumps over the lazy dog.
1569 ˇ
1570 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1571 ˇ
1572 "},
1573 ];
1574
1575 #[gpui::test]
1576 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1577 let mut cx = NeovimBackedTestContext::new(cx).await;
1578
1579 for paragraph_example in PARAGRAPH_EXAMPLES {
1580 cx.simulate_at_each_offset("c i p", paragraph_example)
1581 .await
1582 .assert_matches();
1583 cx.simulate_at_each_offset("c a p", paragraph_example)
1584 .await
1585 .assert_matches();
1586 }
1587 }
1588
1589 #[gpui::test]
1590 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1591 let mut cx = NeovimBackedTestContext::new(cx).await;
1592
1593 for paragraph_example in PARAGRAPH_EXAMPLES {
1594 cx.simulate_at_each_offset("d i p", paragraph_example)
1595 .await
1596 .assert_matches();
1597 cx.simulate_at_each_offset("d a p", paragraph_example)
1598 .await
1599 .assert_matches();
1600 }
1601 }
1602
1603 #[gpui::test]
1604 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1605 let mut cx = NeovimBackedTestContext::new(cx).await;
1606
1607 const EXAMPLES: &[&str] = &[
1608 indoc! {"
1609 ˇThe quick brown
1610 fox jumps over
1611 the lazy dog.
1612 "},
1613 indoc! {"
1614 ˇ
1615
1616 ˇThe quick brown fox jumps
1617 over the lazy dog.
1618 ˇ
1619
1620 ˇThe quick brown fox jumps
1621 over the lazy dog.
1622 "},
1623 indoc! {"
1624 ˇThe quick brown fox jumps over the lazy dog.
1625 ˇ
1626 ˇThe quick brown fox jumps over the lazy dog.
1627
1628 "},
1629 ];
1630
1631 for paragraph_example in EXAMPLES {
1632 cx.simulate_at_each_offset("v i p", paragraph_example)
1633 .await
1634 .assert_matches();
1635 cx.simulate_at_each_offset("v a p", paragraph_example)
1636 .await
1637 .assert_matches();
1638 }
1639 }
1640
1641 // Test string with "`" for opening surrounders and "'" for closing surrounders
1642 const SURROUNDING_MARKER_STRING: &str = indoc! {"
1643 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1644 'ˇfox juˇmps ov`ˇer
1645 the ˇlazy d'o`ˇg"};
1646
1647 const SURROUNDING_OBJECTS: &[(char, char)] = &[
1648 ('"', '"'), // Double Quote
1649 ('(', ')'), // Parentheses
1650 ];
1651
1652 #[gpui::test]
1653 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1654 let mut cx = NeovimBackedTestContext::new(cx).await;
1655
1656 for (start, end) in SURROUNDING_OBJECTS {
1657 let marked_string = SURROUNDING_MARKER_STRING
1658 .replace('`', &start.to_string())
1659 .replace('\'', &end.to_string());
1660
1661 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1662 .await
1663 .assert_matches();
1664 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1665 .await
1666 .assert_matches();
1667 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1668 .await
1669 .assert_matches();
1670 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1671 .await
1672 .assert_matches();
1673 }
1674 }
1675 #[gpui::test]
1676 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1677 let mut cx = NeovimBackedTestContext::new(cx).await;
1678 cx.set_shared_wrap(12).await;
1679
1680 cx.set_shared_state(indoc! {
1681 "\"ˇhello world\"!"
1682 })
1683 .await;
1684 cx.simulate_shared_keystrokes("v i \"").await;
1685 cx.shared_state().await.assert_eq(indoc! {
1686 "\"«hello worldˇ»\"!"
1687 });
1688
1689 cx.set_shared_state(indoc! {
1690 "\"hˇello world\"!"
1691 })
1692 .await;
1693 cx.simulate_shared_keystrokes("v i \"").await;
1694 cx.shared_state().await.assert_eq(indoc! {
1695 "\"«hello worldˇ»\"!"
1696 });
1697
1698 cx.set_shared_state(indoc! {
1699 "helˇlo \"world\"!"
1700 })
1701 .await;
1702 cx.simulate_shared_keystrokes("v i \"").await;
1703 cx.shared_state().await.assert_eq(indoc! {
1704 "hello \"«worldˇ»\"!"
1705 });
1706
1707 cx.set_shared_state(indoc! {
1708 "hello \"wˇorld\"!"
1709 })
1710 .await;
1711 cx.simulate_shared_keystrokes("v i \"").await;
1712 cx.shared_state().await.assert_eq(indoc! {
1713 "hello \"«worldˇ»\"!"
1714 });
1715
1716 cx.set_shared_state(indoc! {
1717 "hello \"wˇorld\"!"
1718 })
1719 .await;
1720 cx.simulate_shared_keystrokes("v a \"").await;
1721 cx.shared_state().await.assert_eq(indoc! {
1722 "hello« \"world\"ˇ»!"
1723 });
1724
1725 cx.set_shared_state(indoc! {
1726 "hello \"wˇorld\" !"
1727 })
1728 .await;
1729 cx.simulate_shared_keystrokes("v a \"").await;
1730 cx.shared_state().await.assert_eq(indoc! {
1731 "hello «\"world\" ˇ»!"
1732 });
1733
1734 cx.set_shared_state(indoc! {
1735 "hello \"wˇorld\"•
1736 goodbye"
1737 })
1738 .await;
1739 cx.simulate_shared_keystrokes("v a \"").await;
1740 cx.shared_state().await.assert_eq(indoc! {
1741 "hello «\"world\" ˇ»
1742 goodbye"
1743 });
1744 }
1745
1746 #[gpui::test]
1747 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1748 let mut cx = VimTestContext::new(cx, true).await;
1749
1750 cx.set_state(
1751 indoc! {
1752 "func empty(a string) bool {
1753 if a == \"\" {
1754 return true
1755 }
1756 ˇreturn false
1757 }"
1758 },
1759 Mode::Normal,
1760 );
1761 cx.simulate_keystrokes("v i {");
1762
1763 cx.set_state(
1764 indoc! {
1765 "func empty(a string) bool {
1766 if a == \"\" {
1767 ˇreturn true
1768 }
1769 return false
1770 }"
1771 },
1772 Mode::Normal,
1773 );
1774 cx.simulate_keystrokes("v i {");
1775
1776 cx.set_state(
1777 indoc! {
1778 "func empty(a string) bool {
1779 if a == \"\" ˇ{
1780 return true
1781 }
1782 return false
1783 }"
1784 },
1785 Mode::Normal,
1786 );
1787 cx.simulate_keystrokes("v i {");
1788 }
1789
1790 #[gpui::test]
1791 async fn test_singleline_surrounding_character_objects_with_escape(
1792 cx: &mut gpui::TestAppContext,
1793 ) {
1794 let mut cx = NeovimBackedTestContext::new(cx).await;
1795 cx.set_shared_state(indoc! {
1796 "h\"e\\\"lˇlo \\\"world\"!"
1797 })
1798 .await;
1799 cx.simulate_shared_keystrokes("v i \"").await;
1800 cx.shared_state().await.assert_eq(indoc! {
1801 "h\"«e\\\"llo \\\"worldˇ»\"!"
1802 });
1803
1804 cx.set_shared_state(indoc! {
1805 "hello \"teˇst \\\"inside\\\" world\""
1806 })
1807 .await;
1808 cx.simulate_shared_keystrokes("v i \"").await;
1809 cx.shared_state().await.assert_eq(indoc! {
1810 "hello \"«test \\\"inside\\\" worldˇ»\""
1811 });
1812 }
1813
1814 #[gpui::test]
1815 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1816 let mut cx = VimTestContext::new(cx, true).await;
1817 cx.set_state(
1818 indoc! {"
1819 fn boop() {
1820 baz(ˇ|a, b| { bar(|j, k| { })})
1821 }"
1822 },
1823 Mode::Normal,
1824 );
1825 cx.simulate_keystrokes("c i |");
1826 cx.assert_state(
1827 indoc! {"
1828 fn boop() {
1829 baz(|ˇ| { bar(|j, k| { })})
1830 }"
1831 },
1832 Mode::Insert,
1833 );
1834 cx.simulate_keystrokes("escape 1 8 |");
1835 cx.assert_state(
1836 indoc! {"
1837 fn boop() {
1838 baz(|| { bar(ˇ|j, k| { })})
1839 }"
1840 },
1841 Mode::Normal,
1842 );
1843
1844 cx.simulate_keystrokes("v a |");
1845 cx.assert_state(
1846 indoc! {"
1847 fn boop() {
1848 baz(|| { bar(«|j, k| ˇ»{ })})
1849 }"
1850 },
1851 Mode::Visual,
1852 );
1853 }
1854
1855 #[gpui::test]
1856 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1857 let mut cx = VimTestContext::new(cx, true).await;
1858
1859 // Generic arguments
1860 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1861 cx.simulate_keystrokes("v i a");
1862 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1863
1864 // Function arguments
1865 cx.set_state(
1866 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1867 Mode::Normal,
1868 );
1869 cx.simulate_keystrokes("d a a");
1870 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1871
1872 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1873 cx.simulate_keystrokes("v a a");
1874 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1875
1876 // Tuple, vec, and array arguments
1877 cx.set_state(
1878 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1879 Mode::Normal,
1880 );
1881 cx.simulate_keystrokes("c i a");
1882 cx.assert_state(
1883 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1884 Mode::Insert,
1885 );
1886
1887 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1888 cx.simulate_keystrokes("c a a");
1889 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1890
1891 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1892 cx.simulate_keystrokes("c i a");
1893 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1894
1895 cx.set_state(
1896 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1897 Mode::Normal,
1898 );
1899 cx.simulate_keystrokes("c a a");
1900 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1901
1902 // Cursor immediately before / after brackets
1903 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1904 cx.simulate_keystrokes("v i a");
1905 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1906
1907 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1908 cx.simulate_keystrokes("v i a");
1909 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1910 }
1911
1912 #[gpui::test]
1913 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
1914 let mut cx = VimTestContext::new(cx, true).await;
1915
1916 // Base use case
1917 cx.set_state(
1918 indoc! {"
1919 fn boop() {
1920 // Comment
1921 baz();ˇ
1922
1923 loop {
1924 bar(1);
1925 bar(2);
1926 }
1927
1928 result
1929 }
1930 "},
1931 Mode::Normal,
1932 );
1933 cx.simulate_keystrokes("v i i");
1934 cx.assert_state(
1935 indoc! {"
1936 fn boop() {
1937 « // Comment
1938 baz();
1939
1940 loop {
1941 bar(1);
1942 bar(2);
1943 }
1944
1945 resultˇ»
1946 }
1947 "},
1948 Mode::Visual,
1949 );
1950
1951 // Around indent (include line above)
1952 cx.set_state(
1953 indoc! {"
1954 const ABOVE: str = true;
1955 fn boop() {
1956
1957 hello();
1958 worˇld()
1959 }
1960 "},
1961 Mode::Normal,
1962 );
1963 cx.simulate_keystrokes("v a i");
1964 cx.assert_state(
1965 indoc! {"
1966 const ABOVE: str = true;
1967 «fn boop() {
1968
1969 hello();
1970 world()ˇ»
1971 }
1972 "},
1973 Mode::Visual,
1974 );
1975
1976 // Around indent (include line above & below)
1977 cx.set_state(
1978 indoc! {"
1979 const ABOVE: str = true;
1980 fn boop() {
1981 hellˇo();
1982 world()
1983
1984 }
1985 const BELOW: str = true;
1986 "},
1987 Mode::Normal,
1988 );
1989 cx.simulate_keystrokes("c a shift-i");
1990 cx.assert_state(
1991 indoc! {"
1992 const ABOVE: str = true;
1993 ˇ
1994 const BELOW: str = true;
1995 "},
1996 Mode::Insert,
1997 );
1998 }
1999
2000 #[gpui::test]
2001 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2002 let mut cx = NeovimBackedTestContext::new(cx).await;
2003
2004 for (start, end) in SURROUNDING_OBJECTS {
2005 let marked_string = SURROUNDING_MARKER_STRING
2006 .replace('`', &start.to_string())
2007 .replace('\'', &end.to_string());
2008
2009 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2010 .await
2011 .assert_matches();
2012 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2013 .await
2014 .assert_matches();
2015 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2016 .await
2017 .assert_matches();
2018 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2019 .await
2020 .assert_matches();
2021 }
2022 }
2023
2024 #[gpui::test]
2025 async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2026 let mut cx = VimTestContext::new(cx, true).await;
2027
2028 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2029 // Single quotes
2030 (
2031 "c i q",
2032 "Thisˇ is a 'quote' example.",
2033 "This is a 'ˇ' example.",
2034 Mode::Insert,
2035 ),
2036 (
2037 "c a q",
2038 "Thisˇ is a 'quote' example.",
2039 "This is a ˇexample.",
2040 Mode::Insert,
2041 ),
2042 (
2043 "c i q",
2044 "This is a \"simple 'qˇuote'\" example.",
2045 "This is a \"simple 'ˇ'\" example.",
2046 Mode::Insert,
2047 ),
2048 (
2049 "c a q",
2050 "This is a \"simple 'qˇuote'\" example.",
2051 "This is a \"simpleˇ\" example.",
2052 Mode::Insert,
2053 ),
2054 (
2055 "c i q",
2056 "This is a 'qˇuote' example.",
2057 "This is a 'ˇ' example.",
2058 Mode::Insert,
2059 ),
2060 (
2061 "c a q",
2062 "This is a 'qˇuote' example.",
2063 "This is a ˇexample.",
2064 Mode::Insert,
2065 ),
2066 (
2067 "d i q",
2068 "This is a 'qˇuote' example.",
2069 "This is a 'ˇ' example.",
2070 Mode::Normal,
2071 ),
2072 (
2073 "d a q",
2074 "This is a 'qˇuote' example.",
2075 "This is a ˇexample.",
2076 Mode::Normal,
2077 ),
2078 // Double quotes
2079 (
2080 "c i q",
2081 "This is a \"qˇuote\" example.",
2082 "This is a \"ˇ\" example.",
2083 Mode::Insert,
2084 ),
2085 (
2086 "c a q",
2087 "This is a \"qˇuote\" example.",
2088 "This is a ˇexample.",
2089 Mode::Insert,
2090 ),
2091 (
2092 "d i q",
2093 "This is a \"qˇuote\" example.",
2094 "This is a \"ˇ\" example.",
2095 Mode::Normal,
2096 ),
2097 (
2098 "d a q",
2099 "This is a \"qˇuote\" example.",
2100 "This is a ˇexample.",
2101 Mode::Normal,
2102 ),
2103 // Back quotes
2104 (
2105 "c i q",
2106 "This is a `qˇuote` example.",
2107 "This is a `ˇ` example.",
2108 Mode::Insert,
2109 ),
2110 (
2111 "c a q",
2112 "This is a `qˇuote` example.",
2113 "This is a ˇexample.",
2114 Mode::Insert,
2115 ),
2116 (
2117 "d i q",
2118 "This is a `qˇuote` example.",
2119 "This is a `ˇ` example.",
2120 Mode::Normal,
2121 ),
2122 (
2123 "d a q",
2124 "This is a `qˇuote` example.",
2125 "This is a ˇexample.",
2126 Mode::Normal,
2127 ),
2128 ];
2129
2130 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2131 cx.set_state(initial_state, Mode::Normal);
2132
2133 cx.simulate_keystrokes(keystrokes);
2134
2135 cx.assert_state(expected_state, *expected_mode);
2136 }
2137
2138 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2139 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2140 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2141 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2142 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2143 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2144 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2145 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2146 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2147 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2148 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2149 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2150 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2151 ];
2152
2153 for (keystrokes, initial_state, mode) in INVALID_CASES {
2154 cx.set_state(initial_state, Mode::Normal);
2155
2156 cx.simulate_keystrokes(keystrokes);
2157
2158 cx.assert_state(initial_state, *mode);
2159 }
2160 }
2161
2162 #[gpui::test]
2163 async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2164 let mut cx = VimTestContext::new(cx, true).await;
2165 cx.update(|_, cx| {
2166 cx.bind_keys([KeyBinding::new(
2167 "b",
2168 AnyBrackets,
2169 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2170 )]);
2171 });
2172
2173 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2174 // Bracket (Parentheses)
2175 (
2176 "c i b",
2177 "Thisˇ is a (simple [quote]) example.",
2178 "This is a (ˇ) example.",
2179 Mode::Insert,
2180 ),
2181 (
2182 "c i b",
2183 "This is a [simple (qˇuote)] example.",
2184 "This is a [simple (ˇ)] example.",
2185 Mode::Insert,
2186 ),
2187 (
2188 "c a b",
2189 "This is a [simple (qˇuote)] example.",
2190 "This is a [simple ˇ] example.",
2191 Mode::Insert,
2192 ),
2193 (
2194 "c a b",
2195 "Thisˇ is a (simple [quote]) example.",
2196 "This is a ˇ example.",
2197 Mode::Insert,
2198 ),
2199 (
2200 "c i b",
2201 "This is a (qˇuote) example.",
2202 "This is a (ˇ) example.",
2203 Mode::Insert,
2204 ),
2205 (
2206 "c a b",
2207 "This is a (qˇuote) example.",
2208 "This is a ˇ example.",
2209 Mode::Insert,
2210 ),
2211 (
2212 "d i b",
2213 "This is a (qˇuote) example.",
2214 "This is a (ˇ) example.",
2215 Mode::Normal,
2216 ),
2217 (
2218 "d a b",
2219 "This is a (qˇuote) example.",
2220 "This is a ˇ example.",
2221 Mode::Normal,
2222 ),
2223 // Square brackets
2224 (
2225 "c i b",
2226 "This is a [qˇuote] example.",
2227 "This is a [ˇ] example.",
2228 Mode::Insert,
2229 ),
2230 (
2231 "c a b",
2232 "This is a [qˇuote] example.",
2233 "This is a ˇ example.",
2234 Mode::Insert,
2235 ),
2236 (
2237 "d i b",
2238 "This is a [qˇuote] example.",
2239 "This is a [ˇ] example.",
2240 Mode::Normal,
2241 ),
2242 (
2243 "d a b",
2244 "This is a [qˇuote] example.",
2245 "This is a ˇ example.",
2246 Mode::Normal,
2247 ),
2248 // Curly brackets
2249 (
2250 "c i b",
2251 "This is a {qˇuote} example.",
2252 "This is a {ˇ} example.",
2253 Mode::Insert,
2254 ),
2255 (
2256 "c a b",
2257 "This is a {qˇuote} example.",
2258 "This is a ˇ example.",
2259 Mode::Insert,
2260 ),
2261 (
2262 "d i b",
2263 "This is a {qˇuote} example.",
2264 "This is a {ˇ} example.",
2265 Mode::Normal,
2266 ),
2267 (
2268 "d a b",
2269 "This is a {qˇuote} example.",
2270 "This is a ˇ example.",
2271 Mode::Normal,
2272 ),
2273 ];
2274
2275 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2276 cx.set_state(initial_state, Mode::Normal);
2277
2278 cx.simulate_keystrokes(keystrokes);
2279
2280 cx.assert_state(expected_state, *expected_mode);
2281 }
2282
2283 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2284 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2285 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2286 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2287 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2288 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2289 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2290 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2291 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2292 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2293 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2294 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2295 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2296 ];
2297
2298 for (keystrokes, initial_state, mode) in INVALID_CASES {
2299 cx.set_state(initial_state, Mode::Normal);
2300
2301 cx.simulate_keystrokes(keystrokes);
2302
2303 cx.assert_state(initial_state, *mode);
2304 }
2305 }
2306
2307 #[gpui::test]
2308 async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
2309 let mut cx = NeovimBackedTestContext::new(cx).await;
2310
2311 cx.set_shared_state("(trailingˇ whitespace )")
2312 .await;
2313 cx.simulate_shared_keystrokes("v i b").await;
2314 cx.shared_state().await.assert_matches();
2315 cx.simulate_shared_keystrokes("escape y i b").await;
2316 cx.shared_clipboard()
2317 .await
2318 .assert_eq("trailing whitespace ");
2319 }
2320
2321 #[gpui::test]
2322 async fn test_tags(cx: &mut gpui::TestAppContext) {
2323 let mut cx = VimTestContext::new_html(cx).await;
2324
2325 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
2326 cx.simulate_keystrokes("v i t");
2327 cx.assert_state(
2328 "<html><head></head><body><b>«hi!ˇ»</b></body>",
2329 Mode::Visual,
2330 );
2331 cx.simulate_keystrokes("a t");
2332 cx.assert_state(
2333 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2334 Mode::Visual,
2335 );
2336 cx.simulate_keystrokes("a t");
2337 cx.assert_state(
2338 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2339 Mode::Visual,
2340 );
2341
2342 // The cursor is before the tag
2343 cx.set_state(
2344 "<html><head></head><body> ˇ <b>hi!</b></body>",
2345 Mode::Normal,
2346 );
2347 cx.simulate_keystrokes("v i t");
2348 cx.assert_state(
2349 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
2350 Mode::Visual,
2351 );
2352 cx.simulate_keystrokes("a t");
2353 cx.assert_state(
2354 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
2355 Mode::Visual,
2356 );
2357
2358 // The cursor is in the open tag
2359 cx.set_state(
2360 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
2361 Mode::Normal,
2362 );
2363 cx.simulate_keystrokes("v a t");
2364 cx.assert_state(
2365 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
2366 Mode::Visual,
2367 );
2368 cx.simulate_keystrokes("i t");
2369 cx.assert_state(
2370 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
2371 Mode::Visual,
2372 );
2373
2374 // current selection length greater than 1
2375 cx.set_state(
2376 "<html><head></head><body><«b>hi!ˇ»</b></body>",
2377 Mode::Visual,
2378 );
2379 cx.simulate_keystrokes("i t");
2380 cx.assert_state(
2381 "<html><head></head><body><b>«hi!ˇ»</b></body>",
2382 Mode::Visual,
2383 );
2384 cx.simulate_keystrokes("a t");
2385 cx.assert_state(
2386 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2387 Mode::Visual,
2388 );
2389
2390 cx.set_state(
2391 "<html><head></head><body><«b>hi!</ˇ»b></body>",
2392 Mode::Visual,
2393 );
2394 cx.simulate_keystrokes("a t");
2395 cx.assert_state(
2396 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2397 Mode::Visual,
2398 );
2399 }
2400}