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