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