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