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