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 scope = map
351 .buffer_snapshot
352 .language_scope_at(relative_to.to_point(map));
353 let in_word = map
354 .chars_at(relative_to)
355 .next()
356 .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
357 .unwrap_or(false);
358
359 if in_word {
360 around_containing_word(map, relative_to, ignore_punctuation)
361 } else {
362 around_next_word(map, relative_to, ignore_punctuation)
363 }
364}
365
366fn around_containing_word(
367 map: &DisplaySnapshot,
368 relative_to: DisplayPoint,
369 ignore_punctuation: bool,
370) -> Option<Range<DisplayPoint>> {
371 in_word(map, relative_to, ignore_punctuation)
372 .map(|range| expand_to_include_whitespace(map, range, true))
373}
374
375fn around_next_word(
376 map: &DisplaySnapshot,
377 relative_to: DisplayPoint,
378 ignore_punctuation: bool,
379) -> Option<Range<DisplayPoint>> {
380 let scope = map
381 .buffer_snapshot
382 .language_scope_at(relative_to.to_point(map));
383 // Get the start of the word
384 let start = movement::find_preceding_boundary_display_point(
385 map,
386 right(map, relative_to, 1),
387 FindRange::SingleLine,
388 |left, right| {
389 coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
390 != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
391 },
392 );
393
394 let mut word_found = false;
395 let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
396 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
397 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
398
399 let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
400
401 if right_kind != CharKind::Whitespace {
402 word_found = true;
403 }
404
405 found
406 });
407
408 Some(start..end)
409}
410
411fn argument(
412 map: &DisplaySnapshot,
413 relative_to: DisplayPoint,
414 around: bool,
415) -> Option<Range<DisplayPoint>> {
416 let snapshot = &map.buffer_snapshot;
417 let offset = relative_to.to_offset(map, Bias::Left);
418
419 // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
420 let excerpt = snapshot.excerpt_containing(offset..offset)?;
421 let buffer = excerpt.buffer();
422
423 fn comma_delimited_range_at(
424 buffer: &BufferSnapshot,
425 mut offset: usize,
426 include_comma: bool,
427 ) -> Option<Range<usize>> {
428 // Seek to the first non-whitespace character
429 offset += buffer
430 .chars_at(offset)
431 .take_while(|c| c.is_whitespace())
432 .map(char::len_utf8)
433 .sum::<usize>();
434
435 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
436 // Filter out empty ranges
437 if open.end == close.start {
438 return false;
439 }
440
441 // If the cursor is outside the brackets, ignore them
442 if open.start == offset || close.end == offset {
443 return false;
444 }
445
446 // TODO: Is there any better way to filter out string brackets?
447 // Used to filter out string brackets
448 return matches!(
449 buffer.chars_at(open.start).next(),
450 Some('(' | '[' | '{' | '<' | '|')
451 );
452 };
453
454 // Find the brackets containing the cursor
455 let (open_bracket, close_bracket) =
456 buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
457
458 let inner_bracket_range = open_bracket.end..close_bracket.start;
459
460 let layer = buffer.syntax_layer_at(offset)?;
461 let node = layer.node();
462 let mut cursor = node.walk();
463
464 // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
465 let mut parent_covers_bracket_range = false;
466 loop {
467 let node = cursor.node();
468 let range = node.byte_range();
469 let covers_bracket_range =
470 range.start == open_bracket.start && range.end == close_bracket.end;
471 if parent_covers_bracket_range && !covers_bracket_range {
472 break;
473 }
474 parent_covers_bracket_range = covers_bracket_range;
475
476 // Unable to find a child node with a parent that covers the bracket range, so no argument to select
477 if cursor.goto_first_child_for_byte(offset).is_none() {
478 return None;
479 }
480 }
481
482 let mut argument_node = cursor.node();
483
484 // If the child node is the open bracket, move to the next sibling.
485 if argument_node.byte_range() == open_bracket {
486 if !cursor.goto_next_sibling() {
487 return Some(inner_bracket_range);
488 }
489 argument_node = cursor.node();
490 }
491 // While the child node is the close bracket or a comma, move to the previous sibling
492 while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
493 if !cursor.goto_previous_sibling() {
494 return Some(inner_bracket_range);
495 }
496 argument_node = cursor.node();
497 if argument_node.byte_range() == open_bracket {
498 return Some(inner_bracket_range);
499 }
500 }
501
502 // The start and end of the argument range, defaulting to the start and end of the argument node
503 let mut start = argument_node.start_byte();
504 let mut end = argument_node.end_byte();
505
506 let mut needs_surrounding_comma = include_comma;
507
508 // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
509 // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
510 while cursor.goto_previous_sibling() {
511 let prev = cursor.node();
512
513 if prev.start_byte() < open_bracket.end {
514 start = open_bracket.end;
515 break;
516 } else if prev.kind() == "," {
517 if needs_surrounding_comma {
518 start = prev.start_byte();
519 needs_surrounding_comma = false;
520 }
521 break;
522 } else if prev.start_byte() < start {
523 start = prev.start_byte();
524 }
525 }
526
527 // Do the same for the end of the argument, extending to next comma or the end of the argument list
528 while cursor.goto_next_sibling() {
529 let next = cursor.node();
530
531 if next.end_byte() > close_bracket.start {
532 end = close_bracket.start;
533 break;
534 } else if next.kind() == "," {
535 if needs_surrounding_comma {
536 // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
537 if let Some(next_arg) = next.next_sibling() {
538 end = next_arg.start_byte();
539 } else {
540 end = next.end_byte();
541 }
542 }
543 break;
544 } else if next.end_byte() > end {
545 end = next.end_byte();
546 }
547 }
548
549 Some(start..end)
550 }
551
552 let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
553
554 if excerpt.contains_buffer_range(result.clone()) {
555 let result = excerpt.map_range_from_buffer(result);
556 Some(result.start.to_display_point(map)..result.end.to_display_point(map))
557 } else {
558 None
559 }
560}
561
562fn sentence(
563 map: &DisplaySnapshot,
564 relative_to: DisplayPoint,
565 around: bool,
566) -> Option<Range<DisplayPoint>> {
567 let mut start = None;
568 let mut previous_end = relative_to;
569
570 let mut chars = map.chars_at(relative_to).peekable();
571
572 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
573 for (char, point) in chars
574 .peek()
575 .cloned()
576 .into_iter()
577 .chain(map.reverse_chars_at(relative_to))
578 {
579 if is_sentence_end(map, point) {
580 break;
581 }
582
583 if is_possible_sentence_start(char) {
584 start = Some(point);
585 }
586
587 previous_end = point;
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_to;
592 for (char, point) in chars {
593 if start.is_none() && is_possible_sentence_start(char) {
594 if around {
595 start = Some(point);
596 continue;
597 } else {
598 end = point;
599 break;
600 }
601 }
602
603 end = point;
604 *end.column_mut() += char.len_utf8() as u32;
605 end = map.clip_point(end, Bias::Left);
606
607 if is_sentence_end(map, end) {
608 break;
609 }
610 }
611
612 let mut range = start.unwrap_or(previous_end)..end;
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, point: DisplayPoint) -> bool {
628 let mut next_chars = map.chars_at(point).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_chars_at(point) {
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 mut range: Range<DisplayPoint>,
659 stop_at_newline: bool,
660) -> Range<DisplayPoint> {
661 let mut whitespace_included = false;
662
663 let mut chars = map.chars_at(range.end).peekable();
664 while let Some((char, point)) = chars.next() {
665 if char == '\n' && stop_at_newline {
666 break;
667 }
668
669 if char.is_whitespace() {
670 // Set end to the next display_point or the character position after the current display_point
671 range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
672 let mut end = point;
673 *end.column_mut() += char.len_utf8() as u32;
674 map.clip_point(end, Bias::Left)
675 });
676
677 if char != '\n' {
678 whitespace_included = true;
679 }
680 } else {
681 // Found non whitespace. Quit out.
682 break;
683 }
684 }
685
686 if !whitespace_included {
687 for (char, point) in map.reverse_chars_at(range.start) {
688 if char == '\n' && stop_at_newline {
689 break;
690 }
691
692 if !char.is_whitespace() {
693 break;
694 }
695
696 range.start = point;
697 }
698 }
699
700 range
701}
702
703/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
704/// where `relative_to` is in. If `around`, principally returns the range ending
705/// at the end of the next paragraph.
706///
707/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
708/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
709/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
710/// the trailing newline is not subject to subsequent operations).
711///
712/// Edge cases:
713/// - If `around` and if the current paragraph is the last paragraph of the
714/// file and is blank, then the selection results in an error.
715/// - If `around` and if the current paragraph is the last paragraph of the
716/// file and is not blank, then the returned range starts at the start of the
717/// previous paragraph, if it exists.
718fn paragraph(
719 map: &DisplaySnapshot,
720 relative_to: DisplayPoint,
721 around: bool,
722) -> Option<Range<DisplayPoint>> {
723 let mut paragraph_start = start_of_paragraph(map, relative_to);
724 let mut paragraph_end = end_of_paragraph(map, relative_to);
725
726 let paragraph_end_row = paragraph_end.row();
727 let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
728 let point = relative_to.to_point(map);
729 let current_line_is_empty = map.buffer_snapshot.is_line_blank(point.row);
730
731 if around {
732 if paragraph_ends_with_eof {
733 if current_line_is_empty {
734 return None;
735 }
736
737 let paragraph_start_row = paragraph_start.row();
738 if paragraph_start_row != 0 {
739 let previous_paragraph_last_line_start =
740 Point::new(paragraph_start_row - 1, 0).to_display_point(map);
741 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
742 }
743 } else {
744 let next_paragraph_start = Point::new(paragraph_end_row + 1, 0).to_display_point(map);
745 paragraph_end = end_of_paragraph(map, next_paragraph_start);
746 }
747 }
748
749 let range = paragraph_start..paragraph_end;
750 Some(range)
751}
752
753/// Returns a position of the start of the current paragraph, where a paragraph
754/// is defined as a run of non-blank lines or a run of blank lines.
755pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
756 let point = display_point.to_point(map);
757 if point.row == 0 {
758 return DisplayPoint::zero();
759 }
760
761 let is_current_line_blank = map.buffer_snapshot.is_line_blank(point.row);
762
763 for row in (0..point.row).rev() {
764 let blank = map.buffer_snapshot.is_line_blank(row);
765 if blank != is_current_line_blank {
766 return Point::new(row + 1, 0).to_display_point(map);
767 }
768 }
769
770 DisplayPoint::zero()
771}
772
773/// Returns a position of the end of the current paragraph, where a paragraph
774/// is defined as a run of non-blank lines or a run of blank lines.
775/// The trailing newline is excluded from the paragraph.
776pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
777 let point = display_point.to_point(map);
778 if point.row == map.max_buffer_row() {
779 return map.max_point();
780 }
781
782 let is_current_line_blank = map.buffer_snapshot.is_line_blank(point.row);
783
784 for row in point.row + 1..map.max_buffer_row() + 1 {
785 let blank = map.buffer_snapshot.is_line_blank(row);
786 if blank != is_current_line_blank {
787 let previous_row = row - 1;
788 return Point::new(previous_row, map.buffer_snapshot.line_len(previous_row))
789 .to_display_point(map);
790 }
791 }
792
793 map.max_point()
794}
795
796fn surrounding_markers(
797 map: &DisplaySnapshot,
798 relative_to: DisplayPoint,
799 around: bool,
800 search_across_lines: bool,
801 open_marker: char,
802 close_marker: char,
803) -> Option<Range<DisplayPoint>> {
804 let point = relative_to.to_offset(map, Bias::Left);
805
806 let mut matched_closes = 0;
807 let mut opening = None;
808
809 if let Some((ch, range)) = movement::chars_after(map, point).next() {
810 if ch == open_marker {
811 if open_marker == close_marker {
812 let mut total = 0;
813 for (ch, _) in movement::chars_before(map, point) {
814 if ch == '\n' {
815 break;
816 }
817 if ch == open_marker {
818 total += 1;
819 }
820 }
821 if total % 2 == 0 {
822 opening = Some(range)
823 }
824 } else {
825 opening = Some(range)
826 }
827 }
828 }
829
830 if opening.is_none() {
831 for (ch, range) in movement::chars_before(map, point) {
832 if ch == '\n' && !search_across_lines {
833 break;
834 }
835
836 if ch == open_marker {
837 if matched_closes == 0 {
838 opening = Some(range);
839 break;
840 }
841 matched_closes -= 1;
842 } else if ch == close_marker {
843 matched_closes += 1
844 }
845 }
846 }
847
848 if opening.is_none() {
849 for (ch, range) in movement::chars_after(map, point) {
850 if ch == open_marker {
851 opening = Some(range);
852 break;
853 } else if ch == close_marker {
854 break;
855 }
856 }
857 }
858
859 let Some(mut opening) = opening else {
860 return None;
861 };
862
863 let mut matched_opens = 0;
864 let mut closing = None;
865
866 for (ch, range) in movement::chars_after(map, opening.end) {
867 if ch == '\n' && !search_across_lines {
868 break;
869 }
870
871 if ch == close_marker {
872 if matched_opens == 0 {
873 closing = Some(range);
874 break;
875 }
876 matched_opens -= 1;
877 } else if ch == open_marker {
878 matched_opens += 1;
879 }
880 }
881
882 let Some(mut closing) = closing else {
883 return None;
884 };
885
886 if around && !search_across_lines {
887 let mut found = false;
888
889 for (ch, range) in movement::chars_after(map, closing.end) {
890 if ch.is_whitespace() && ch != '\n' {
891 found = true;
892 closing.end = range.end;
893 } else {
894 break;
895 }
896 }
897
898 if !found {
899 for (ch, range) in movement::chars_before(map, opening.start) {
900 if ch.is_whitespace() && ch != '\n' {
901 opening.start = range.start
902 } else {
903 break;
904 }
905 }
906 }
907 }
908
909 if !around && search_across_lines {
910 if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
911 if ch == '\n' {
912 opening.end = range.end
913 }
914 }
915
916 for (ch, range) in movement::chars_before(map, closing.start) {
917 if !ch.is_whitespace() {
918 break;
919 }
920 if ch != '\n' {
921 closing.start = range.start
922 }
923 }
924 }
925
926 let result = if around {
927 opening.start..closing.end
928 } else {
929 opening.end..closing.start
930 };
931
932 Some(
933 map.clip_point(result.start.to_display_point(map), Bias::Left)
934 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
935 )
936}
937
938#[cfg(test)]
939mod test {
940 use indoc::indoc;
941
942 use crate::{
943 state::Mode,
944 test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
945 };
946
947 const WORD_LOCATIONS: &str = indoc! {"
948 The quick ˇbrowˇnˇ•••
949 fox ˇjuˇmpsˇ over
950 the lazy dogˇ••
951 ˇ
952 ˇ
953 ˇ
954 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
955 ˇ••
956 ˇ••
957 ˇ fox-jumpˇs over
958 the lazy dogˇ•
959 ˇ
960 "
961 };
962
963 #[gpui::test]
964 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
965 let mut cx = NeovimBackedTestContext::new(cx).await;
966
967 cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
968 .await;
969 cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
970 .await;
971 cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
972 .await;
973 cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
974 .await;
975 }
976
977 #[gpui::test]
978 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
979 let mut cx = NeovimBackedTestContext::new(cx).await;
980
981 cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
982 .await;
983 cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
984 .await;
985 cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
986 .await;
987 cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
988 .await;
989 }
990
991 #[gpui::test]
992 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
993 let mut cx = NeovimBackedTestContext::new(cx).await;
994
995 /*
996 cx.set_shared_state("The quick ˇbrown\nfox").await;
997 cx.simulate_shared_keystrokes(["v"]).await;
998 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
999 cx.simulate_shared_keystrokes(["i", "w"]).await;
1000 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1001 */
1002 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1003 cx.simulate_shared_keystrokes(["v"]).await;
1004 cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
1005 cx.simulate_shared_keystrokes(["i", "w"]).await;
1006 cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
1007
1008 cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
1009 .await;
1010 cx.assert_binding_matches_all_exempted(
1011 ["v", "h", "i", "w"],
1012 WORD_LOCATIONS,
1013 ExemptionFeatures::NonEmptyVisualTextObjects,
1014 )
1015 .await;
1016 cx.assert_binding_matches_all_exempted(
1017 ["v", "l", "i", "w"],
1018 WORD_LOCATIONS,
1019 ExemptionFeatures::NonEmptyVisualTextObjects,
1020 )
1021 .await;
1022 cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
1023 .await;
1024
1025 cx.assert_binding_matches_all_exempted(
1026 ["v", "i", "h", "shift-w"],
1027 WORD_LOCATIONS,
1028 ExemptionFeatures::NonEmptyVisualTextObjects,
1029 )
1030 .await;
1031 cx.assert_binding_matches_all_exempted(
1032 ["v", "i", "l", "shift-w"],
1033 WORD_LOCATIONS,
1034 ExemptionFeatures::NonEmptyVisualTextObjects,
1035 )
1036 .await;
1037
1038 cx.assert_binding_matches_all_exempted(
1039 ["v", "a", "w"],
1040 WORD_LOCATIONS,
1041 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1042 )
1043 .await;
1044 cx.assert_binding_matches_all_exempted(
1045 ["v", "a", "shift-w"],
1046 WORD_LOCATIONS,
1047 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1048 )
1049 .await;
1050 }
1051
1052 const SENTENCE_EXAMPLES: &[&'static str] = &[
1053 "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
1054 indoc! {"
1055 ˇThe quick ˇbrownˇ
1056 fox jumps over
1057 the lazy doˇgˇ.ˇ ˇThe quick ˇ
1058 brown fox jumps over
1059 "},
1060 indoc! {"
1061 The quick brown fox jumps.
1062 Over the lazy dog
1063 ˇ
1064 ˇ
1065 ˇ fox-jumpˇs over
1066 the lazy dog.ˇ
1067 ˇ
1068 "},
1069 r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
1070 ];
1071
1072 #[gpui::test]
1073 async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
1074 let mut cx = NeovimBackedTestContext::new(cx)
1075 .await
1076 .binding(["c", "i", "s"]);
1077 cx.add_initial_state_exemptions(
1078 "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n fox-jumps over\nthe lazy dog.\n\n",
1079 ExemptionFeatures::SentenceOnEmptyLines);
1080 cx.add_initial_state_exemptions(
1081 "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
1082 ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
1083 cx.add_initial_state_exemptions(
1084 "The quick brown fox jumps.\nOver the lazy dog\n\n\n fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
1085 ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
1086 for sentence_example in SENTENCE_EXAMPLES {
1087 cx.assert_all(sentence_example).await;
1088 }
1089
1090 let mut cx = cx.binding(["c", "a", "s"]);
1091 cx.add_initial_state_exemptions(
1092 "The quick brown?ˇ Fox Jumps! Over the lazy.",
1093 ExemptionFeatures::IncorrectLandingPosition,
1094 );
1095 cx.add_initial_state_exemptions(
1096 "The quick brown.)]\'\" Brown fox jumps.ˇ ",
1097 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1098 );
1099
1100 for sentence_example in SENTENCE_EXAMPLES {
1101 cx.assert_all(sentence_example).await;
1102 }
1103 }
1104
1105 #[gpui::test]
1106 async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
1107 let mut cx = NeovimBackedTestContext::new(cx)
1108 .await
1109 .binding(["d", "i", "s"]);
1110 cx.add_initial_state_exemptions(
1111 "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n fox-jumps over\nthe lazy dog.\n\n",
1112 ExemptionFeatures::SentenceOnEmptyLines);
1113 cx.add_initial_state_exemptions(
1114 "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
1115 ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
1116 cx.add_initial_state_exemptions(
1117 "The quick brown fox jumps.\nOver the lazy dog\n\n\n fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
1118 ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
1119
1120 for sentence_example in SENTENCE_EXAMPLES {
1121 cx.assert_all(sentence_example).await;
1122 }
1123
1124 let mut cx = cx.binding(["d", "a", "s"]);
1125 cx.add_initial_state_exemptions(
1126 "The quick brown?ˇ Fox Jumps! Over the lazy.",
1127 ExemptionFeatures::IncorrectLandingPosition,
1128 );
1129 cx.add_initial_state_exemptions(
1130 "The quick brown.)]\'\" Brown fox jumps.ˇ ",
1131 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1132 );
1133
1134 for sentence_example in SENTENCE_EXAMPLES {
1135 cx.assert_all(sentence_example).await;
1136 }
1137 }
1138
1139 #[gpui::test]
1140 async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
1141 let mut cx = NeovimBackedTestContext::new(cx)
1142 .await
1143 .binding(["v", "i", "s"]);
1144 for sentence_example in SENTENCE_EXAMPLES {
1145 cx.assert_all_exempted(sentence_example, ExemptionFeatures::SentenceOnEmptyLines)
1146 .await;
1147 }
1148
1149 let mut cx = cx.binding(["v", "a", "s"]);
1150 for sentence_example in SENTENCE_EXAMPLES {
1151 cx.assert_all_exempted(
1152 sentence_example,
1153 ExemptionFeatures::AroundSentenceStartingBetweenIncludesWrongWhitespace,
1154 )
1155 .await;
1156 }
1157 }
1158
1159 const PARAGRAPH_EXAMPLES: &[&'static str] = &[
1160 // Single line
1161 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1162 // Multiple lines without empty lines
1163 indoc! {"
1164 ˇThe quick brownˇ
1165 ˇfox jumps overˇ
1166 the lazy dog.ˇ
1167 "},
1168 // Heading blank paragraph and trailing normal paragraph
1169 indoc! {"
1170 ˇ
1171 ˇ
1172 ˇThe quick brown fox jumps
1173 ˇover the lazy dog.
1174 ˇ
1175 ˇ
1176 ˇThe quick brown fox jumpsˇ
1177 ˇover the lazy dog.ˇ
1178 "},
1179 // Inserted blank paragraph and trailing blank paragraph
1180 indoc! {"
1181 ˇThe quick brown fox jumps
1182 ˇover the lazy dog.
1183 ˇ
1184 ˇ
1185 ˇ
1186 ˇThe quick brown fox jumpsˇ
1187 ˇover the lazy dog.ˇ
1188 ˇ
1189 ˇ
1190 ˇ
1191 "},
1192 // "Blank" paragraph with whitespace characters
1193 indoc! {"
1194 ˇThe quick brown fox jumps
1195 over the lazy dog.
1196
1197 ˇ \t
1198
1199 ˇThe quick brown fox jumps
1200 over the lazy dog.ˇ
1201 ˇ
1202 ˇ \t
1203 \t \t
1204 "},
1205 // Single line "paragraphs", where selection size might be zero.
1206 indoc! {"
1207 ˇThe quick brown fox jumps over the lazy dog.
1208 ˇ
1209 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1210 ˇ
1211 "},
1212 ];
1213
1214 #[gpui::test]
1215 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1216 let mut cx = NeovimBackedTestContext::new(cx).await;
1217
1218 for paragraph_example in PARAGRAPH_EXAMPLES {
1219 cx.assert_binding_matches_all(["c", "i", "p"], paragraph_example)
1220 .await;
1221 cx.assert_binding_matches_all(["c", "a", "p"], paragraph_example)
1222 .await;
1223 }
1224 }
1225
1226 #[gpui::test]
1227 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1228 let mut cx = NeovimBackedTestContext::new(cx).await;
1229
1230 for paragraph_example in PARAGRAPH_EXAMPLES {
1231 cx.assert_binding_matches_all(["d", "i", "p"], paragraph_example)
1232 .await;
1233 cx.assert_binding_matches_all(["d", "a", "p"], paragraph_example)
1234 .await;
1235 }
1236 }
1237
1238 #[gpui::test]
1239 async fn test_paragraph_object_with_landing_positions_not_at_beginning_of_line(
1240 cx: &mut gpui::TestAppContext,
1241 ) {
1242 // Landing position not at the beginning of the line
1243 const PARAGRAPH_LANDING_POSITION_EXAMPLE: &'static str = indoc! {"
1244 The quick brown fox jumpsˇ
1245 over the lazy dog.ˇ
1246 ˇ ˇ\tˇ
1247 ˇ ˇ
1248 ˇ\tˇ ˇ\tˇ
1249 ˇThe quick brown fox jumpsˇ
1250 ˇover the lazy dog.ˇ
1251 ˇ ˇ\tˇ
1252 ˇ
1253 ˇ ˇ\tˇ
1254 ˇ\tˇ ˇ\tˇ
1255 "};
1256
1257 let mut cx = NeovimBackedTestContext::new(cx).await;
1258
1259 cx.assert_binding_matches_all_exempted(
1260 ["c", "i", "p"],
1261 PARAGRAPH_LANDING_POSITION_EXAMPLE,
1262 ExemptionFeatures::IncorrectLandingPosition,
1263 )
1264 .await;
1265 cx.assert_binding_matches_all_exempted(
1266 ["c", "a", "p"],
1267 PARAGRAPH_LANDING_POSITION_EXAMPLE,
1268 ExemptionFeatures::IncorrectLandingPosition,
1269 )
1270 .await;
1271 cx.assert_binding_matches_all_exempted(
1272 ["d", "i", "p"],
1273 PARAGRAPH_LANDING_POSITION_EXAMPLE,
1274 ExemptionFeatures::IncorrectLandingPosition,
1275 )
1276 .await;
1277 cx.assert_binding_matches_all_exempted(
1278 ["d", "a", "p"],
1279 PARAGRAPH_LANDING_POSITION_EXAMPLE,
1280 ExemptionFeatures::IncorrectLandingPosition,
1281 )
1282 .await;
1283 }
1284
1285 #[gpui::test]
1286 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1287 let mut cx = NeovimBackedTestContext::new(cx).await;
1288
1289 const EXAMPLES: &[&'static str] = &[
1290 indoc! {"
1291 ˇThe quick brown
1292 fox jumps over
1293 the lazy dog.
1294 "},
1295 indoc! {"
1296 ˇ
1297
1298 ˇThe quick brown fox jumps
1299 over the lazy dog.
1300 ˇ
1301
1302 ˇThe quick brown fox jumps
1303 over the lazy dog.
1304 "},
1305 indoc! {"
1306 ˇThe quick brown fox jumps over the lazy dog.
1307 ˇ
1308 ˇThe quick brown fox jumps over the lazy dog.
1309
1310 "},
1311 ];
1312
1313 for paragraph_example in EXAMPLES {
1314 cx.assert_binding_matches_all(["v", "i", "p"], paragraph_example)
1315 .await;
1316 cx.assert_binding_matches_all(["v", "a", "p"], paragraph_example)
1317 .await;
1318 }
1319 }
1320
1321 // Test string with "`" for opening surrounders and "'" for closing surrounders
1322 const SURROUNDING_MARKER_STRING: &str = indoc! {"
1323 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1324 'ˇfox juˇmps ovˇ`ˇer
1325 the ˇlazy dˇ'ˇoˇ`ˇg"};
1326
1327 const SURROUNDING_OBJECTS: &[(char, char)] = &[
1328 ('\'', '\''), // Quote
1329 ('`', '`'), // Back Quote
1330 ('"', '"'), // Double Quote
1331 ('(', ')'), // Parentheses
1332 ('[', ']'), // SquareBrackets
1333 ('{', '}'), // CurlyBrackets
1334 ('<', '>'), // AngleBrackets
1335 ];
1336
1337 #[gpui::test]
1338 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1339 let mut cx = NeovimBackedTestContext::new(cx).await;
1340
1341 for (start, end) in SURROUNDING_OBJECTS {
1342 let marked_string = SURROUNDING_MARKER_STRING
1343 .replace('`', &start.to_string())
1344 .replace('\'', &end.to_string());
1345
1346 cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
1347 .await;
1348 cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
1349 .await;
1350 cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
1351 .await;
1352 cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
1353 .await;
1354 }
1355 }
1356 #[gpui::test]
1357 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1358 let mut cx = NeovimBackedTestContext::new(cx).await;
1359 cx.set_shared_wrap(12).await;
1360
1361 cx.set_shared_state(indoc! {
1362 "helˇlo \"world\"!"
1363 })
1364 .await;
1365 cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
1366 cx.assert_shared_state(indoc! {
1367 "hello \"«worldˇ»\"!"
1368 })
1369 .await;
1370
1371 cx.set_shared_state(indoc! {
1372 "hello \"wˇorld\"!"
1373 })
1374 .await;
1375 cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
1376 cx.assert_shared_state(indoc! {
1377 "hello \"«worldˇ»\"!"
1378 })
1379 .await;
1380
1381 cx.set_shared_state(indoc! {
1382 "hello \"wˇorld\"!"
1383 })
1384 .await;
1385 cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1386 cx.assert_shared_state(indoc! {
1387 "hello« \"world\"ˇ»!"
1388 })
1389 .await;
1390
1391 cx.set_shared_state(indoc! {
1392 "hello \"wˇorld\" !"
1393 })
1394 .await;
1395 cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1396 cx.assert_shared_state(indoc! {
1397 "hello «\"world\" ˇ»!"
1398 })
1399 .await;
1400
1401 cx.set_shared_state(indoc! {
1402 "hello \"wˇorld\"•
1403 goodbye"
1404 })
1405 .await;
1406 cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1407 cx.assert_shared_state(indoc! {
1408 "hello «\"world\" ˇ»
1409 goodbye"
1410 })
1411 .await;
1412 }
1413
1414 #[gpui::test]
1415 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1416 let mut cx = NeovimBackedTestContext::new(cx).await;
1417
1418 cx.set_shared_state(indoc! {
1419 "func empty(a string) bool {
1420 if a == \"\" {
1421 return true
1422 }
1423 ˇreturn false
1424 }"
1425 })
1426 .await;
1427 cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1428 cx.assert_shared_state(indoc! {"
1429 func empty(a string) bool {
1430 « if a == \"\" {
1431 return true
1432 }
1433 return false
1434 ˇ»}"})
1435 .await;
1436 cx.set_shared_state(indoc! {
1437 "func empty(a string) bool {
1438 if a == \"\" {
1439 ˇreturn true
1440 }
1441 return false
1442 }"
1443 })
1444 .await;
1445 cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1446 cx.assert_shared_state(indoc! {"
1447 func empty(a string) bool {
1448 if a == \"\" {
1449 « return true
1450 ˇ» }
1451 return false
1452 }"})
1453 .await;
1454
1455 cx.set_shared_state(indoc! {
1456 "func empty(a string) bool {
1457 if a == \"\" ˇ{
1458 return true
1459 }
1460 return false
1461 }"
1462 })
1463 .await;
1464 cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1465 cx.assert_shared_state(indoc! {"
1466 func empty(a string) bool {
1467 if a == \"\" {
1468 « return true
1469 ˇ» }
1470 return false
1471 }"})
1472 .await;
1473 }
1474
1475 #[gpui::test]
1476 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1477 let mut cx = VimTestContext::new(cx, true).await;
1478 cx.set_state(
1479 indoc! {"
1480 fn boop() {
1481 baz(ˇ|a, b| { bar(|j, k| { })})
1482 }"
1483 },
1484 Mode::Normal,
1485 );
1486 cx.simulate_keystrokes(["c", "i", "|"]);
1487 cx.assert_state(
1488 indoc! {"
1489 fn boop() {
1490 baz(|ˇ| { bar(|j, k| { })})
1491 }"
1492 },
1493 Mode::Insert,
1494 );
1495 cx.simulate_keystrokes(["escape", "1", "8", "|"]);
1496 cx.assert_state(
1497 indoc! {"
1498 fn boop() {
1499 baz(|| { bar(ˇ|j, k| { })})
1500 }"
1501 },
1502 Mode::Normal,
1503 );
1504
1505 cx.simulate_keystrokes(["v", "a", "|"]);
1506 cx.assert_state(
1507 indoc! {"
1508 fn boop() {
1509 baz(|| { bar(«|j, k| ˇ»{ })})
1510 }"
1511 },
1512 Mode::Visual,
1513 );
1514 }
1515
1516 #[gpui::test]
1517 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1518 let mut cx = VimTestContext::new(cx, true).await;
1519
1520 // Generic arguments
1521 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1522 cx.simulate_keystrokes(["v", "i", "a"]);
1523 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1524
1525 // Function arguments
1526 cx.set_state(
1527 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1528 Mode::Normal,
1529 );
1530 cx.simulate_keystrokes(["d", "a", "a"]);
1531 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1532
1533 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1534 cx.simulate_keystrokes(["v", "a", "a"]);
1535 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1536
1537 // Tuple, vec, and array arguments
1538 cx.set_state(
1539 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1540 Mode::Normal,
1541 );
1542 cx.simulate_keystrokes(["c", "i", "a"]);
1543 cx.assert_state(
1544 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1545 Mode::Insert,
1546 );
1547
1548 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1549 cx.simulate_keystrokes(["c", "a", "a"]);
1550 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1551
1552 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1553 cx.simulate_keystrokes(["c", "i", "a"]);
1554 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1555
1556 cx.set_state(
1557 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1558 Mode::Normal,
1559 );
1560 cx.simulate_keystrokes(["c", "a", "a"]);
1561 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1562
1563 // Cursor immediately before / after brackets
1564 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1565 cx.simulate_keystrokes(["v", "i", "a"]);
1566 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1567
1568 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1569 cx.simulate_keystrokes(["v", "i", "a"]);
1570 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1571 }
1572
1573 #[gpui::test]
1574 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1575 let mut cx = NeovimBackedTestContext::new(cx).await;
1576
1577 for (start, end) in SURROUNDING_OBJECTS {
1578 let marked_string = SURROUNDING_MARKER_STRING
1579 .replace('`', &start.to_string())
1580 .replace('\'', &end.to_string());
1581
1582 cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
1583 .await;
1584 cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
1585 .await;
1586 cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
1587 .await;
1588 cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
1589 .await;
1590 }
1591 }
1592
1593 #[gpui::test]
1594 async fn test_tags(cx: &mut gpui::TestAppContext) {
1595 let mut cx = VimTestContext::new_html(cx).await;
1596
1597 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1598 cx.simulate_keystrokes(["v", "i", "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 cx.simulate_keystrokes(["a", "t"]);
1609 cx.assert_state(
1610 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1611 Mode::Visual,
1612 );
1613
1614 // The cursor is before the tag
1615 cx.set_state(
1616 "<html><head></head><body> ˇ <b>hi!</b></body>",
1617 Mode::Normal,
1618 );
1619 cx.simulate_keystrokes(["v", "i", "t"]);
1620 cx.assert_state(
1621 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
1622 Mode::Visual,
1623 );
1624 cx.simulate_keystrokes(["a", "t"]);
1625 cx.assert_state(
1626 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
1627 Mode::Visual,
1628 );
1629
1630 // The cursor is in the open tag
1631 cx.set_state(
1632 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1633 Mode::Normal,
1634 );
1635 cx.simulate_keystrokes(["v", "a", "t"]);
1636 cx.assert_state(
1637 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1638 Mode::Visual,
1639 );
1640 cx.simulate_keystrokes(["i", "t"]);
1641 cx.assert_state(
1642 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1643 Mode::Visual,
1644 );
1645
1646 // current selection length greater than 1
1647 cx.set_state(
1648 "<html><head></head><body><«b>hi!ˇ»</b></body>",
1649 Mode::Visual,
1650 );
1651 cx.simulate_keystrokes(["i", "t"]);
1652 cx.assert_state(
1653 "<html><head></head><body><b>«hi!ˇ»</b></body>",
1654 Mode::Visual,
1655 );
1656 cx.simulate_keystrokes(["a", "t"]);
1657 cx.assert_state(
1658 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1659 Mode::Visual,
1660 );
1661
1662 cx.set_state(
1663 "<html><head></head><body><«b>hi!</ˇ»b></body>",
1664 Mode::Visual,
1665 );
1666 cx.simulate_keystrokes(["a", "t"]);
1667 cx.assert_state(
1668 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1669 Mode::Visual,
1670 );
1671 }
1672}