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 let offset = excerpt.map_offset_to_buffer(offset);
518
519 let mut matches: Vec<Range<usize>> = buffer
520 .text_object_ranges(offset..offset, TreeSitterOptions::default())
521 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
522 .collect();
523 matches.sort_by_key(|r| (r.end - r.start));
524 if let Some(buffer_range) = matches.first() {
525 let range = excerpt.map_range_from_buffer(buffer_range.clone());
526 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
527 }
528
529 let around = target.around()?;
530 let mut matches: Vec<Range<usize>> = buffer
531 .text_object_ranges(offset..offset, TreeSitterOptions::default())
532 .filter_map(|(r, m)| if m == around { Some(r) } else { None })
533 .collect();
534 matches.sort_by_key(|r| (r.end - r.start));
535 let around_range = matches.first()?;
536
537 let mut matches: Vec<Range<usize>> = buffer
538 .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
539 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
540 .collect();
541 matches.sort_by_key(|r| r.start);
542 if let Some(buffer_range) = matches.first() {
543 if !buffer_range.is_empty() {
544 let range = excerpt.map_range_from_buffer(buffer_range.clone());
545 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
546 }
547 }
548 let buffer_range = excerpt.map_range_from_buffer(around_range.clone());
549 return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
550}
551
552fn argument(
553 map: &DisplaySnapshot,
554 relative_to: DisplayPoint,
555 around: bool,
556) -> Option<Range<DisplayPoint>> {
557 let snapshot = &map.buffer_snapshot;
558 let offset = relative_to.to_offset(map, Bias::Left);
559
560 // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
561 let excerpt = snapshot.excerpt_containing(offset..offset)?;
562 let buffer = excerpt.buffer();
563
564 fn comma_delimited_range_at(
565 buffer: &BufferSnapshot,
566 mut offset: usize,
567 include_comma: bool,
568 ) -> Option<Range<usize>> {
569 // Seek to the first non-whitespace character
570 offset += buffer
571 .chars_at(offset)
572 .take_while(|c| c.is_whitespace())
573 .map(char::len_utf8)
574 .sum::<usize>();
575
576 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
577 // Filter out empty ranges
578 if open.end == close.start {
579 return false;
580 }
581
582 // If the cursor is outside the brackets, ignore them
583 if open.start == offset || close.end == offset {
584 return false;
585 }
586
587 // TODO: Is there any better way to filter out string brackets?
588 // Used to filter out string brackets
589 matches!(
590 buffer.chars_at(open.start).next(),
591 Some('(' | '[' | '{' | '<' | '|')
592 )
593 };
594
595 // Find the brackets containing the cursor
596 let (open_bracket, close_bracket) =
597 buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
598
599 let inner_bracket_range = open_bracket.end..close_bracket.start;
600
601 let layer = buffer.syntax_layer_at(offset)?;
602 let node = layer.node();
603 let mut cursor = node.walk();
604
605 // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
606 let mut parent_covers_bracket_range = false;
607 loop {
608 let node = cursor.node();
609 let range = node.byte_range();
610 let covers_bracket_range =
611 range.start == open_bracket.start && range.end == close_bracket.end;
612 if parent_covers_bracket_range && !covers_bracket_range {
613 break;
614 }
615 parent_covers_bracket_range = covers_bracket_range;
616
617 // Unable to find a child node with a parent that covers the bracket range, so no argument to select
618 cursor.goto_first_child_for_byte(offset)?;
619 }
620
621 let mut argument_node = cursor.node();
622
623 // If the child node is the open bracket, move to the next sibling.
624 if argument_node.byte_range() == open_bracket {
625 if !cursor.goto_next_sibling() {
626 return Some(inner_bracket_range);
627 }
628 argument_node = cursor.node();
629 }
630 // While the child node is the close bracket or a comma, move to the previous sibling
631 while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
632 if !cursor.goto_previous_sibling() {
633 return Some(inner_bracket_range);
634 }
635 argument_node = cursor.node();
636 if argument_node.byte_range() == open_bracket {
637 return Some(inner_bracket_range);
638 }
639 }
640
641 // The start and end of the argument range, defaulting to the start and end of the argument node
642 let mut start = argument_node.start_byte();
643 let mut end = argument_node.end_byte();
644
645 let mut needs_surrounding_comma = include_comma;
646
647 // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
648 // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
649 while cursor.goto_previous_sibling() {
650 let prev = cursor.node();
651
652 if prev.start_byte() < open_bracket.end {
653 start = open_bracket.end;
654 break;
655 } else if prev.kind() == "," {
656 if needs_surrounding_comma {
657 start = prev.start_byte();
658 needs_surrounding_comma = false;
659 }
660 break;
661 } else if prev.start_byte() < start {
662 start = prev.start_byte();
663 }
664 }
665
666 // Do the same for the end of the argument, extending to next comma or the end of the argument list
667 while cursor.goto_next_sibling() {
668 let next = cursor.node();
669
670 if next.end_byte() > close_bracket.start {
671 end = close_bracket.start;
672 break;
673 } else if next.kind() == "," {
674 if needs_surrounding_comma {
675 // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
676 if let Some(next_arg) = next.next_sibling() {
677 end = next_arg.start_byte();
678 } else {
679 end = next.end_byte();
680 }
681 }
682 break;
683 } else if next.end_byte() > end {
684 end = next.end_byte();
685 }
686 }
687
688 Some(start..end)
689 }
690
691 let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
692
693 if excerpt.contains_buffer_range(result.clone()) {
694 let result = excerpt.map_range_from_buffer(result);
695 Some(result.start.to_display_point(map)..result.end.to_display_point(map))
696 } else {
697 None
698 }
699}
700
701fn indent(
702 map: &DisplaySnapshot,
703 relative_to: DisplayPoint,
704 around: bool,
705 include_below: bool,
706) -> Option<Range<DisplayPoint>> {
707 let point = relative_to.to_point(map);
708 let row = point.row;
709
710 let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
711
712 // Loop backwards until we find a non-blank line with less indent
713 let mut start_row = row;
714 for prev_row in (0..row).rev() {
715 let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
716 if indent.is_line_empty() {
717 continue;
718 }
719 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
720 if around {
721 // When around is true, include the first line with less indent
722 start_row = prev_row;
723 }
724 break;
725 }
726 start_row = prev_row;
727 }
728
729 // Loop forwards until we find a non-blank line with less indent
730 let mut end_row = row;
731 let max_rows = map.buffer_snapshot.max_row().0;
732 for next_row in (row + 1)..=max_rows {
733 let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
734 if indent.is_line_empty() {
735 continue;
736 }
737 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
738 if around && include_below {
739 // When around is true and including below, include this line
740 end_row = next_row;
741 }
742 break;
743 }
744 end_row = next_row;
745 }
746
747 let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
748 let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
749 let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
750 Some(start..end)
751}
752
753fn sentence(
754 map: &DisplaySnapshot,
755 relative_to: DisplayPoint,
756 around: bool,
757) -> Option<Range<DisplayPoint>> {
758 let mut start = None;
759 let relative_offset = relative_to.to_offset(map, Bias::Left);
760 let mut previous_end = relative_offset;
761
762 let mut chars = map.buffer_chars_at(previous_end).peekable();
763
764 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
765 for (char, offset) in chars
766 .peek()
767 .cloned()
768 .into_iter()
769 .chain(map.reverse_buffer_chars_at(previous_end))
770 {
771 if is_sentence_end(map, offset) {
772 break;
773 }
774
775 if is_possible_sentence_start(char) {
776 start = Some(offset);
777 }
778
779 previous_end = offset;
780 }
781
782 // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
783 let mut end = relative_offset;
784 for (char, offset) in chars {
785 if start.is_none() && is_possible_sentence_start(char) {
786 if around {
787 start = Some(offset);
788 continue;
789 } else {
790 end = offset;
791 break;
792 }
793 }
794
795 if char != '\n' {
796 end = offset + char.len_utf8();
797 }
798
799 if is_sentence_end(map, end) {
800 break;
801 }
802 }
803
804 let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
805 if around {
806 range = expand_to_include_whitespace(map, range, false);
807 }
808
809 Some(range)
810}
811
812fn is_possible_sentence_start(character: char) -> bool {
813 !character.is_whitespace() && character != '.'
814}
815
816const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
817const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
818const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
819fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
820 let mut next_chars = map.buffer_chars_at(offset).peekable();
821 if let Some((char, _)) = next_chars.next() {
822 // We are at a double newline. This position is a sentence end.
823 if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
824 return true;
825 }
826
827 // The next text is not a valid whitespace. This is not a sentence end
828 if !SENTENCE_END_WHITESPACE.contains(&char) {
829 return false;
830 }
831 }
832
833 for (char, _) in map.reverse_buffer_chars_at(offset) {
834 if SENTENCE_END_PUNCTUATION.contains(&char) {
835 return true;
836 }
837
838 if !SENTENCE_END_FILLERS.contains(&char) {
839 return false;
840 }
841 }
842
843 false
844}
845
846/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
847/// whitespace to the end first and falls back to the start if there was none.
848fn expand_to_include_whitespace(
849 map: &DisplaySnapshot,
850 range: Range<DisplayPoint>,
851 stop_at_newline: bool,
852) -> Range<DisplayPoint> {
853 let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
854 let mut whitespace_included = false;
855
856 let chars = map.buffer_chars_at(range.end).peekable();
857 for (char, offset) in chars {
858 if char == '\n' && stop_at_newline {
859 break;
860 }
861
862 if char.is_whitespace() {
863 if char != '\n' {
864 range.end = offset + char.len_utf8();
865 whitespace_included = true;
866 }
867 } else {
868 // Found non whitespace. Quit out.
869 break;
870 }
871 }
872
873 if !whitespace_included {
874 for (char, point) in map.reverse_buffer_chars_at(range.start) {
875 if char == '\n' && stop_at_newline {
876 break;
877 }
878
879 if !char.is_whitespace() {
880 break;
881 }
882
883 range.start = point;
884 }
885 }
886
887 range.start.to_display_point(map)..range.end.to_display_point(map)
888}
889
890/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
891/// where `relative_to` is in. If `around`, principally returns the range ending
892/// at the end of the next paragraph.
893///
894/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
895/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
896/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
897/// the trailing newline is not subject to subsequent operations).
898///
899/// Edge cases:
900/// - If `around` and if the current paragraph is the last paragraph of the
901/// file and is blank, then the selection results in an error.
902/// - If `around` and if the current paragraph is the last paragraph of the
903/// file and is not blank, then the returned range starts at the start of the
904/// previous paragraph, if it exists.
905fn paragraph(
906 map: &DisplaySnapshot,
907 relative_to: DisplayPoint,
908 around: bool,
909) -> Option<Range<DisplayPoint>> {
910 let mut paragraph_start = start_of_paragraph(map, relative_to);
911 let mut paragraph_end = end_of_paragraph(map, relative_to);
912
913 let paragraph_end_row = paragraph_end.row();
914 let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
915 let point = relative_to.to_point(map);
916 let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
917
918 if around {
919 if paragraph_ends_with_eof {
920 if current_line_is_empty {
921 return None;
922 }
923
924 let paragraph_start_row = paragraph_start.row();
925 if paragraph_start_row.0 != 0 {
926 let previous_paragraph_last_line_start =
927 DisplayPoint::new(paragraph_start_row - 1, 0);
928 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
929 }
930 } else {
931 let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
932 paragraph_end = end_of_paragraph(map, next_paragraph_start);
933 }
934 }
935
936 let range = paragraph_start..paragraph_end;
937 Some(range)
938}
939
940/// Returns a position of the start of the current paragraph, where a paragraph
941/// is defined as a run of non-blank lines or a run of blank lines.
942pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
943 let point = display_point.to_point(map);
944 if point.row == 0 {
945 return DisplayPoint::zero();
946 }
947
948 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
949
950 for row in (0..point.row).rev() {
951 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
952 if blank != is_current_line_blank {
953 return Point::new(row + 1, 0).to_display_point(map);
954 }
955 }
956
957 DisplayPoint::zero()
958}
959
960/// Returns a position of the end of the current paragraph, where a paragraph
961/// is defined as a run of non-blank lines or a run of blank lines.
962/// The trailing newline is excluded from the paragraph.
963pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
964 let point = display_point.to_point(map);
965 if point.row == map.buffer_snapshot.max_row().0 {
966 return map.max_point();
967 }
968
969 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
970
971 for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
972 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
973 if blank != is_current_line_blank {
974 let previous_row = row - 1;
975 return Point::new(
976 previous_row,
977 map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
978 )
979 .to_display_point(map);
980 }
981 }
982
983 map.max_point()
984}
985
986fn surrounding_markers(
987 map: &DisplaySnapshot,
988 relative_to: DisplayPoint,
989 around: bool,
990 search_across_lines: bool,
991 open_marker: char,
992 close_marker: char,
993) -> Option<Range<DisplayPoint>> {
994 let point = relative_to.to_offset(map, Bias::Left);
995
996 let mut matched_closes = 0;
997 let mut opening = None;
998
999 let mut before_ch = match movement::chars_before(map, point).next() {
1000 Some((ch, _)) => ch,
1001 _ => '\0',
1002 };
1003 if let Some((ch, range)) = movement::chars_after(map, point).next() {
1004 if ch == open_marker && before_ch != '\\' {
1005 if open_marker == close_marker {
1006 let mut total = 0;
1007 for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
1008 {
1009 if ch == '\n' {
1010 break;
1011 }
1012 if ch == open_marker && before_ch != '\\' {
1013 total += 1;
1014 }
1015 }
1016 if total % 2 == 0 {
1017 opening = Some(range)
1018 }
1019 } else {
1020 opening = Some(range)
1021 }
1022 }
1023 }
1024
1025 if opening.is_none() {
1026 let mut chars_before = movement::chars_before(map, point).peekable();
1027 while let Some((ch, range)) = chars_before.next() {
1028 if ch == '\n' && !search_across_lines {
1029 break;
1030 }
1031
1032 if let Some((before_ch, _)) = chars_before.peek() {
1033 if *before_ch == '\\' {
1034 continue;
1035 }
1036 }
1037
1038 if ch == open_marker {
1039 if matched_closes == 0 {
1040 opening = Some(range);
1041 break;
1042 }
1043 matched_closes -= 1;
1044 } else if ch == close_marker {
1045 matched_closes += 1
1046 }
1047 }
1048 }
1049 if opening.is_none() {
1050 for (ch, range) in movement::chars_after(map, point) {
1051 if before_ch != '\\' {
1052 if ch == open_marker {
1053 opening = Some(range);
1054 break;
1055 } else if ch == close_marker {
1056 break;
1057 }
1058 }
1059
1060 before_ch = ch;
1061 }
1062 }
1063
1064 let mut opening = opening?;
1065
1066 let mut matched_opens = 0;
1067 let mut closing = None;
1068 before_ch = match movement::chars_before(map, opening.end).next() {
1069 Some((ch, _)) => ch,
1070 _ => '\0',
1071 };
1072 for (ch, range) in movement::chars_after(map, opening.end) {
1073 if ch == '\n' && !search_across_lines {
1074 break;
1075 }
1076
1077 if before_ch != '\\' {
1078 if ch == close_marker {
1079 if matched_opens == 0 {
1080 closing = Some(range);
1081 break;
1082 }
1083 matched_opens -= 1;
1084 } else if ch == open_marker {
1085 matched_opens += 1;
1086 }
1087 }
1088
1089 before_ch = ch;
1090 }
1091
1092 let mut closing = closing?;
1093
1094 if around && !search_across_lines {
1095 let mut found = false;
1096
1097 for (ch, range) in movement::chars_after(map, closing.end) {
1098 if ch.is_whitespace() && ch != '\n' {
1099 found = true;
1100 closing.end = range.end;
1101 } else {
1102 break;
1103 }
1104 }
1105
1106 if !found {
1107 for (ch, range) in movement::chars_before(map, opening.start) {
1108 if ch.is_whitespace() && ch != '\n' {
1109 opening.start = range.start
1110 } else {
1111 break;
1112 }
1113 }
1114 }
1115 }
1116
1117 if !around && search_across_lines {
1118 if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
1119 if ch == '\n' {
1120 opening.end = range.end
1121 }
1122 }
1123
1124 for (ch, range) in movement::chars_before(map, closing.start) {
1125 if !ch.is_whitespace() {
1126 break;
1127 }
1128 if ch != '\n' {
1129 closing.start = range.start
1130 }
1131 }
1132 }
1133
1134 let result = if around {
1135 opening.start..closing.end
1136 } else {
1137 opening.end..closing.start
1138 };
1139
1140 Some(
1141 map.clip_point(result.start.to_display_point(map), Bias::Left)
1142 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1143 )
1144}
1145
1146#[cfg(test)]
1147mod test {
1148 use indoc::indoc;
1149
1150 use crate::{
1151 state::Mode,
1152 test::{NeovimBackedTestContext, VimTestContext},
1153 };
1154
1155 const WORD_LOCATIONS: &str = indoc! {"
1156 The quick ˇbrowˇnˇ•••
1157 fox ˇjuˇmpsˇ over
1158 the lazy dogˇ••
1159 ˇ
1160 ˇ
1161 ˇ
1162 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1163 ˇ••
1164 ˇ••
1165 ˇ fox-jumpˇs over
1166 the lazy dogˇ•
1167 ˇ
1168 "
1169 };
1170
1171 #[gpui::test]
1172 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1173 let mut cx = NeovimBackedTestContext::new(cx).await;
1174
1175 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1176 .await
1177 .assert_matches();
1178 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1179 .await
1180 .assert_matches();
1181 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1182 .await
1183 .assert_matches();
1184 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1185 .await
1186 .assert_matches();
1187 }
1188
1189 #[gpui::test]
1190 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1191 let mut cx = NeovimBackedTestContext::new(cx).await;
1192
1193 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1194 .await
1195 .assert_matches();
1196 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1197 .await
1198 .assert_matches();
1199 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1200 .await
1201 .assert_matches();
1202 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1203 .await
1204 .assert_matches();
1205 }
1206
1207 #[gpui::test]
1208 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1209 let mut cx = NeovimBackedTestContext::new(cx).await;
1210
1211 /*
1212 cx.set_shared_state("The quick ˇbrown\nfox").await;
1213 cx.simulate_shared_keystrokes(["v"]).await;
1214 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1215 cx.simulate_shared_keystrokes(["i", "w"]).await;
1216 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1217 */
1218 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1219 cx.simulate_shared_keystrokes("v").await;
1220 cx.shared_state()
1221 .await
1222 .assert_eq("The quick brown\n«\nˇ»fox");
1223 cx.simulate_shared_keystrokes("i w").await;
1224 cx.shared_state()
1225 .await
1226 .assert_eq("The quick brown\n«\nˇ»fox");
1227
1228 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1229 .await
1230 .assert_matches();
1231 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1232 .await
1233 .assert_matches();
1234 }
1235
1236 const PARAGRAPH_EXAMPLES: &[&str] = &[
1237 // Single line
1238 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1239 // Multiple lines without empty lines
1240 indoc! {"
1241 ˇThe quick brownˇ
1242 ˇfox jumps overˇ
1243 the lazy dog.ˇ
1244 "},
1245 // Heading blank paragraph and trailing normal paragraph
1246 indoc! {"
1247 ˇ
1248 ˇ
1249 ˇThe quick brown fox jumps
1250 ˇover the lazy dog.
1251 ˇ
1252 ˇ
1253 ˇThe quick brown fox jumpsˇ
1254 ˇover the lazy dog.ˇ
1255 "},
1256 // Inserted blank paragraph and trailing blank paragraph
1257 indoc! {"
1258 ˇThe quick brown fox jumps
1259 ˇover the lazy dog.
1260 ˇ
1261 ˇ
1262 ˇ
1263 ˇThe quick brown fox jumpsˇ
1264 ˇover the lazy dog.ˇ
1265 ˇ
1266 ˇ
1267 ˇ
1268 "},
1269 // "Blank" paragraph with whitespace characters
1270 indoc! {"
1271 ˇThe quick brown fox jumps
1272 over the lazy dog.
1273
1274 ˇ \t
1275
1276 ˇThe quick brown fox jumps
1277 over the lazy dog.ˇ
1278 ˇ
1279 ˇ \t
1280 \t \t
1281 "},
1282 // Single line "paragraphs", where selection size might be zero.
1283 indoc! {"
1284 ˇThe quick brown fox jumps over the lazy dog.
1285 ˇ
1286 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1287 ˇ
1288 "},
1289 ];
1290
1291 #[gpui::test]
1292 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1293 let mut cx = NeovimBackedTestContext::new(cx).await;
1294
1295 for paragraph_example in PARAGRAPH_EXAMPLES {
1296 cx.simulate_at_each_offset("c i p", paragraph_example)
1297 .await
1298 .assert_matches();
1299 cx.simulate_at_each_offset("c a p", paragraph_example)
1300 .await
1301 .assert_matches();
1302 }
1303 }
1304
1305 #[gpui::test]
1306 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1307 let mut cx = NeovimBackedTestContext::new(cx).await;
1308
1309 for paragraph_example in PARAGRAPH_EXAMPLES {
1310 cx.simulate_at_each_offset("d i p", paragraph_example)
1311 .await
1312 .assert_matches();
1313 cx.simulate_at_each_offset("d a p", paragraph_example)
1314 .await
1315 .assert_matches();
1316 }
1317 }
1318
1319 #[gpui::test]
1320 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1321 let mut cx = NeovimBackedTestContext::new(cx).await;
1322
1323 const EXAMPLES: &[&str] = &[
1324 indoc! {"
1325 ˇThe quick brown
1326 fox jumps over
1327 the lazy dog.
1328 "},
1329 indoc! {"
1330 ˇ
1331
1332 ˇThe quick brown fox jumps
1333 over the lazy dog.
1334 ˇ
1335
1336 ˇThe quick brown fox jumps
1337 over the lazy dog.
1338 "},
1339 indoc! {"
1340 ˇThe quick brown fox jumps over the lazy dog.
1341 ˇ
1342 ˇThe quick brown fox jumps over the lazy dog.
1343
1344 "},
1345 ];
1346
1347 for paragraph_example in EXAMPLES {
1348 cx.simulate_at_each_offset("v i p", paragraph_example)
1349 .await
1350 .assert_matches();
1351 cx.simulate_at_each_offset("v a p", paragraph_example)
1352 .await
1353 .assert_matches();
1354 }
1355 }
1356
1357 // Test string with "`" for opening surrounders and "'" for closing surrounders
1358 const SURROUNDING_MARKER_STRING: &str = indoc! {"
1359 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1360 'ˇfox juˇmps ov`ˇer
1361 the ˇlazy d'o`ˇg"};
1362
1363 const SURROUNDING_OBJECTS: &[(char, char)] = &[
1364 ('"', '"'), // Double Quote
1365 ('(', ')'), // Parentheses
1366 ];
1367
1368 #[gpui::test]
1369 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1370 let mut cx = NeovimBackedTestContext::new(cx).await;
1371
1372 for (start, end) in SURROUNDING_OBJECTS {
1373 let marked_string = SURROUNDING_MARKER_STRING
1374 .replace('`', &start.to_string())
1375 .replace('\'', &end.to_string());
1376
1377 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1378 .await
1379 .assert_matches();
1380 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1381 .await
1382 .assert_matches();
1383 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1384 .await
1385 .assert_matches();
1386 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1387 .await
1388 .assert_matches();
1389 }
1390 }
1391 #[gpui::test]
1392 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1393 let mut cx = NeovimBackedTestContext::new(cx).await;
1394 cx.set_shared_wrap(12).await;
1395
1396 cx.set_shared_state(indoc! {
1397 "\"ˇhello world\"!"
1398 })
1399 .await;
1400 cx.simulate_shared_keystrokes("v i \"").await;
1401 cx.shared_state().await.assert_eq(indoc! {
1402 "\"«hello worldˇ»\"!"
1403 });
1404
1405 cx.set_shared_state(indoc! {
1406 "\"hˇello world\"!"
1407 })
1408 .await;
1409 cx.simulate_shared_keystrokes("v i \"").await;
1410 cx.shared_state().await.assert_eq(indoc! {
1411 "\"«hello worldˇ»\"!"
1412 });
1413
1414 cx.set_shared_state(indoc! {
1415 "helˇlo \"world\"!"
1416 })
1417 .await;
1418 cx.simulate_shared_keystrokes("v i \"").await;
1419 cx.shared_state().await.assert_eq(indoc! {
1420 "hello \"«worldˇ»\"!"
1421 });
1422
1423 cx.set_shared_state(indoc! {
1424 "hello \"wˇorld\"!"
1425 })
1426 .await;
1427 cx.simulate_shared_keystrokes("v i \"").await;
1428 cx.shared_state().await.assert_eq(indoc! {
1429 "hello \"«worldˇ»\"!"
1430 });
1431
1432 cx.set_shared_state(indoc! {
1433 "hello \"wˇorld\"!"
1434 })
1435 .await;
1436 cx.simulate_shared_keystrokes("v a \"").await;
1437 cx.shared_state().await.assert_eq(indoc! {
1438 "hello« \"world\"ˇ»!"
1439 });
1440
1441 cx.set_shared_state(indoc! {
1442 "hello \"wˇorld\" !"
1443 })
1444 .await;
1445 cx.simulate_shared_keystrokes("v a \"").await;
1446 cx.shared_state().await.assert_eq(indoc! {
1447 "hello «\"world\" ˇ»!"
1448 });
1449
1450 cx.set_shared_state(indoc! {
1451 "hello \"wˇorld\"•
1452 goodbye"
1453 })
1454 .await;
1455 cx.simulate_shared_keystrokes("v a \"").await;
1456 cx.shared_state().await.assert_eq(indoc! {
1457 "hello «\"world\" ˇ»
1458 goodbye"
1459 });
1460 }
1461
1462 #[gpui::test]
1463 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1464 let mut cx = NeovimBackedTestContext::new(cx).await;
1465
1466 cx.set_shared_state(indoc! {
1467 "func empty(a string) bool {
1468 if a == \"\" {
1469 return true
1470 }
1471 ˇreturn false
1472 }"
1473 })
1474 .await;
1475 cx.simulate_shared_keystrokes("v i {").await;
1476 cx.shared_state().await.assert_eq(indoc! {"
1477 func empty(a string) bool {
1478 « if a == \"\" {
1479 return true
1480 }
1481 return false
1482 ˇ»}"});
1483 cx.set_shared_state(indoc! {
1484 "func empty(a string) bool {
1485 if a == \"\" {
1486 ˇreturn true
1487 }
1488 return false
1489 }"
1490 })
1491 .await;
1492 cx.simulate_shared_keystrokes("v i {").await;
1493 cx.shared_state().await.assert_eq(indoc! {"
1494 func empty(a string) bool {
1495 if a == \"\" {
1496 « return true
1497 ˇ» }
1498 return false
1499 }"});
1500
1501 cx.set_shared_state(indoc! {
1502 "func empty(a string) bool {
1503 if a == \"\" ˇ{
1504 return true
1505 }
1506 return false
1507 }"
1508 })
1509 .await;
1510 cx.simulate_shared_keystrokes("v i {").await;
1511 cx.shared_state().await.assert_eq(indoc! {"
1512 func empty(a string) bool {
1513 if a == \"\" {
1514 « return true
1515 ˇ» }
1516 return false
1517 }"});
1518 }
1519
1520 #[gpui::test]
1521 async fn test_singleline_surrounding_character_objects_with_escape(
1522 cx: &mut gpui::TestAppContext,
1523 ) {
1524 let mut cx = NeovimBackedTestContext::new(cx).await;
1525 cx.set_shared_state(indoc! {
1526 "h\"e\\\"lˇlo \\\"world\"!"
1527 })
1528 .await;
1529 cx.simulate_shared_keystrokes("v i \"").await;
1530 cx.shared_state().await.assert_eq(indoc! {
1531 "h\"«e\\\"llo \\\"worldˇ»\"!"
1532 });
1533
1534 cx.set_shared_state(indoc! {
1535 "hello \"teˇst \\\"inside\\\" world\""
1536 })
1537 .await;
1538 cx.simulate_shared_keystrokes("v i \"").await;
1539 cx.shared_state().await.assert_eq(indoc! {
1540 "hello \"«test \\\"inside\\\" worldˇ»\""
1541 });
1542 }
1543
1544 #[gpui::test]
1545 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1546 let mut cx = VimTestContext::new(cx, true).await;
1547 cx.set_state(
1548 indoc! {"
1549 fn boop() {
1550 baz(ˇ|a, b| { bar(|j, k| { })})
1551 }"
1552 },
1553 Mode::Normal,
1554 );
1555 cx.simulate_keystrokes("c i |");
1556 cx.assert_state(
1557 indoc! {"
1558 fn boop() {
1559 baz(|ˇ| { bar(|j, k| { })})
1560 }"
1561 },
1562 Mode::Insert,
1563 );
1564 cx.simulate_keystrokes("escape 1 8 |");
1565 cx.assert_state(
1566 indoc! {"
1567 fn boop() {
1568 baz(|| { bar(ˇ|j, k| { })})
1569 }"
1570 },
1571 Mode::Normal,
1572 );
1573
1574 cx.simulate_keystrokes("v a |");
1575 cx.assert_state(
1576 indoc! {"
1577 fn boop() {
1578 baz(|| { bar(«|j, k| ˇ»{ })})
1579 }"
1580 },
1581 Mode::Visual,
1582 );
1583 }
1584
1585 #[gpui::test]
1586 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1587 let mut cx = VimTestContext::new(cx, true).await;
1588
1589 // Generic arguments
1590 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1591 cx.simulate_keystrokes("v i a");
1592 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1593
1594 // Function arguments
1595 cx.set_state(
1596 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1597 Mode::Normal,
1598 );
1599 cx.simulate_keystrokes("d a a");
1600 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1601
1602 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1603 cx.simulate_keystrokes("v a a");
1604 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1605
1606 // Tuple, vec, and array arguments
1607 cx.set_state(
1608 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1609 Mode::Normal,
1610 );
1611 cx.simulate_keystrokes("c i a");
1612 cx.assert_state(
1613 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1614 Mode::Insert,
1615 );
1616
1617 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1618 cx.simulate_keystrokes("c a a");
1619 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1620
1621 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1622 cx.simulate_keystrokes("c i a");
1623 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1624
1625 cx.set_state(
1626 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1627 Mode::Normal,
1628 );
1629 cx.simulate_keystrokes("c a a");
1630 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1631
1632 // Cursor immediately before / after brackets
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 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1638 cx.simulate_keystrokes("v i a");
1639 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1640 }
1641
1642 #[gpui::test]
1643 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
1644 let mut cx = VimTestContext::new(cx, true).await;
1645
1646 // Base use case
1647 cx.set_state(
1648 indoc! {"
1649 fn boop() {
1650 // Comment
1651 baz();ˇ
1652
1653 loop {
1654 bar(1);
1655 bar(2);
1656 }
1657
1658 result
1659 }
1660 "},
1661 Mode::Normal,
1662 );
1663 cx.simulate_keystrokes("v i i");
1664 cx.assert_state(
1665 indoc! {"
1666 fn boop() {
1667 « // Comment
1668 baz();
1669
1670 loop {
1671 bar(1);
1672 bar(2);
1673 }
1674
1675 resultˇ»
1676 }
1677 "},
1678 Mode::Visual,
1679 );
1680
1681 // Around indent (include line above)
1682 cx.set_state(
1683 indoc! {"
1684 const ABOVE: str = true;
1685 fn boop() {
1686
1687 hello();
1688 worˇld()
1689 }
1690 "},
1691 Mode::Normal,
1692 );
1693 cx.simulate_keystrokes("v a i");
1694 cx.assert_state(
1695 indoc! {"
1696 const ABOVE: str = true;
1697 «fn boop() {
1698
1699 hello();
1700 world()ˇ»
1701 }
1702 "},
1703 Mode::Visual,
1704 );
1705
1706 // Around indent (include line above & below)
1707 cx.set_state(
1708 indoc! {"
1709 const ABOVE: str = true;
1710 fn boop() {
1711 hellˇo();
1712 world()
1713
1714 }
1715 const BELOW: str = true;
1716 "},
1717 Mode::Normal,
1718 );
1719 cx.simulate_keystrokes("c a shift-i");
1720 cx.assert_state(
1721 indoc! {"
1722 const ABOVE: str = true;
1723 ˇ
1724 const BELOW: str = true;
1725 "},
1726 Mode::Insert,
1727 );
1728 }
1729
1730 #[gpui::test]
1731 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1732 let mut cx = NeovimBackedTestContext::new(cx).await;
1733
1734 for (start, end) in SURROUNDING_OBJECTS {
1735 let marked_string = SURROUNDING_MARKER_STRING
1736 .replace('`', &start.to_string())
1737 .replace('\'', &end.to_string());
1738
1739 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1740 .await
1741 .assert_matches();
1742 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1743 .await
1744 .assert_matches();
1745 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1746 .await
1747 .assert_matches();
1748 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1749 .await
1750 .assert_matches();
1751 }
1752 }
1753
1754 #[gpui::test]
1755 async fn test_tags(cx: &mut gpui::TestAppContext) {
1756 let mut cx = VimTestContext::new_html(cx).await;
1757
1758 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1759 cx.simulate_keystrokes("v i t");
1760 cx.assert_state(
1761 "<html><head></head><body><b>«hi!ˇ»</b></body>",
1762 Mode::Visual,
1763 );
1764 cx.simulate_keystrokes("a t");
1765 cx.assert_state(
1766 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1767 Mode::Visual,
1768 );
1769 cx.simulate_keystrokes("a t");
1770 cx.assert_state(
1771 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1772 Mode::Visual,
1773 );
1774
1775 // The cursor is before the tag
1776 cx.set_state(
1777 "<html><head></head><body> ˇ <b>hi!</b></body>",
1778 Mode::Normal,
1779 );
1780 cx.simulate_keystrokes("v i t");
1781 cx.assert_state(
1782 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
1783 Mode::Visual,
1784 );
1785 cx.simulate_keystrokes("a t");
1786 cx.assert_state(
1787 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
1788 Mode::Visual,
1789 );
1790
1791 // The cursor is in the open tag
1792 cx.set_state(
1793 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1794 Mode::Normal,
1795 );
1796 cx.simulate_keystrokes("v a t");
1797 cx.assert_state(
1798 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1799 Mode::Visual,
1800 );
1801 cx.simulate_keystrokes("i t");
1802 cx.assert_state(
1803 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1804 Mode::Visual,
1805 );
1806
1807 // current selection length greater than 1
1808 cx.set_state(
1809 "<html><head></head><body><«b>hi!ˇ»</b></body>",
1810 Mode::Visual,
1811 );
1812 cx.simulate_keystrokes("i t");
1813 cx.assert_state(
1814 "<html><head></head><body><b>«hi!ˇ»</b></body>",
1815 Mode::Visual,
1816 );
1817 cx.simulate_keystrokes("a t");
1818 cx.assert_state(
1819 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1820 Mode::Visual,
1821 );
1822
1823 cx.set_state(
1824 "<html><head></head><body><«b>hi!</ˇ»b></body>",
1825 Mode::Visual,
1826 );
1827 cx.simulate_keystrokes("a t");
1828 cx.assert_state(
1829 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1830 Mode::Visual,
1831 );
1832 }
1833}