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