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