1use std::ops::Range;
2
3use crate::{motion::right, state::Mode, Vim};
4use editor::{
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement::{self, FindRange},
7 Bias, DisplayPoint, Editor,
8};
9
10use itertools::Itertools;
11
12use gpui::{actions, impl_actions, ViewContext};
13use language::{BufferSnapshot, CharKind, Point, Selection};
14use multi_buffer::MultiBufferRow;
15use serde::Deserialize;
16
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
18pub enum Object {
19 Word { ignore_punctuation: bool },
20 Sentence,
21 Paragraph,
22 Quotes,
23 BackQuotes,
24 DoubleQuotes,
25 VerticalBars,
26 Parentheses,
27 SquareBrackets,
28 CurlyBrackets,
29 AngleBrackets,
30 Argument,
31 IndentObj { include_below: bool },
32 Tag,
33}
34
35#[derive(Clone, Deserialize, PartialEq)]
36#[serde(rename_all = "camelCase")]
37struct Word {
38 #[serde(default)]
39 ignore_punctuation: bool,
40}
41#[derive(Clone, Deserialize, PartialEq)]
42#[serde(rename_all = "camelCase")]
43struct IndentObj {
44 #[serde(default)]
45 include_below: bool,
46}
47
48impl_actions!(vim, [Word, IndentObj]);
49
50actions!(
51 vim,
52 [
53 Sentence,
54 Paragraph,
55 Quotes,
56 BackQuotes,
57 DoubleQuotes,
58 VerticalBars,
59 Parentheses,
60 SquareBrackets,
61 CurlyBrackets,
62 AngleBrackets,
63 Argument,
64 Tag
65 ]
66);
67
68pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
69 Vim::action(
70 editor,
71 cx,
72 |vim, &Word { ignore_punctuation }: &Word, cx| {
73 vim.object(Object::Word { ignore_punctuation }, cx)
74 },
75 );
76 Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
77 Vim::action(editor, cx, |vim, _: &Sentence, cx| {
78 vim.object(Object::Sentence, cx)
79 });
80 Vim::action(editor, cx, |vim, _: &Paragraph, cx| {
81 vim.object(Object::Paragraph, cx)
82 });
83 Vim::action(editor, cx, |vim, _: &Quotes, cx| {
84 vim.object(Object::Quotes, cx)
85 });
86 Vim::action(editor, cx, |vim, _: &BackQuotes, cx| {
87 vim.object(Object::BackQuotes, cx)
88 });
89 Vim::action(editor, cx, |vim, _: &DoubleQuotes, cx| {
90 vim.object(Object::DoubleQuotes, cx)
91 });
92 Vim::action(editor, cx, |vim, _: &Parentheses, cx| {
93 vim.object(Object::Parentheses, cx)
94 });
95 Vim::action(editor, cx, |vim, _: &SquareBrackets, cx| {
96 vim.object(Object::SquareBrackets, cx)
97 });
98 Vim::action(editor, cx, |vim, _: &CurlyBrackets, cx| {
99 vim.object(Object::CurlyBrackets, cx)
100 });
101 Vim::action(editor, cx, |vim, _: &AngleBrackets, cx| {
102 vim.object(Object::AngleBrackets, cx)
103 });
104 Vim::action(editor, cx, |vim, _: &VerticalBars, cx| {
105 vim.object(Object::VerticalBars, cx)
106 });
107 Vim::action(editor, cx, |vim, _: &Argument, cx| {
108 vim.object(Object::Argument, cx)
109 });
110 Vim::action(
111 editor,
112 cx,
113 |vim, &IndentObj { include_below }: &IndentObj, cx| {
114 vim.object(Object::IndentObj { include_below }, cx)
115 },
116 );
117}
118
119impl Vim {
120 fn object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
121 match self.mode {
122 Mode::Normal => self.normal_object(object, cx),
123 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx),
124 Mode::Insert | Mode::Replace => {
125 // Shouldn't execute a text object in insert mode. Ignoring
126 }
127 }
128 }
129}
130
131impl Object {
132 pub fn is_multiline(self) -> bool {
133 match self {
134 Object::Word { .. }
135 | Object::Quotes
136 | Object::BackQuotes
137 | Object::VerticalBars
138 | Object::DoubleQuotes => false,
139 Object::Sentence
140 | Object::Paragraph
141 | Object::Parentheses
142 | Object::Tag
143 | Object::AngleBrackets
144 | Object::CurlyBrackets
145 | Object::SquareBrackets
146 | Object::Argument
147 | Object::IndentObj { .. } => true,
148 }
149 }
150
151 pub fn always_expands_both_ways(self) -> bool {
152 match self {
153 Object::Word { .. }
154 | Object::Sentence
155 | Object::Paragraph
156 | Object::Argument
157 | Object::IndentObj { .. } => false,
158 Object::Quotes
159 | Object::BackQuotes
160 | Object::DoubleQuotes
161 | Object::VerticalBars
162 | Object::Parentheses
163 | Object::SquareBrackets
164 | Object::Tag
165 | Object::CurlyBrackets
166 | Object::AngleBrackets => true,
167 }
168 }
169
170 pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
171 match self {
172 Object::Word { .. }
173 | Object::Sentence
174 | Object::Quotes
175 | Object::BackQuotes
176 | Object::DoubleQuotes => {
177 if current_mode == Mode::VisualBlock {
178 Mode::VisualBlock
179 } else {
180 Mode::Visual
181 }
182 }
183 Object::Parentheses
184 | Object::SquareBrackets
185 | Object::CurlyBrackets
186 | Object::AngleBrackets
187 | Object::VerticalBars
188 | Object::Tag
189 | Object::Argument
190 | Object::IndentObj { .. } => Mode::Visual,
191 Object::Paragraph => Mode::VisualLine,
192 }
193 }
194
195 pub fn range(
196 self,
197 map: &DisplaySnapshot,
198 selection: Selection<DisplayPoint>,
199 around: bool,
200 ) -> Option<Range<DisplayPoint>> {
201 let relative_to = selection.head();
202 match self {
203 Object::Word { ignore_punctuation } => {
204 if around {
205 around_word(map, relative_to, ignore_punctuation)
206 } else {
207 in_word(map, relative_to, ignore_punctuation)
208 }
209 }
210 Object::Sentence => sentence(map, relative_to, around),
211 Object::Paragraph => paragraph(map, relative_to, around),
212 Object::Quotes => {
213 surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
214 }
215 Object::BackQuotes => {
216 surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
217 }
218 Object::DoubleQuotes => {
219 surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
220 }
221 Object::VerticalBars => {
222 surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
223 }
224 Object::Parentheses => {
225 surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
226 }
227 Object::Tag => {
228 let head = selection.head();
229 let range = selection.range();
230 surrounding_html_tag(map, head, range, around)
231 }
232 Object::SquareBrackets => {
233 surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
234 }
235 Object::CurlyBrackets => {
236 surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
237 }
238 Object::AngleBrackets => {
239 surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
240 }
241 Object::Argument => argument(map, relative_to, around),
242 Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
243 }
244 }
245
246 pub fn expand_selection(
247 self,
248 map: &DisplaySnapshot,
249 selection: &mut Selection<DisplayPoint>,
250 around: bool,
251 ) -> bool {
252 if let Some(range) = self.range(map, selection.clone(), around) {
253 selection.start = range.start;
254 selection.end = range.end;
255 true
256 } else {
257 false
258 }
259 }
260}
261
262/// Returns a range that surrounds the word `relative_to` is in.
263///
264/// If `relative_to` is at the start of a word, return the word.
265/// If `relative_to` is between words, return the space between.
266fn in_word(
267 map: &DisplaySnapshot,
268 relative_to: DisplayPoint,
269 ignore_punctuation: bool,
270) -> Option<Range<DisplayPoint>> {
271 // Use motion::right so that we consider the character under the cursor when looking for the start
272 let classifier = map
273 .buffer_snapshot
274 .char_classifier_at(relative_to.to_point(map))
275 .ignore_punctuation(ignore_punctuation);
276 let start = movement::find_preceding_boundary_display_point(
277 map,
278 right(map, relative_to, 1),
279 movement::FindRange::SingleLine,
280 |left, right| classifier.kind(left) != classifier.kind(right),
281 );
282
283 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
284 classifier.kind(left) != classifier.kind(right)
285 });
286
287 Some(start..end)
288}
289
290pub fn surrounding_html_tag(
291 map: &DisplaySnapshot,
292 head: DisplayPoint,
293 range: Range<DisplayPoint>,
294 around: bool,
295) -> Option<Range<DisplayPoint>> {
296 fn read_tag(chars: impl Iterator<Item = char>) -> String {
297 chars
298 .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
299 .collect()
300 }
301 fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
302 if Some('<') != chars.next() {
303 return None;
304 }
305 Some(read_tag(chars))
306 }
307 fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
308 if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
309 return None;
310 }
311 Some(read_tag(chars))
312 }
313
314 let snapshot = &map.buffer_snapshot;
315 let offset = head.to_offset(map, Bias::Left);
316 let excerpt = snapshot.excerpt_containing(offset..offset)?;
317 let buffer = excerpt.buffer();
318 let offset = excerpt.map_offset_to_buffer(offset);
319
320 // Find the most closest to current offset
321 let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
322 let mut last_child_node = cursor.node();
323 while cursor.goto_first_child_for_byte(offset).is_some() {
324 last_child_node = cursor.node();
325 }
326
327 let mut last_child_node = Some(last_child_node);
328 while let Some(cur_node) = last_child_node {
329 if cur_node.child_count() >= 2 {
330 let first_child = cur_node.child(0);
331 let last_child = cur_node.child(cur_node.child_count() - 1);
332 if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
333 let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
334 let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
335 // It needs to be handled differently according to the selection length
336 let is_valid = if range.end.to_offset(map, Bias::Left)
337 - range.start.to_offset(map, Bias::Left)
338 <= 1
339 {
340 offset <= last_child.end_byte()
341 } else {
342 range.start.to_offset(map, Bias::Left) >= first_child.start_byte()
343 && range.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
344 };
345 if open_tag.is_some() && open_tag == close_tag && is_valid {
346 let range = if around {
347 first_child.byte_range().start..last_child.byte_range().end
348 } else {
349 first_child.byte_range().end..last_child.byte_range().start
350 };
351 if excerpt.contains_buffer_range(range.clone()) {
352 let result = excerpt.map_range_from_buffer(range);
353 return Some(
354 result.start.to_display_point(map)..result.end.to_display_point(map),
355 );
356 }
357 }
358 }
359 }
360 last_child_node = cur_node.parent();
361 }
362 None
363}
364
365/// Returns a range that surrounds the word and following whitespace
366/// relative_to is in.
367///
368/// If `relative_to` is at the start of a word, return the word and following whitespace.
369/// If `relative_to` is between words, return the whitespace back and the following word.
370///
371/// if in word
372/// delete that word
373/// if there is whitespace following the word, delete that as well
374/// otherwise, delete any preceding whitespace
375/// otherwise
376/// delete whitespace around cursor
377/// delete word following the cursor
378fn around_word(
379 map: &DisplaySnapshot,
380 relative_to: DisplayPoint,
381 ignore_punctuation: bool,
382) -> Option<Range<DisplayPoint>> {
383 let offset = relative_to.to_offset(map, Bias::Left);
384 let classifier = map
385 .buffer_snapshot
386 .char_classifier_at(offset)
387 .ignore_punctuation(ignore_punctuation);
388 let in_word = map
389 .buffer_chars_at(offset)
390 .next()
391 .map(|(c, _)| !classifier.is_whitespace(c))
392 .unwrap_or(false);
393
394 if in_word {
395 around_containing_word(map, relative_to, ignore_punctuation)
396 } else {
397 around_next_word(map, relative_to, ignore_punctuation)
398 }
399}
400
401fn around_containing_word(
402 map: &DisplaySnapshot,
403 relative_to: DisplayPoint,
404 ignore_punctuation: bool,
405) -> Option<Range<DisplayPoint>> {
406 in_word(map, relative_to, ignore_punctuation)
407 .map(|range| expand_to_include_whitespace(map, range, true))
408}
409
410fn around_next_word(
411 map: &DisplaySnapshot,
412 relative_to: DisplayPoint,
413 ignore_punctuation: bool,
414) -> Option<Range<DisplayPoint>> {
415 let classifier = map
416 .buffer_snapshot
417 .char_classifier_at(relative_to.to_point(map))
418 .ignore_punctuation(ignore_punctuation);
419 // Get the start of the word
420 let start = movement::find_preceding_boundary_display_point(
421 map,
422 right(map, relative_to, 1),
423 FindRange::SingleLine,
424 |left, right| classifier.kind(left) != classifier.kind(right),
425 );
426
427 let mut word_found = false;
428 let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
429 let left_kind = classifier.kind(left);
430 let right_kind = classifier.kind(right);
431
432 let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
433
434 if right_kind != CharKind::Whitespace {
435 word_found = true;
436 }
437
438 found
439 });
440
441 Some(start..end)
442}
443
444fn argument(
445 map: &DisplaySnapshot,
446 relative_to: DisplayPoint,
447 around: bool,
448) -> Option<Range<DisplayPoint>> {
449 let snapshot = &map.buffer_snapshot;
450 let offset = relative_to.to_offset(map, Bias::Left);
451
452 // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
453 let excerpt = snapshot.excerpt_containing(offset..offset)?;
454 let buffer = excerpt.buffer();
455
456 fn comma_delimited_range_at(
457 buffer: &BufferSnapshot,
458 mut offset: usize,
459 include_comma: bool,
460 ) -> Option<Range<usize>> {
461 // Seek to the first non-whitespace character
462 offset += buffer
463 .chars_at(offset)
464 .take_while(|c| c.is_whitespace())
465 .map(char::len_utf8)
466 .sum::<usize>();
467
468 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
469 // Filter out empty ranges
470 if open.end == close.start {
471 return false;
472 }
473
474 // If the cursor is outside the brackets, ignore them
475 if open.start == offset || close.end == offset {
476 return false;
477 }
478
479 // TODO: Is there any better way to filter out string brackets?
480 // Used to filter out string brackets
481 matches!(
482 buffer.chars_at(open.start).next(),
483 Some('(' | '[' | '{' | '<' | '|')
484 )
485 };
486
487 // Find the brackets containing the cursor
488 let (open_bracket, close_bracket) =
489 buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
490
491 let inner_bracket_range = open_bracket.end..close_bracket.start;
492
493 let layer = buffer.syntax_layer_at(offset)?;
494 let node = layer.node();
495 let mut cursor = node.walk();
496
497 // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
498 let mut parent_covers_bracket_range = false;
499 loop {
500 let node = cursor.node();
501 let range = node.byte_range();
502 let covers_bracket_range =
503 range.start == open_bracket.start && range.end == close_bracket.end;
504 if parent_covers_bracket_range && !covers_bracket_range {
505 break;
506 }
507 parent_covers_bracket_range = covers_bracket_range;
508
509 // Unable to find a child node with a parent that covers the bracket range, so no argument to select
510 cursor.goto_first_child_for_byte(offset)?;
511 }
512
513 let mut argument_node = cursor.node();
514
515 // If the child node is the open bracket, move to the next sibling.
516 if argument_node.byte_range() == open_bracket {
517 if !cursor.goto_next_sibling() {
518 return Some(inner_bracket_range);
519 }
520 argument_node = cursor.node();
521 }
522 // While the child node is the close bracket or a comma, move to the previous sibling
523 while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
524 if !cursor.goto_previous_sibling() {
525 return Some(inner_bracket_range);
526 }
527 argument_node = cursor.node();
528 if argument_node.byte_range() == open_bracket {
529 return Some(inner_bracket_range);
530 }
531 }
532
533 // The start and end of the argument range, defaulting to the start and end of the argument node
534 let mut start = argument_node.start_byte();
535 let mut end = argument_node.end_byte();
536
537 let mut needs_surrounding_comma = include_comma;
538
539 // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
540 // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
541 while cursor.goto_previous_sibling() {
542 let prev = cursor.node();
543
544 if prev.start_byte() < open_bracket.end {
545 start = open_bracket.end;
546 break;
547 } else if prev.kind() == "," {
548 if needs_surrounding_comma {
549 start = prev.start_byte();
550 needs_surrounding_comma = false;
551 }
552 break;
553 } else if prev.start_byte() < start {
554 start = prev.start_byte();
555 }
556 }
557
558 // Do the same for the end of the argument, extending to next comma or the end of the argument list
559 while cursor.goto_next_sibling() {
560 let next = cursor.node();
561
562 if next.end_byte() > close_bracket.start {
563 end = close_bracket.start;
564 break;
565 } else if next.kind() == "," {
566 if needs_surrounding_comma {
567 // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
568 if let Some(next_arg) = next.next_sibling() {
569 end = next_arg.start_byte();
570 } else {
571 end = next.end_byte();
572 }
573 }
574 break;
575 } else if next.end_byte() > end {
576 end = next.end_byte();
577 }
578 }
579
580 Some(start..end)
581 }
582
583 let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
584
585 if excerpt.contains_buffer_range(result.clone()) {
586 let result = excerpt.map_range_from_buffer(result);
587 Some(result.start.to_display_point(map)..result.end.to_display_point(map))
588 } else {
589 None
590 }
591}
592
593fn indent(
594 map: &DisplaySnapshot,
595 relative_to: DisplayPoint,
596 around: bool,
597 include_below: bool,
598) -> Option<Range<DisplayPoint>> {
599 let point = relative_to.to_point(map);
600 let row = point.row;
601
602 let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
603
604 // Loop backwards until we find a non-blank line with less indent
605 let mut start_row = row;
606 for prev_row in (0..row).rev() {
607 let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
608 if indent.is_line_empty() {
609 continue;
610 }
611 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
612 if around {
613 // When around is true, include the first line with less indent
614 start_row = prev_row;
615 }
616 break;
617 }
618 start_row = prev_row;
619 }
620
621 // Loop forwards until we find a non-blank line with less indent
622 let mut end_row = row;
623 let max_rows = map.max_buffer_row().0;
624 for next_row in (row + 1)..=max_rows {
625 let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
626 if indent.is_line_empty() {
627 continue;
628 }
629 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
630 if around && include_below {
631 // When around is true and including below, include this line
632 end_row = next_row;
633 }
634 break;
635 }
636 end_row = next_row;
637 }
638
639 let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
640 let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
641 let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
642 Some(start..end)
643}
644
645fn sentence(
646 map: &DisplaySnapshot,
647 relative_to: DisplayPoint,
648 around: bool,
649) -> Option<Range<DisplayPoint>> {
650 let mut start = None;
651 let relative_offset = relative_to.to_offset(map, Bias::Left);
652 let mut previous_end = relative_offset;
653
654 let mut chars = map.buffer_chars_at(previous_end).peekable();
655
656 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
657 for (char, offset) in chars
658 .peek()
659 .cloned()
660 .into_iter()
661 .chain(map.reverse_buffer_chars_at(previous_end))
662 {
663 if is_sentence_end(map, offset) {
664 break;
665 }
666
667 if is_possible_sentence_start(char) {
668 start = Some(offset);
669 }
670
671 previous_end = offset;
672 }
673
674 // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
675 let mut end = relative_offset;
676 for (char, offset) in chars {
677 if start.is_none() && is_possible_sentence_start(char) {
678 if around {
679 start = Some(offset);
680 continue;
681 } else {
682 end = offset;
683 break;
684 }
685 }
686
687 if char != '\n' {
688 end = offset + char.len_utf8();
689 }
690
691 if is_sentence_end(map, end) {
692 break;
693 }
694 }
695
696 let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
697 if around {
698 range = expand_to_include_whitespace(map, range, false);
699 }
700
701 Some(range)
702}
703
704fn is_possible_sentence_start(character: char) -> bool {
705 !character.is_whitespace() && character != '.'
706}
707
708const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
709const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
710const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
711fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
712 let mut next_chars = map.buffer_chars_at(offset).peekable();
713 if let Some((char, _)) = next_chars.next() {
714 // We are at a double newline. This position is a sentence end.
715 if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
716 return true;
717 }
718
719 // The next text is not a valid whitespace. This is not a sentence end
720 if !SENTENCE_END_WHITESPACE.contains(&char) {
721 return false;
722 }
723 }
724
725 for (char, _) in map.reverse_buffer_chars_at(offset) {
726 if SENTENCE_END_PUNCTUATION.contains(&char) {
727 return true;
728 }
729
730 if !SENTENCE_END_FILLERS.contains(&char) {
731 return false;
732 }
733 }
734
735 false
736}
737
738/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
739/// whitespace to the end first and falls back to the start if there was none.
740fn expand_to_include_whitespace(
741 map: &DisplaySnapshot,
742 range: Range<DisplayPoint>,
743 stop_at_newline: bool,
744) -> Range<DisplayPoint> {
745 let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
746 let mut whitespace_included = false;
747
748 let chars = map.buffer_chars_at(range.end).peekable();
749 for (char, offset) in chars {
750 if char == '\n' && stop_at_newline {
751 break;
752 }
753
754 if char.is_whitespace() {
755 if char != '\n' {
756 range.end = offset + char.len_utf8();
757 whitespace_included = true;
758 }
759 } else {
760 // Found non whitespace. Quit out.
761 break;
762 }
763 }
764
765 if !whitespace_included {
766 for (char, point) in map.reverse_buffer_chars_at(range.start) {
767 if char == '\n' && stop_at_newline {
768 break;
769 }
770
771 if !char.is_whitespace() {
772 break;
773 }
774
775 range.start = point;
776 }
777 }
778
779 range.start.to_display_point(map)..range.end.to_display_point(map)
780}
781
782/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
783/// where `relative_to` is in. If `around`, principally returns the range ending
784/// at the end of the next paragraph.
785///
786/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
787/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
788/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
789/// the trailing newline is not subject to subsequent operations).
790///
791/// Edge cases:
792/// - If `around` and if the current paragraph is the last paragraph of the
793/// file and is blank, then the selection results in an error.
794/// - If `around` and if the current paragraph is the last paragraph of the
795/// file and is not blank, then the returned range starts at the start of the
796/// previous paragraph, if it exists.
797fn paragraph(
798 map: &DisplaySnapshot,
799 relative_to: DisplayPoint,
800 around: bool,
801) -> Option<Range<DisplayPoint>> {
802 let mut paragraph_start = start_of_paragraph(map, relative_to);
803 let mut paragraph_end = end_of_paragraph(map, relative_to);
804
805 let paragraph_end_row = paragraph_end.row();
806 let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
807 let point = relative_to.to_point(map);
808 let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
809
810 if around {
811 if paragraph_ends_with_eof {
812 if current_line_is_empty {
813 return None;
814 }
815
816 let paragraph_start_row = paragraph_start.row();
817 if paragraph_start_row.0 != 0 {
818 let previous_paragraph_last_line_start =
819 DisplayPoint::new(paragraph_start_row - 1, 0);
820 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
821 }
822 } else {
823 let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
824 paragraph_end = end_of_paragraph(map, next_paragraph_start);
825 }
826 }
827
828 let range = paragraph_start..paragraph_end;
829 Some(range)
830}
831
832/// Returns a position of the start of the current paragraph, where a paragraph
833/// is defined as a run of non-blank lines or a run of blank lines.
834pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
835 let point = display_point.to_point(map);
836 if point.row == 0 {
837 return DisplayPoint::zero();
838 }
839
840 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
841
842 for row in (0..point.row).rev() {
843 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
844 if blank != is_current_line_blank {
845 return Point::new(row + 1, 0).to_display_point(map);
846 }
847 }
848
849 DisplayPoint::zero()
850}
851
852/// Returns a position of the end of the current paragraph, where a paragraph
853/// is defined as a run of non-blank lines or a run of blank lines.
854/// The trailing newline is excluded from the paragraph.
855pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
856 let point = display_point.to_point(map);
857 if point.row == map.max_buffer_row().0 {
858 return map.max_point();
859 }
860
861 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
862
863 for row in point.row + 1..map.max_buffer_row().0 + 1 {
864 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
865 if blank != is_current_line_blank {
866 let previous_row = row - 1;
867 return Point::new(
868 previous_row,
869 map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
870 )
871 .to_display_point(map);
872 }
873 }
874
875 map.max_point()
876}
877
878fn surrounding_markers(
879 map: &DisplaySnapshot,
880 relative_to: DisplayPoint,
881 around: bool,
882 search_across_lines: bool,
883 open_marker: char,
884 close_marker: char,
885) -> Option<Range<DisplayPoint>> {
886 let point = relative_to.to_offset(map, Bias::Left);
887
888 let mut matched_closes = 0;
889 let mut opening = None;
890
891 let mut before_ch = match movement::chars_before(map, point).next() {
892 Some((ch, _)) => ch,
893 _ => '\0',
894 };
895 if let Some((ch, range)) = movement::chars_after(map, point).next() {
896 if ch == open_marker && before_ch != '\\' {
897 if open_marker == close_marker {
898 let mut total = 0;
899 for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
900 {
901 if ch == '\n' {
902 break;
903 }
904 if ch == open_marker && before_ch != '\\' {
905 total += 1;
906 }
907 }
908 if total % 2 == 0 {
909 opening = Some(range)
910 }
911 } else {
912 opening = Some(range)
913 }
914 }
915 }
916
917 if opening.is_none() {
918 let mut chars_before = movement::chars_before(map, point).peekable();
919 while let Some((ch, range)) = chars_before.next() {
920 if ch == '\n' && !search_across_lines {
921 break;
922 }
923
924 if let Some((before_ch, _)) = chars_before.peek() {
925 if *before_ch == '\\' {
926 continue;
927 }
928 }
929
930 if ch == open_marker {
931 if matched_closes == 0 {
932 opening = Some(range);
933 break;
934 }
935 matched_closes -= 1;
936 } else if ch == close_marker {
937 matched_closes += 1
938 }
939 }
940 }
941 if opening.is_none() {
942 for (ch, range) in movement::chars_after(map, point) {
943 if before_ch != '\\' {
944 if ch == open_marker {
945 opening = Some(range);
946 break;
947 } else if ch == close_marker {
948 break;
949 }
950 }
951
952 before_ch = ch;
953 }
954 }
955
956 let mut opening = opening?;
957
958 let mut matched_opens = 0;
959 let mut closing = None;
960 before_ch = match movement::chars_before(map, opening.end).next() {
961 Some((ch, _)) => ch,
962 _ => '\0',
963 };
964 for (ch, range) in movement::chars_after(map, opening.end) {
965 if ch == '\n' && !search_across_lines {
966 break;
967 }
968
969 if before_ch != '\\' {
970 if ch == close_marker {
971 if matched_opens == 0 {
972 closing = Some(range);
973 break;
974 }
975 matched_opens -= 1;
976 } else if ch == open_marker {
977 matched_opens += 1;
978 }
979 }
980
981 before_ch = ch;
982 }
983
984 let mut closing = closing?;
985
986 if around && !search_across_lines {
987 let mut found = false;
988
989 for (ch, range) in movement::chars_after(map, closing.end) {
990 if ch.is_whitespace() && ch != '\n' {
991 found = true;
992 closing.end = range.end;
993 } else {
994 break;
995 }
996 }
997
998 if !found {
999 for (ch, range) in movement::chars_before(map, opening.start) {
1000 if ch.is_whitespace() && ch != '\n' {
1001 opening.start = range.start
1002 } else {
1003 break;
1004 }
1005 }
1006 }
1007 }
1008
1009 if !around && search_across_lines {
1010 if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
1011 if ch == '\n' {
1012 opening.end = range.end
1013 }
1014 }
1015
1016 for (ch, range) in movement::chars_before(map, closing.start) {
1017 if !ch.is_whitespace() {
1018 break;
1019 }
1020 if ch != '\n' {
1021 closing.start = range.start
1022 }
1023 }
1024 }
1025
1026 let result = if around {
1027 opening.start..closing.end
1028 } else {
1029 opening.end..closing.start
1030 };
1031
1032 Some(
1033 map.clip_point(result.start.to_display_point(map), Bias::Left)
1034 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1035 )
1036}
1037
1038#[cfg(test)]
1039mod test {
1040 use indoc::indoc;
1041
1042 use crate::{
1043 state::Mode,
1044 test::{NeovimBackedTestContext, VimTestContext},
1045 };
1046
1047 const WORD_LOCATIONS: &str = indoc! {"
1048 The quick ˇbrowˇnˇ•••
1049 fox ˇjuˇmpsˇ over
1050 the lazy dogˇ••
1051 ˇ
1052 ˇ
1053 ˇ
1054 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1055 ˇ••
1056 ˇ••
1057 ˇ fox-jumpˇs over
1058 the lazy dogˇ•
1059 ˇ
1060 "
1061 };
1062
1063 #[gpui::test]
1064 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1065 let mut cx = NeovimBackedTestContext::new(cx).await;
1066
1067 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1068 .await
1069 .assert_matches();
1070 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1071 .await
1072 .assert_matches();
1073 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1074 .await
1075 .assert_matches();
1076 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1077 .await
1078 .assert_matches();
1079 }
1080
1081 #[gpui::test]
1082 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1083 let mut cx = NeovimBackedTestContext::new(cx).await;
1084
1085 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1086 .await
1087 .assert_matches();
1088 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1089 .await
1090 .assert_matches();
1091 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1092 .await
1093 .assert_matches();
1094 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1095 .await
1096 .assert_matches();
1097 }
1098
1099 #[gpui::test]
1100 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1101 let mut cx = NeovimBackedTestContext::new(cx).await;
1102
1103 /*
1104 cx.set_shared_state("The quick ˇbrown\nfox").await;
1105 cx.simulate_shared_keystrokes(["v"]).await;
1106 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1107 cx.simulate_shared_keystrokes(["i", "w"]).await;
1108 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1109 */
1110 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1111 cx.simulate_shared_keystrokes("v").await;
1112 cx.shared_state()
1113 .await
1114 .assert_eq("The quick brown\n«\nˇ»fox");
1115 cx.simulate_shared_keystrokes("i w").await;
1116 cx.shared_state()
1117 .await
1118 .assert_eq("The quick brown\n«\nˇ»fox");
1119
1120 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1121 .await
1122 .assert_matches();
1123 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1124 .await
1125 .assert_matches();
1126 }
1127
1128 const PARAGRAPH_EXAMPLES: &[&str] = &[
1129 // Single line
1130 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1131 // Multiple lines without empty lines
1132 indoc! {"
1133 ˇThe quick brownˇ
1134 ˇfox jumps overˇ
1135 the lazy dog.ˇ
1136 "},
1137 // Heading blank paragraph and trailing normal paragraph
1138 indoc! {"
1139 ˇ
1140 ˇ
1141 ˇThe quick brown fox jumps
1142 ˇover the lazy dog.
1143 ˇ
1144 ˇ
1145 ˇThe quick brown fox jumpsˇ
1146 ˇover the lazy dog.ˇ
1147 "},
1148 // Inserted blank paragraph and trailing blank paragraph
1149 indoc! {"
1150 ˇThe quick brown fox jumps
1151 ˇover the lazy dog.
1152 ˇ
1153 ˇ
1154 ˇ
1155 ˇThe quick brown fox jumpsˇ
1156 ˇover the lazy dog.ˇ
1157 ˇ
1158 ˇ
1159 ˇ
1160 "},
1161 // "Blank" paragraph with whitespace characters
1162 indoc! {"
1163 ˇThe quick brown fox jumps
1164 over the lazy dog.
1165
1166 ˇ \t
1167
1168 ˇThe quick brown fox jumps
1169 over the lazy dog.ˇ
1170 ˇ
1171 ˇ \t
1172 \t \t
1173 "},
1174 // Single line "paragraphs", where selection size might be zero.
1175 indoc! {"
1176 ˇThe quick brown fox jumps over the lazy dog.
1177 ˇ
1178 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1179 ˇ
1180 "},
1181 ];
1182
1183 #[gpui::test]
1184 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1185 let mut cx = NeovimBackedTestContext::new(cx).await;
1186
1187 for paragraph_example in PARAGRAPH_EXAMPLES {
1188 cx.simulate_at_each_offset("c i p", paragraph_example)
1189 .await
1190 .assert_matches();
1191 cx.simulate_at_each_offset("c a p", paragraph_example)
1192 .await
1193 .assert_matches();
1194 }
1195 }
1196
1197 #[gpui::test]
1198 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1199 let mut cx = NeovimBackedTestContext::new(cx).await;
1200
1201 for paragraph_example in PARAGRAPH_EXAMPLES {
1202 cx.simulate_at_each_offset("d i p", paragraph_example)
1203 .await
1204 .assert_matches();
1205 cx.simulate_at_each_offset("d a p", paragraph_example)
1206 .await
1207 .assert_matches();
1208 }
1209 }
1210
1211 #[gpui::test]
1212 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1213 let mut cx = NeovimBackedTestContext::new(cx).await;
1214
1215 const EXAMPLES: &[&str] = &[
1216 indoc! {"
1217 ˇThe quick brown
1218 fox jumps over
1219 the lazy dog.
1220 "},
1221 indoc! {"
1222 ˇ
1223
1224 ˇThe quick brown fox jumps
1225 over the lazy dog.
1226 ˇ
1227
1228 ˇThe quick brown fox jumps
1229 over the lazy dog.
1230 "},
1231 indoc! {"
1232 ˇThe quick brown fox jumps over the lazy dog.
1233 ˇ
1234 ˇThe quick brown fox jumps over the lazy dog.
1235
1236 "},
1237 ];
1238
1239 for paragraph_example in EXAMPLES {
1240 cx.simulate_at_each_offset("v i p", paragraph_example)
1241 .await
1242 .assert_matches();
1243 cx.simulate_at_each_offset("v a p", paragraph_example)
1244 .await
1245 .assert_matches();
1246 }
1247 }
1248
1249 // Test string with "`" for opening surrounders and "'" for closing surrounders
1250 const SURROUNDING_MARKER_STRING: &str = indoc! {"
1251 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1252 'ˇfox juˇmps ov`ˇer
1253 the ˇlazy d'o`ˇg"};
1254
1255 const SURROUNDING_OBJECTS: &[(char, char)] = &[
1256 ('"', '"'), // Double Quote
1257 ('(', ')'), // Parentheses
1258 ];
1259
1260 #[gpui::test]
1261 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1262 let mut cx = NeovimBackedTestContext::new(cx).await;
1263
1264 for (start, end) in SURROUNDING_OBJECTS {
1265 let marked_string = SURROUNDING_MARKER_STRING
1266 .replace('`', &start.to_string())
1267 .replace('\'', &end.to_string());
1268
1269 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1270 .await
1271 .assert_matches();
1272 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1273 .await
1274 .assert_matches();
1275 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1276 .await
1277 .assert_matches();
1278 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1279 .await
1280 .assert_matches();
1281 }
1282 }
1283 #[gpui::test]
1284 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1285 let mut cx = NeovimBackedTestContext::new(cx).await;
1286 cx.set_shared_wrap(12).await;
1287
1288 cx.set_shared_state(indoc! {
1289 "\"ˇhello world\"!"
1290 })
1291 .await;
1292 cx.simulate_shared_keystrokes("v i \"").await;
1293 cx.shared_state().await.assert_eq(indoc! {
1294 "\"«hello worldˇ»\"!"
1295 });
1296
1297 cx.set_shared_state(indoc! {
1298 "\"hˇello world\"!"
1299 })
1300 .await;
1301 cx.simulate_shared_keystrokes("v i \"").await;
1302 cx.shared_state().await.assert_eq(indoc! {
1303 "\"«hello worldˇ»\"!"
1304 });
1305
1306 cx.set_shared_state(indoc! {
1307 "helˇlo \"world\"!"
1308 })
1309 .await;
1310 cx.simulate_shared_keystrokes("v i \"").await;
1311 cx.shared_state().await.assert_eq(indoc! {
1312 "hello \"«worldˇ»\"!"
1313 });
1314
1315 cx.set_shared_state(indoc! {
1316 "hello \"wˇorld\"!"
1317 })
1318 .await;
1319 cx.simulate_shared_keystrokes("v i \"").await;
1320 cx.shared_state().await.assert_eq(indoc! {
1321 "hello \"«worldˇ»\"!"
1322 });
1323
1324 cx.set_shared_state(indoc! {
1325 "hello \"wˇorld\"!"
1326 })
1327 .await;
1328 cx.simulate_shared_keystrokes("v a \"").await;
1329 cx.shared_state().await.assert_eq(indoc! {
1330 "hello« \"world\"ˇ»!"
1331 });
1332
1333 cx.set_shared_state(indoc! {
1334 "hello \"wˇorld\" !"
1335 })
1336 .await;
1337 cx.simulate_shared_keystrokes("v a \"").await;
1338 cx.shared_state().await.assert_eq(indoc! {
1339 "hello «\"world\" ˇ»!"
1340 });
1341
1342 cx.set_shared_state(indoc! {
1343 "hello \"wˇorld\"•
1344 goodbye"
1345 })
1346 .await;
1347 cx.simulate_shared_keystrokes("v a \"").await;
1348 cx.shared_state().await.assert_eq(indoc! {
1349 "hello «\"world\" ˇ»
1350 goodbye"
1351 });
1352 }
1353
1354 #[gpui::test]
1355 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1356 let mut cx = NeovimBackedTestContext::new(cx).await;
1357
1358 cx.set_shared_state(indoc! {
1359 "func empty(a string) bool {
1360 if a == \"\" {
1361 return true
1362 }
1363 ˇreturn false
1364 }"
1365 })
1366 .await;
1367 cx.simulate_shared_keystrokes("v i {").await;
1368 cx.shared_state().await.assert_eq(indoc! {"
1369 func empty(a string) bool {
1370 « if a == \"\" {
1371 return true
1372 }
1373 return false
1374 ˇ»}"});
1375 cx.set_shared_state(indoc! {
1376 "func empty(a string) bool {
1377 if a == \"\" {
1378 ˇreturn true
1379 }
1380 return false
1381 }"
1382 })
1383 .await;
1384 cx.simulate_shared_keystrokes("v i {").await;
1385 cx.shared_state().await.assert_eq(indoc! {"
1386 func empty(a string) bool {
1387 if a == \"\" {
1388 « return true
1389 ˇ» }
1390 return false
1391 }"});
1392
1393 cx.set_shared_state(indoc! {
1394 "func empty(a string) bool {
1395 if a == \"\" ˇ{
1396 return true
1397 }
1398 return false
1399 }"
1400 })
1401 .await;
1402 cx.simulate_shared_keystrokes("v i {").await;
1403 cx.shared_state().await.assert_eq(indoc! {"
1404 func empty(a string) bool {
1405 if a == \"\" {
1406 « return true
1407 ˇ» }
1408 return false
1409 }"});
1410 }
1411
1412 #[gpui::test]
1413 async fn test_singleline_surrounding_character_objects_with_escape(
1414 cx: &mut gpui::TestAppContext,
1415 ) {
1416 let mut cx = NeovimBackedTestContext::new(cx).await;
1417 cx.set_shared_state(indoc! {
1418 "h\"e\\\"lˇlo \\\"world\"!"
1419 })
1420 .await;
1421 cx.simulate_shared_keystrokes("v i \"").await;
1422 cx.shared_state().await.assert_eq(indoc! {
1423 "h\"«e\\\"llo \\\"worldˇ»\"!"
1424 });
1425
1426 cx.set_shared_state(indoc! {
1427 "hello \"teˇst \\\"inside\\\" world\""
1428 })
1429 .await;
1430 cx.simulate_shared_keystrokes("v i \"").await;
1431 cx.shared_state().await.assert_eq(indoc! {
1432 "hello \"«test \\\"inside\\\" worldˇ»\""
1433 });
1434 }
1435
1436 #[gpui::test]
1437 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1438 let mut cx = VimTestContext::new(cx, true).await;
1439 cx.set_state(
1440 indoc! {"
1441 fn boop() {
1442 baz(ˇ|a, b| { bar(|j, k| { })})
1443 }"
1444 },
1445 Mode::Normal,
1446 );
1447 cx.simulate_keystrokes("c i |");
1448 cx.assert_state(
1449 indoc! {"
1450 fn boop() {
1451 baz(|ˇ| { bar(|j, k| { })})
1452 }"
1453 },
1454 Mode::Insert,
1455 );
1456 cx.simulate_keystrokes("escape 1 8 |");
1457 cx.assert_state(
1458 indoc! {"
1459 fn boop() {
1460 baz(|| { bar(ˇ|j, k| { })})
1461 }"
1462 },
1463 Mode::Normal,
1464 );
1465
1466 cx.simulate_keystrokes("v a |");
1467 cx.assert_state(
1468 indoc! {"
1469 fn boop() {
1470 baz(|| { bar(«|j, k| ˇ»{ })})
1471 }"
1472 },
1473 Mode::Visual,
1474 );
1475 }
1476
1477 #[gpui::test]
1478 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1479 let mut cx = VimTestContext::new(cx, true).await;
1480
1481 // Generic arguments
1482 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1483 cx.simulate_keystrokes("v i a");
1484 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1485
1486 // Function arguments
1487 cx.set_state(
1488 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1489 Mode::Normal,
1490 );
1491 cx.simulate_keystrokes("d a a");
1492 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1493
1494 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1495 cx.simulate_keystrokes("v a a");
1496 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1497
1498 // Tuple, vec, and array arguments
1499 cx.set_state(
1500 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1501 Mode::Normal,
1502 );
1503 cx.simulate_keystrokes("c i a");
1504 cx.assert_state(
1505 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1506 Mode::Insert,
1507 );
1508
1509 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1510 cx.simulate_keystrokes("c a a");
1511 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1512
1513 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1514 cx.simulate_keystrokes("c i a");
1515 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1516
1517 cx.set_state(
1518 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1519 Mode::Normal,
1520 );
1521 cx.simulate_keystrokes("c a a");
1522 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1523
1524 // Cursor immediately before / after brackets
1525 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1526 cx.simulate_keystrokes("v i a");
1527 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1528
1529 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1530 cx.simulate_keystrokes("v i a");
1531 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1532 }
1533
1534 #[gpui::test]
1535 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
1536 let mut cx = VimTestContext::new(cx, true).await;
1537
1538 // Base use case
1539 cx.set_state(
1540 indoc! {"
1541 fn boop() {
1542 // Comment
1543 baz();ˇ
1544
1545 loop {
1546 bar(1);
1547 bar(2);
1548 }
1549
1550 result
1551 }
1552 "},
1553 Mode::Normal,
1554 );
1555 cx.simulate_keystrokes("v i i");
1556 cx.assert_state(
1557 indoc! {"
1558 fn boop() {
1559 « // Comment
1560 baz();
1561
1562 loop {
1563 bar(1);
1564 bar(2);
1565 }
1566
1567 resultˇ»
1568 }
1569 "},
1570 Mode::Visual,
1571 );
1572
1573 // Around indent (include line above)
1574 cx.set_state(
1575 indoc! {"
1576 const ABOVE: str = true;
1577 fn boop() {
1578
1579 hello();
1580 worˇld()
1581 }
1582 "},
1583 Mode::Normal,
1584 );
1585 cx.simulate_keystrokes("v a i");
1586 cx.assert_state(
1587 indoc! {"
1588 const ABOVE: str = true;
1589 «fn boop() {
1590
1591 hello();
1592 world()ˇ»
1593 }
1594 "},
1595 Mode::Visual,
1596 );
1597
1598 // Around indent (include line above & below)
1599 cx.set_state(
1600 indoc! {"
1601 const ABOVE: str = true;
1602 fn boop() {
1603 hellˇo();
1604 world()
1605
1606 }
1607 const BELOW: str = true;
1608 "},
1609 Mode::Normal,
1610 );
1611 cx.simulate_keystrokes("c a shift-i");
1612 cx.assert_state(
1613 indoc! {"
1614 const ABOVE: str = true;
1615 ˇ
1616 const BELOW: str = true;
1617 "},
1618 Mode::Insert,
1619 );
1620 }
1621
1622 #[gpui::test]
1623 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1624 let mut cx = NeovimBackedTestContext::new(cx).await;
1625
1626 for (start, end) in SURROUNDING_OBJECTS {
1627 let marked_string = SURROUNDING_MARKER_STRING
1628 .replace('`', &start.to_string())
1629 .replace('\'', &end.to_string());
1630
1631 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1632 .await
1633 .assert_matches();
1634 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1635 .await
1636 .assert_matches();
1637 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1638 .await
1639 .assert_matches();
1640 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1641 .await
1642 .assert_matches();
1643 }
1644 }
1645
1646 #[gpui::test]
1647 async fn test_tags(cx: &mut gpui::TestAppContext) {
1648 let mut cx = VimTestContext::new_html(cx).await;
1649
1650 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1651 cx.simulate_keystrokes("v i t");
1652 cx.assert_state(
1653 "<html><head></head><body><b>«hi!ˇ»</b></body>",
1654 Mode::Visual,
1655 );
1656 cx.simulate_keystrokes("a t");
1657 cx.assert_state(
1658 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1659 Mode::Visual,
1660 );
1661 cx.simulate_keystrokes("a t");
1662 cx.assert_state(
1663 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1664 Mode::Visual,
1665 );
1666
1667 // The cursor is before the tag
1668 cx.set_state(
1669 "<html><head></head><body> ˇ <b>hi!</b></body>",
1670 Mode::Normal,
1671 );
1672 cx.simulate_keystrokes("v i t");
1673 cx.assert_state(
1674 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
1675 Mode::Visual,
1676 );
1677 cx.simulate_keystrokes("a t");
1678 cx.assert_state(
1679 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
1680 Mode::Visual,
1681 );
1682
1683 // The cursor is in the open tag
1684 cx.set_state(
1685 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1686 Mode::Normal,
1687 );
1688 cx.simulate_keystrokes("v a t");
1689 cx.assert_state(
1690 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1691 Mode::Visual,
1692 );
1693 cx.simulate_keystrokes("i t");
1694 cx.assert_state(
1695 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1696 Mode::Visual,
1697 );
1698
1699 // current selection length greater than 1
1700 cx.set_state(
1701 "<html><head></head><body><«b>hi!ˇ»</b></body>",
1702 Mode::Visual,
1703 );
1704 cx.simulate_keystrokes("i t");
1705 cx.assert_state(
1706 "<html><head></head><body><b>«hi!ˇ»</b></body>",
1707 Mode::Visual,
1708 );
1709 cx.simulate_keystrokes("a t");
1710 cx.assert_state(
1711 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1712 Mode::Visual,
1713 );
1714
1715 cx.set_state(
1716 "<html><head></head><body><«b>hi!</ˇ»b></body>",
1717 Mode::Visual,
1718 );
1719 cx.simulate_keystrokes("a t");
1720 cx.assert_state(
1721 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1722 Mode::Visual,
1723 );
1724 }
1725}