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