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