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