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