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