1use std::ops::Range;
2
3use crate::{
4 Vim,
5 motion::{is_subword_end, is_subword_start, right},
6 state::{Mode, Operator},
7};
8use editor::{
9 Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset,
10 display_map::{DisplaySnapshot, ToDisplayPoint},
11 movement::{self, FindRange},
12};
13use gpui::{Action, Window, actions};
14use itertools::Itertools;
15use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
16use multi_buffer::MultiBufferRow;
17use schemars::JsonSchema;
18use serde::Deserialize;
19use ui::Context;
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
22#[serde(rename_all = "snake_case")]
23pub enum Object {
24 Word { ignore_punctuation: bool },
25 Subword { ignore_punctuation: bool },
26 Sentence,
27 Paragraph,
28 Quotes,
29 BackQuotes,
30 AnyQuotes,
31 MiniQuotes,
32 DoubleQuotes,
33 VerticalBars,
34 AnyBrackets,
35 MiniBrackets,
36 Parentheses,
37 SquareBrackets,
38 CurlyBrackets,
39 AngleBrackets,
40 Argument,
41 IndentObj { include_below: bool },
42 Tag,
43 Method,
44 Class,
45 Comment,
46 EntireFile,
47}
48
49/// Selects a word text object.
50#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
51#[action(namespace = vim)]
52#[serde(deny_unknown_fields)]
53struct Word {
54 #[serde(default)]
55 ignore_punctuation: bool,
56}
57
58/// Selects a subword text object.
59#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
60#[action(namespace = vim)]
61#[serde(deny_unknown_fields)]
62struct Subword {
63 #[serde(default)]
64 ignore_punctuation: bool,
65}
66/// Selects text at the same indentation level.
67#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
68#[action(namespace = vim)]
69#[serde(deny_unknown_fields)]
70struct IndentObj {
71 #[serde(default)]
72 include_below: bool,
73}
74
75#[derive(Debug, Clone)]
76pub struct CandidateRange {
77 pub start: DisplayPoint,
78 pub end: DisplayPoint,
79}
80
81#[derive(Debug, Clone)]
82pub struct CandidateWithRanges {
83 candidate: CandidateRange,
84 open_range: Range<MultiBufferOffset>,
85 close_range: Range<MultiBufferOffset>,
86}
87
88/// Selects text at the same indentation level.
89#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
90#[action(namespace = vim)]
91#[serde(deny_unknown_fields)]
92struct Parentheses {
93 #[serde(default)]
94 opening: bool,
95}
96
97/// Selects text at the same indentation level.
98#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
99#[action(namespace = vim)]
100#[serde(deny_unknown_fields)]
101struct SquareBrackets {
102 #[serde(default)]
103 opening: bool,
104}
105
106/// Selects text at the same indentation level.
107#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
108#[action(namespace = vim)]
109#[serde(deny_unknown_fields)]
110struct AngleBrackets {
111 #[serde(default)]
112 opening: bool,
113}
114/// Selects text at the same indentation level.
115#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
116#[action(namespace = vim)]
117#[serde(deny_unknown_fields)]
118struct CurlyBrackets {
119 #[serde(default)]
120 opening: bool,
121}
122
123fn cover_or_next<I: Iterator<Item = (Range<MultiBufferOffset>, Range<MultiBufferOffset>)>>(
124 candidates: Option<I>,
125 caret: DisplayPoint,
126 map: &DisplaySnapshot,
127) -> Option<CandidateWithRanges> {
128 let caret_offset = caret.to_offset(map, Bias::Left);
129 let mut covering = vec![];
130 let mut next_ones = vec![];
131 let snapshot = map.buffer_snapshot();
132
133 if let Some(ranges) = candidates {
134 for (open_range, close_range) in ranges {
135 let start_off = open_range.start;
136 let end_off = close_range.end;
137 let candidate = CandidateWithRanges {
138 candidate: CandidateRange {
139 start: start_off.to_display_point(map),
140 end: end_off.to_display_point(map),
141 },
142 open_range: open_range.clone(),
143 close_range: close_range.clone(),
144 };
145
146 if open_range
147 .start
148 .to_offset(snapshot)
149 .to_display_point(map)
150 .row()
151 == caret_offset.to_display_point(map).row()
152 {
153 if start_off <= caret_offset && caret_offset < end_off {
154 covering.push(candidate);
155 } else if start_off >= caret_offset {
156 next_ones.push(candidate);
157 }
158 }
159 }
160 }
161
162 // 1) covering -> smallest width
163 if !covering.is_empty() {
164 return covering.into_iter().min_by_key(|r| {
165 r.candidate.end.to_offset(map, Bias::Right)
166 - r.candidate.start.to_offset(map, Bias::Left)
167 });
168 }
169
170 // 2) next -> closest by start
171 if !next_ones.is_empty() {
172 return next_ones.into_iter().min_by_key(|r| {
173 let start = r.candidate.start.to_offset(map, Bias::Left);
174 (start.0 as isize - caret_offset.0 as isize).abs()
175 });
176 }
177
178 None
179}
180
181type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool;
182
183struct DelimiterRange {
184 open: Range<MultiBufferOffset>,
185 close: Range<MultiBufferOffset>,
186}
187
188impl DelimiterRange {
189 fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range<DisplayPoint> {
190 if around {
191 self.open.start.to_display_point(map)..self.close.end.to_display_point(map)
192 } else {
193 self.open.end.to_display_point(map)..self.close.start.to_display_point(map)
194 }
195 }
196}
197
198fn find_mini_delimiters(
199 map: &DisplaySnapshot,
200 display_point: DisplayPoint,
201 around: bool,
202 is_valid_delimiter: &DelimiterPredicate,
203) -> Option<Range<DisplayPoint>> {
204 let point = map.clip_at_line_end(display_point).to_point(map);
205 let offset = point.to_offset(&map.buffer_snapshot());
206
207 let line_range = get_line_range(map, point);
208 let visible_line_range = get_visible_line_range(&line_range);
209
210 let snapshot = &map.buffer_snapshot();
211 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
212 let buffer = excerpt.buffer();
213 let buffer_offset = excerpt.map_offset_to_buffer(offset);
214
215 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
216 is_valid_delimiter(buffer, open.start, close.start)
217 };
218
219 // Try to find delimiters in visible range first
220 let ranges = map
221 .buffer_snapshot()
222 .bracket_ranges(visible_line_range)
223 .map(|ranges| {
224 ranges.filter_map(|(open, close)| {
225 // Convert the ranges from multibuffer space to buffer space as
226 // that is what `is_valid_delimiter` expects, otherwise it might
227 // panic as the values might be out of bounds.
228 let buffer_open = excerpt.map_range_to_buffer(open.clone());
229 let buffer_close = excerpt.map_range_to_buffer(close.clone());
230
231 if is_valid_delimiter(buffer, buffer_open.start.0, buffer_close.start.0) {
232 Some((open, close))
233 } else {
234 None
235 }
236 })
237 });
238
239 if let Some(candidate) = cover_or_next(ranges, display_point, map) {
240 return Some(
241 DelimiterRange {
242 open: candidate.open_range,
243 close: candidate.close_range,
244 }
245 .to_display_range(map, around),
246 );
247 }
248
249 // Fall back to innermost enclosing brackets
250 let (open_bracket, close_bracket) = buffer
251 .innermost_enclosing_bracket_ranges(buffer_offset..buffer_offset, Some(&bracket_filter))?;
252
253 Some(
254 DelimiterRange {
255 open: excerpt.map_range_from_buffer(
256 BufferOffset(open_bracket.start)..BufferOffset(open_bracket.end),
257 ),
258 close: excerpt.map_range_from_buffer(
259 BufferOffset(close_bracket.start)..BufferOffset(close_bracket.end),
260 ),
261 }
262 .to_display_range(map, around),
263 )
264}
265
266fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range<Point> {
267 let (start, mut end) = (
268 map.prev_line_boundary(point).0,
269 map.next_line_boundary(point).0,
270 );
271
272 if end == point {
273 end = map.max_point().to_point(map);
274 }
275
276 start..end
277}
278
279fn get_visible_line_range(line_range: &Range<Point>) -> Range<Point> {
280 let end_column = line_range.end.column.saturating_sub(1);
281 line_range.start..Point::new(line_range.end.row, end_column)
282}
283
284fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool {
285 matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`'))
286}
287
288fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool {
289 matches!(
290 buffer.chars_at(start).next(),
291 Some('(' | '[' | '{' | '<' | '|')
292 )
293}
294
295fn find_mini_quotes(
296 map: &DisplaySnapshot,
297 display_point: DisplayPoint,
298 around: bool,
299) -> Option<Range<DisplayPoint>> {
300 find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
301}
302
303fn find_mini_brackets(
304 map: &DisplaySnapshot,
305 display_point: DisplayPoint,
306 around: bool,
307) -> Option<Range<DisplayPoint>> {
308 find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
309}
310
311actions!(
312 vim,
313 [
314 /// Selects a sentence text object.
315 Sentence,
316 /// Selects a paragraph text object.
317 Paragraph,
318 /// Selects text within single quotes.
319 Quotes,
320 /// Selects text within backticks.
321 BackQuotes,
322 /// Selects text within the nearest quotes (single or double).
323 MiniQuotes,
324 /// Selects text within any type of quotes.
325 AnyQuotes,
326 /// Selects text within double quotes.
327 DoubleQuotes,
328 /// Selects text within vertical bars (pipes).
329 VerticalBars,
330 /// Selects text within the nearest brackets.
331 MiniBrackets,
332 /// Selects text within any type of brackets.
333 AnyBrackets,
334 /// Selects a function argument.
335 Argument,
336 /// Selects an HTML/XML tag.
337 Tag,
338 /// Selects a method or function.
339 Method,
340 /// Selects a class definition.
341 Class,
342 /// Selects a comment block.
343 Comment,
344 /// Selects the entire file.
345 EntireFile
346 ]
347);
348
349pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
350 Vim::action(
351 editor,
352 cx,
353 |vim, &Word { ignore_punctuation }: &Word, window, cx| {
354 vim.object(Object::Word { ignore_punctuation }, window, cx)
355 },
356 );
357 Vim::action(
358 editor,
359 cx,
360 |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
361 vim.object(Object::Subword { ignore_punctuation }, window, cx)
362 },
363 );
364 Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
365 vim.object(Object::Tag, window, cx)
366 });
367 Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
368 vim.object(Object::Sentence, window, cx)
369 });
370 Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
371 vim.object(Object::Paragraph, window, cx)
372 });
373 Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
374 vim.object(Object::Quotes, window, cx)
375 });
376 Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
377 vim.object(Object::BackQuotes, window, cx)
378 });
379 Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| {
380 vim.object(Object::MiniQuotes, window, cx)
381 });
382 Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| {
383 vim.object(Object::MiniBrackets, window, cx)
384 });
385 Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
386 vim.object(Object::AnyQuotes, window, cx)
387 });
388 Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
389 vim.object(Object::AnyBrackets, window, cx)
390 });
391 Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
392 vim.object(Object::BackQuotes, window, cx)
393 });
394 Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
395 vim.object(Object::DoubleQuotes, window, cx)
396 });
397 Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| {
398 vim.object_impl(Object::Parentheses, action.opening, window, cx)
399 });
400 Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
401 vim.object_impl(Object::SquareBrackets, action.opening, window, cx)
402 });
403 Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
404 vim.object_impl(Object::CurlyBrackets, action.opening, window, cx)
405 });
406 Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
407 vim.object_impl(Object::AngleBrackets, action.opening, window, cx)
408 });
409 Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
410 vim.object(Object::VerticalBars, window, cx)
411 });
412 Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
413 vim.object(Object::Argument, window, cx)
414 });
415 Vim::action(editor, cx, |vim, _: &Method, window, cx| {
416 vim.object(Object::Method, window, cx)
417 });
418 Vim::action(editor, cx, |vim, _: &Class, window, cx| {
419 vim.object(Object::Class, window, cx)
420 });
421 Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
422 vim.object(Object::EntireFile, window, cx)
423 });
424 Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
425 if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
426 vim.push_operator(Operator::Object { around: true }, window, cx);
427 }
428 vim.object(Object::Comment, window, cx)
429 });
430 Vim::action(
431 editor,
432 cx,
433 |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
434 vim.object(Object::IndentObj { include_below }, window, cx)
435 },
436 );
437}
438
439impl Vim {
440 fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
441 self.object_impl(object, false, window, cx);
442 }
443
444 fn object_impl(
445 &mut self,
446 object: Object,
447 opening: bool,
448 window: &mut Window,
449 cx: &mut Context<Self>,
450 ) {
451 let count = Self::take_count(cx);
452
453 match self.mode {
454 Mode::Normal | Mode::HelixNormal => {
455 self.normal_object(object, count, opening, window, cx)
456 }
457 Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
458 self.visual_object(object, count, window, cx)
459 }
460 Mode::Insert | Mode::Replace => {
461 // Shouldn't execute a text object in insert mode. Ignoring
462 }
463 }
464 }
465}
466
467impl Object {
468 pub fn is_multiline(self) -> bool {
469 match self {
470 Object::Word { .. }
471 | Object::Subword { .. }
472 | Object::Quotes
473 | Object::BackQuotes
474 | Object::AnyQuotes
475 | Object::MiniQuotes
476 | Object::VerticalBars
477 | Object::DoubleQuotes => false,
478 Object::Sentence
479 | Object::Paragraph
480 | Object::AnyBrackets
481 | Object::MiniBrackets
482 | Object::Parentheses
483 | Object::Tag
484 | Object::AngleBrackets
485 | Object::CurlyBrackets
486 | Object::SquareBrackets
487 | Object::Argument
488 | Object::Method
489 | Object::Class
490 | Object::EntireFile
491 | Object::Comment
492 | Object::IndentObj { .. } => true,
493 }
494 }
495
496 pub fn always_expands_both_ways(self) -> bool {
497 match self {
498 Object::Word { .. }
499 | Object::Subword { .. }
500 | Object::Sentence
501 | Object::Paragraph
502 | Object::Argument
503 | Object::IndentObj { .. } => false,
504 Object::Quotes
505 | Object::BackQuotes
506 | Object::AnyQuotes
507 | Object::MiniQuotes
508 | Object::DoubleQuotes
509 | Object::VerticalBars
510 | Object::AnyBrackets
511 | Object::MiniBrackets
512 | Object::Parentheses
513 | Object::SquareBrackets
514 | Object::Tag
515 | Object::Method
516 | Object::Class
517 | Object::Comment
518 | Object::EntireFile
519 | Object::CurlyBrackets
520 | Object::AngleBrackets => true,
521 }
522 }
523
524 pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
525 match self {
526 Object::Word { .. }
527 | Object::Subword { .. }
528 | Object::Sentence
529 | Object::Quotes
530 | Object::AnyQuotes
531 | Object::MiniQuotes
532 | Object::BackQuotes
533 | Object::DoubleQuotes => {
534 if current_mode == Mode::VisualBlock {
535 Mode::VisualBlock
536 } else {
537 Mode::Visual
538 }
539 }
540 Object::Parentheses
541 | Object::AnyBrackets
542 | Object::MiniBrackets
543 | Object::SquareBrackets
544 | Object::CurlyBrackets
545 | Object::AngleBrackets
546 | Object::VerticalBars
547 | Object::Tag
548 | Object::Comment
549 | Object::Argument
550 | Object::IndentObj { .. } => Mode::Visual,
551 Object::Method | Object::Class => {
552 if around {
553 Mode::VisualLine
554 } else {
555 Mode::Visual
556 }
557 }
558 Object::Paragraph | Object::EntireFile => Mode::VisualLine,
559 }
560 }
561
562 pub fn range(
563 self,
564 map: &DisplaySnapshot,
565 selection: Selection<DisplayPoint>,
566 around: bool,
567 times: Option<usize>,
568 ) -> Option<Range<DisplayPoint>> {
569 let relative_to = selection.head();
570 match self {
571 Object::Word { ignore_punctuation } => {
572 let count = times.unwrap_or(1);
573 if around {
574 around_word(map, relative_to, ignore_punctuation, count)
575 } else {
576 in_word(map, relative_to, ignore_punctuation, count).map(|range| {
577 // For iw with count > 1, vim includes trailing whitespace
578 if count > 1 {
579 let spans_multiple_lines = range.start.row() != range.end.row();
580 expand_to_include_whitespace(map, range, !spans_multiple_lines)
581 } else {
582 range
583 }
584 })
585 }
586 }
587 Object::Subword { ignore_punctuation } => {
588 if around {
589 around_subword(map, relative_to, ignore_punctuation)
590 } else {
591 in_subword(map, relative_to, ignore_punctuation)
592 }
593 }
594 Object::Sentence => sentence(map, relative_to, around),
595 //change others later
596 Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
597 Object::Quotes => {
598 surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
599 }
600 Object::BackQuotes => {
601 surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
602 }
603 Object::AnyQuotes => {
604 let quote_types = ['\'', '"', '`'];
605 let cursor_offset = relative_to.to_offset(map, Bias::Left);
606
607 // Find innermost range directly without collecting all ranges
608 let mut innermost = None;
609 let mut min_size = usize::MAX;
610
611 // First pass: find innermost enclosing range
612 for quote in quote_types {
613 if let Some(range) = surrounding_markers(
614 map,
615 relative_to,
616 around,
617 self.is_multiline(),
618 quote,
619 quote,
620 ) {
621 let start_offset = range.start.to_offset(map, Bias::Left);
622 let end_offset = range.end.to_offset(map, Bias::Right);
623
624 if cursor_offset >= start_offset && cursor_offset <= end_offset {
625 let size = end_offset - start_offset;
626 if size < min_size {
627 min_size = size;
628 innermost = Some(range);
629 }
630 }
631 }
632 }
633
634 if let Some(range) = innermost {
635 return Some(range);
636 }
637
638 // Fallback: find nearest pair if not inside any quotes
639 quote_types
640 .iter()
641 .flat_map(|"e| {
642 surrounding_markers(
643 map,
644 relative_to,
645 around,
646 self.is_multiline(),
647 quote,
648 quote,
649 )
650 })
651 .min_by_key(|range| {
652 let start_offset = range.start.to_offset(map, Bias::Left);
653 let end_offset = range.end.to_offset(map, Bias::Right);
654 if cursor_offset < start_offset {
655 (start_offset - cursor_offset) as isize
656 } else if cursor_offset > end_offset {
657 (cursor_offset - end_offset) as isize
658 } else {
659 0
660 }
661 })
662 }
663 Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
664 Object::DoubleQuotes => {
665 surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
666 }
667 Object::VerticalBars => {
668 surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
669 }
670 Object::Parentheses => {
671 surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
672 }
673 Object::Tag => {
674 let head = selection.head();
675 let range = selection.range();
676 surrounding_html_tag(map, head, range, around)
677 }
678 Object::AnyBrackets => {
679 let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
680 let cursor_offset = relative_to.to_offset(map, Bias::Left);
681
682 // Find innermost enclosing bracket range
683 let mut innermost = None;
684 let mut min_size = usize::MAX;
685
686 for &(open, close) in bracket_pairs.iter() {
687 if let Some(range) = surrounding_markers(
688 map,
689 relative_to,
690 around,
691 self.is_multiline(),
692 open,
693 close,
694 ) {
695 let start_offset = range.start.to_offset(map, Bias::Left);
696 let end_offset = range.end.to_offset(map, Bias::Right);
697
698 if cursor_offset >= start_offset && cursor_offset <= end_offset {
699 let size = end_offset - start_offset;
700 if size < min_size {
701 min_size = size;
702 innermost = Some(range);
703 }
704 }
705 }
706 }
707
708 if let Some(range) = innermost {
709 return Some(range);
710 }
711
712 // Fallback: find nearest bracket pair if not inside any
713 bracket_pairs
714 .iter()
715 .flat_map(|&(open, close)| {
716 surrounding_markers(
717 map,
718 relative_to,
719 around,
720 self.is_multiline(),
721 open,
722 close,
723 )
724 })
725 .min_by_key(|range| {
726 let start_offset = range.start.to_offset(map, Bias::Left);
727 let end_offset = range.end.to_offset(map, Bias::Right);
728 if cursor_offset < start_offset {
729 (start_offset - cursor_offset) as isize
730 } else if cursor_offset > end_offset {
731 (cursor_offset - end_offset) as isize
732 } else {
733 0
734 }
735 })
736 }
737 Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
738 Object::SquareBrackets => {
739 surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
740 }
741 Object::CurlyBrackets => {
742 surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
743 }
744 Object::AngleBrackets => {
745 surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
746 }
747 Object::Method => text_object(
748 map,
749 relative_to,
750 if around {
751 TextObject::AroundFunction
752 } else {
753 TextObject::InsideFunction
754 },
755 ),
756 Object::Comment => text_object(
757 map,
758 relative_to,
759 if around {
760 TextObject::AroundComment
761 } else {
762 TextObject::InsideComment
763 },
764 ),
765 Object::Class => text_object(
766 map,
767 relative_to,
768 if around {
769 TextObject::AroundClass
770 } else {
771 TextObject::InsideClass
772 },
773 ),
774 Object::Argument => argument(map, relative_to, around),
775 Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
776 Object::EntireFile => entire_file(map),
777 }
778 }
779
780 pub fn expand_selection(
781 self,
782 map: &DisplaySnapshot,
783 selection: &mut Selection<DisplayPoint>,
784 around: bool,
785 times: Option<usize>,
786 ) -> bool {
787 if let Some(range) = self.range(map, selection.clone(), around, times) {
788 selection.start = range.start;
789 selection.end = range.end;
790 true
791 } else {
792 false
793 }
794 }
795}
796
797/// Returns a range that surrounds the word `relative_to` is in.
798///
799/// If `relative_to` is at the start of a word, return the word.
800/// If `relative_to` is between words, return the space between.
801/// If `times` > 1, extend to include additional words.
802fn in_word(
803 map: &DisplaySnapshot,
804 relative_to: DisplayPoint,
805 ignore_punctuation: bool,
806 times: usize,
807) -> Option<Range<DisplayPoint>> {
808 // Use motion::right so that we consider the character under the cursor when looking for the start
809 let classifier = map
810 .buffer_snapshot()
811 .char_classifier_at(relative_to.to_point(map))
812 .ignore_punctuation(ignore_punctuation);
813 let start = movement::find_preceding_boundary_display_point(
814 map,
815 right(map, relative_to, 1),
816 movement::FindRange::SingleLine,
817 |left, right| classifier.kind(left) != classifier.kind(right),
818 );
819
820 let mut end =
821 movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
822 classifier.kind(left) != classifier.kind(right)
823 });
824
825 let is_boundary = |left: char, right: char| classifier.kind(left) != classifier.kind(right);
826
827 for _ in 1..times {
828 let kind_at_end = map
829 .buffer_chars_at(end.to_offset(map, Bias::Right))
830 .next()
831 .map(|(c, _)| classifier.kind(c));
832
833 // Skip whitespace but not punctuation (punctuation is its own word unit).
834 let next_end = if kind_at_end == Some(CharKind::Whitespace) {
835 let after_whitespace =
836 movement::find_boundary(map, end, FindRange::MultiLine, is_boundary);
837 movement::find_boundary(map, after_whitespace, FindRange::MultiLine, is_boundary)
838 } else {
839 movement::find_boundary(map, end, FindRange::MultiLine, is_boundary)
840 };
841 if next_end == end {
842 break;
843 }
844 end = next_end;
845 }
846
847 Some(start..end)
848}
849
850fn in_subword(
851 map: &DisplaySnapshot,
852 relative_to: DisplayPoint,
853 ignore_punctuation: bool,
854) -> Option<Range<DisplayPoint>> {
855 let offset = relative_to.to_offset(map, Bias::Left);
856 // Use motion::right so that we consider the character under the cursor when looking for the start
857 let classifier = map
858 .buffer_snapshot()
859 .char_classifier_at(relative_to.to_point(map))
860 .ignore_punctuation(ignore_punctuation);
861 let in_subword = map
862 .buffer_chars_at(offset)
863 .next()
864 .map(|(c, _)| {
865 let is_separator = "._-".contains(c);
866 !classifier.is_whitespace(c) && !is_separator
867 })
868 .unwrap_or(false);
869
870 let start = if in_subword {
871 movement::find_preceding_boundary_display_point(
872 map,
873 right(map, relative_to, 1),
874 movement::FindRange::SingleLine,
875 |left, right| {
876 let is_word_start = classifier.kind(left) != classifier.kind(right);
877 is_word_start || is_subword_start(left, right, "._-")
878 },
879 )
880 } else {
881 movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
882 let is_word_start = classifier.kind(left) != classifier.kind(right);
883 is_word_start || is_subword_start(left, right, "._-")
884 })
885 };
886
887 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
888 let is_word_end = classifier.kind(left) != classifier.kind(right);
889 is_word_end || is_subword_end(left, right, "._-")
890 });
891
892 Some(start..end)
893}
894
895pub fn surrounding_html_tag(
896 map: &DisplaySnapshot,
897 head: DisplayPoint,
898 range: Range<DisplayPoint>,
899 around: bool,
900) -> Option<Range<DisplayPoint>> {
901 fn read_tag(chars: impl Iterator<Item = char>) -> String {
902 chars
903 .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
904 .collect()
905 }
906 fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
907 if Some('<') != chars.next() {
908 return None;
909 }
910 Some(read_tag(chars))
911 }
912 fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
913 if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
914 return None;
915 }
916 Some(read_tag(chars))
917 }
918
919 let snapshot = &map.buffer_snapshot();
920 let offset = head.to_offset(map, Bias::Left);
921 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
922 let buffer = excerpt.buffer();
923 let offset = excerpt.map_offset_to_buffer(offset);
924
925 // Find the most closest to current offset
926 let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
927 let mut last_child_node = cursor.node();
928 while cursor.goto_first_child_for_byte(offset.0).is_some() {
929 last_child_node = cursor.node();
930 }
931
932 let mut last_child_node = Some(last_child_node);
933 while let Some(cur_node) = last_child_node {
934 if cur_node.child_count() >= 2 {
935 let first_child = cur_node.child(0);
936 let last_child = cur_node.child(cur_node.child_count() as u32 - 1);
937 if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
938 let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
939 let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
940 // It needs to be handled differently according to the selection length
941 let is_valid = if range.end.to_offset(map, Bias::Left)
942 - range.start.to_offset(map, Bias::Left)
943 <= 1
944 {
945 offset.0 <= last_child.end_byte()
946 } else {
947 excerpt
948 .map_offset_to_buffer(range.start.to_offset(map, Bias::Left))
949 .0
950 >= first_child.start_byte()
951 && excerpt
952 .map_offset_to_buffer(range.end.to_offset(map, Bias::Left))
953 .0
954 <= last_child.start_byte() + 1
955 };
956 if open_tag.is_some() && open_tag == close_tag && is_valid {
957 let range = if around {
958 first_child.byte_range().start..last_child.byte_range().end
959 } else {
960 first_child.byte_range().end..last_child.byte_range().start
961 };
962 let range = BufferOffset(range.start)..BufferOffset(range.end);
963 if excerpt.contains_buffer_range(range.clone()) {
964 let result = excerpt.map_range_from_buffer(range);
965 return Some(
966 result.start.to_display_point(map)..result.end.to_display_point(map),
967 );
968 }
969 }
970 }
971 }
972 last_child_node = cur_node.parent();
973 }
974 None
975}
976
977/// Returns a range that surrounds the word and following whitespace
978/// relative_to is in.
979///
980/// If `relative_to` is at the start of a word, return the word and following whitespace.
981/// If `relative_to` is between words, return the whitespace back and the following word.
982///
983/// if in word
984/// delete that word
985/// if there is whitespace following the word, delete that as well
986/// otherwise, delete any preceding whitespace
987/// otherwise
988/// delete whitespace around cursor
989/// delete word following the cursor
990/// If `times` > 1, extend to include additional words.
991fn around_word(
992 map: &DisplaySnapshot,
993 relative_to: DisplayPoint,
994 ignore_punctuation: bool,
995 times: usize,
996) -> Option<Range<DisplayPoint>> {
997 let offset = relative_to.to_offset(map, Bias::Left);
998 let classifier = map
999 .buffer_snapshot()
1000 .char_classifier_at(offset)
1001 .ignore_punctuation(ignore_punctuation);
1002 let in_word = map
1003 .buffer_chars_at(offset)
1004 .next()
1005 .map(|(c, _)| !classifier.is_whitespace(c))
1006 .unwrap_or(false);
1007
1008 if in_word {
1009 around_containing_word(map, relative_to, ignore_punctuation, times)
1010 } else {
1011 around_next_word(map, relative_to, ignore_punctuation, times)
1012 }
1013}
1014
1015fn around_subword(
1016 map: &DisplaySnapshot,
1017 relative_to: DisplayPoint,
1018 ignore_punctuation: bool,
1019) -> Option<Range<DisplayPoint>> {
1020 // Use motion::right so that we consider the character under the cursor when looking for the start
1021 let classifier = map
1022 .buffer_snapshot()
1023 .char_classifier_at(relative_to.to_point(map))
1024 .ignore_punctuation(ignore_punctuation);
1025 let start = movement::find_preceding_boundary_display_point(
1026 map,
1027 right(map, relative_to, 1),
1028 movement::FindRange::SingleLine,
1029 |left, right| {
1030 let is_separator = |c: char| "._-".contains(c);
1031 let is_word_start =
1032 classifier.kind(left) != classifier.kind(right) && !is_separator(left);
1033 is_word_start || is_subword_start(left, right, "._-")
1034 },
1035 );
1036
1037 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
1038 let is_separator = |c: char| "._-".contains(c);
1039 let is_word_end = classifier.kind(left) != classifier.kind(right) && !is_separator(right);
1040 is_word_end || is_subword_end(left, right, "._-")
1041 });
1042
1043 Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
1044}
1045
1046fn around_containing_word(
1047 map: &DisplaySnapshot,
1048 relative_to: DisplayPoint,
1049 ignore_punctuation: bool,
1050 times: usize,
1051) -> Option<Range<DisplayPoint>> {
1052 in_word(map, relative_to, ignore_punctuation, times).map(|range| {
1053 let spans_multiple_lines = range.start.row() != range.end.row();
1054 let stop_at_newline = !spans_multiple_lines;
1055
1056 let line_start = DisplayPoint::new(range.start.row(), 0);
1057 let is_first_word = map
1058 .buffer_chars_at(line_start.to_offset(map, Bias::Left))
1059 .take_while(|(ch, offset)| {
1060 offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace()
1061 })
1062 .count()
1063 > 0;
1064
1065 if is_first_word {
1066 // For first word on line, trim indentation
1067 let mut expanded = expand_to_include_whitespace(map, range.clone(), stop_at_newline);
1068 expanded.start = range.start;
1069 expanded
1070 } else {
1071 expand_to_include_whitespace(map, range, stop_at_newline)
1072 }
1073 })
1074}
1075
1076fn around_next_word(
1077 map: &DisplaySnapshot,
1078 relative_to: DisplayPoint,
1079 ignore_punctuation: bool,
1080 times: usize,
1081) -> Option<Range<DisplayPoint>> {
1082 let classifier = map
1083 .buffer_snapshot()
1084 .char_classifier_at(relative_to.to_point(map))
1085 .ignore_punctuation(ignore_punctuation);
1086 let start = movement::find_preceding_boundary_display_point(
1087 map,
1088 right(map, relative_to, 1),
1089 FindRange::SingleLine,
1090 |left, right| classifier.kind(left) != classifier.kind(right),
1091 );
1092
1093 let mut word_found = false;
1094 let mut end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
1095 let left_kind = classifier.kind(left);
1096 let right_kind = classifier.kind(right);
1097
1098 let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
1099
1100 if right_kind != CharKind::Whitespace {
1101 word_found = true;
1102 }
1103
1104 found
1105 });
1106
1107 for _ in 1..times {
1108 let next_end = movement::find_boundary(map, end, FindRange::MultiLine, |left, right| {
1109 let left_kind = classifier.kind(left);
1110 let right_kind = classifier.kind(right);
1111
1112 let in_word_unit = left_kind != CharKind::Whitespace;
1113 (in_word_unit && left_kind != right_kind) || right == '\n' && left == '\n'
1114 });
1115 if next_end == end {
1116 break;
1117 }
1118 end = next_end;
1119 }
1120
1121 Some(start..end)
1122}
1123
1124fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
1125 Some(DisplayPoint::zero()..map.max_point())
1126}
1127
1128fn text_object(
1129 map: &DisplaySnapshot,
1130 relative_to: DisplayPoint,
1131 target: TextObject,
1132) -> Option<Range<DisplayPoint>> {
1133 let snapshot = &map.buffer_snapshot();
1134 let offset = relative_to.to_offset(map, Bias::Left);
1135
1136 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1137 let buffer = excerpt.buffer();
1138 let offset = excerpt.map_offset_to_buffer(offset);
1139
1140 let mut matches: Vec<Range<usize>> = buffer
1141 .text_object_ranges(offset..offset, TreeSitterOptions::default())
1142 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1143 .collect();
1144 matches.sort_by_key(|r| r.end - r.start);
1145 if let Some(buffer_range) = matches.first() {
1146 let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1147 let range = excerpt.map_range_from_buffer(buffer_range);
1148 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1149 }
1150
1151 let around = target.around()?;
1152 let mut matches: Vec<Range<usize>> = buffer
1153 .text_object_ranges(offset..offset, TreeSitterOptions::default())
1154 .filter_map(|(r, m)| if m == around { Some(r) } else { None })
1155 .collect();
1156 matches.sort_by_key(|r| r.end - r.start);
1157 let around_range = matches.first()?;
1158
1159 let mut matches: Vec<Range<usize>> = buffer
1160 .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
1161 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1162 .collect();
1163 matches.sort_by_key(|r| r.start);
1164 if let Some(buffer_range) = matches.first()
1165 && !buffer_range.is_empty()
1166 {
1167 let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1168 let range = excerpt.map_range_from_buffer(buffer_range);
1169 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1170 }
1171 let around_range = BufferOffset(around_range.start)..BufferOffset(around_range.end);
1172 let buffer_range = excerpt.map_range_from_buffer(around_range);
1173 return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
1174}
1175
1176fn argument(
1177 map: &DisplaySnapshot,
1178 relative_to: DisplayPoint,
1179 around: bool,
1180) -> Option<Range<DisplayPoint>> {
1181 let snapshot = &map.buffer_snapshot();
1182 let offset = relative_to.to_offset(map, Bias::Left);
1183
1184 // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
1185 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1186 let buffer = excerpt.buffer();
1187
1188 fn comma_delimited_range_at(
1189 buffer: &BufferSnapshot,
1190 mut offset: BufferOffset,
1191 include_comma: bool,
1192 ) -> Option<Range<BufferOffset>> {
1193 // Seek to the first non-whitespace character
1194 offset += buffer
1195 .chars_at(offset)
1196 .take_while(|c| c.is_whitespace())
1197 .map(char::len_utf8)
1198 .sum::<usize>();
1199
1200 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
1201 // Filter out empty ranges
1202 if open.end == close.start {
1203 return false;
1204 }
1205
1206 // If the cursor is outside the brackets, ignore them
1207 if open.start == offset.0 || close.end == offset.0 {
1208 return false;
1209 }
1210
1211 // TODO: Is there any better way to filter out string brackets?
1212 // Used to filter out string brackets
1213 matches!(
1214 buffer.chars_at(open.start).next(),
1215 Some('(' | '[' | '{' | '<' | '|')
1216 )
1217 };
1218
1219 // Find the brackets containing the cursor
1220 let (open_bracket, close_bracket) =
1221 buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
1222
1223 let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start);
1224
1225 let layer = buffer.syntax_layer_at(offset)?;
1226 let node = layer.node();
1227 let mut cursor = node.walk();
1228
1229 // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
1230 let mut parent_covers_bracket_range = false;
1231 loop {
1232 let node = cursor.node();
1233 let range = node.byte_range();
1234 let covers_bracket_range =
1235 range.start == open_bracket.start && range.end == close_bracket.end;
1236 if parent_covers_bracket_range && !covers_bracket_range {
1237 break;
1238 }
1239 parent_covers_bracket_range = covers_bracket_range;
1240
1241 // Unable to find a child node with a parent that covers the bracket range, so no argument to select
1242 cursor.goto_first_child_for_byte(offset.0)?;
1243 }
1244
1245 let mut argument_node = cursor.node();
1246
1247 // If the child node is the open bracket, move to the next sibling.
1248 if argument_node.byte_range() == open_bracket {
1249 if !cursor.goto_next_sibling() {
1250 return Some(inner_bracket_range);
1251 }
1252 argument_node = cursor.node();
1253 }
1254 // While the child node is the close bracket or a comma, move to the previous sibling
1255 while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
1256 if !cursor.goto_previous_sibling() {
1257 return Some(inner_bracket_range);
1258 }
1259 argument_node = cursor.node();
1260 if argument_node.byte_range() == open_bracket {
1261 return Some(inner_bracket_range);
1262 }
1263 }
1264
1265 // The start and end of the argument range, defaulting to the start and end of the argument node
1266 let mut start = argument_node.start_byte();
1267 let mut end = argument_node.end_byte();
1268
1269 let mut needs_surrounding_comma = include_comma;
1270
1271 // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
1272 // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
1273 while cursor.goto_previous_sibling() {
1274 let prev = cursor.node();
1275
1276 if prev.start_byte() < open_bracket.end {
1277 start = open_bracket.end;
1278 break;
1279 } else if prev.kind() == "," {
1280 if needs_surrounding_comma {
1281 start = prev.start_byte();
1282 needs_surrounding_comma = false;
1283 }
1284 break;
1285 } else if prev.start_byte() < start {
1286 start = prev.start_byte();
1287 }
1288 }
1289
1290 // Do the same for the end of the argument, extending to next comma or the end of the argument list
1291 while cursor.goto_next_sibling() {
1292 let next = cursor.node();
1293
1294 if next.end_byte() > close_bracket.start {
1295 end = close_bracket.start;
1296 break;
1297 } else if next.kind() == "," {
1298 if needs_surrounding_comma {
1299 // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
1300 if let Some(next_arg) = next.next_sibling() {
1301 end = next_arg.start_byte();
1302 } else {
1303 end = next.end_byte();
1304 }
1305 }
1306 break;
1307 } else if next.end_byte() > end {
1308 end = next.end_byte();
1309 }
1310 }
1311
1312 Some(BufferOffset(start)..BufferOffset(end))
1313 }
1314
1315 let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
1316
1317 if excerpt.contains_buffer_range(result.clone()) {
1318 let result = excerpt.map_range_from_buffer(result);
1319 Some(result.start.to_display_point(map)..result.end.to_display_point(map))
1320 } else {
1321 None
1322 }
1323}
1324
1325fn indent(
1326 map: &DisplaySnapshot,
1327 relative_to: DisplayPoint,
1328 around: bool,
1329 include_below: bool,
1330) -> Option<Range<DisplayPoint>> {
1331 let point = relative_to.to_point(map);
1332 let row = point.row;
1333
1334 let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
1335
1336 // Loop backwards until we find a non-blank line with less indent
1337 let mut start_row = row;
1338 for prev_row in (0..row).rev() {
1339 let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
1340 if indent.is_line_empty() {
1341 continue;
1342 }
1343 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1344 if around {
1345 // When around is true, include the first line with less indent
1346 start_row = prev_row;
1347 }
1348 break;
1349 }
1350 start_row = prev_row;
1351 }
1352
1353 // Loop forwards until we find a non-blank line with less indent
1354 let mut end_row = row;
1355 let max_rows = map.buffer_snapshot().max_row().0;
1356 for next_row in (row + 1)..=max_rows {
1357 let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
1358 if indent.is_line_empty() {
1359 continue;
1360 }
1361 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1362 if around && include_below {
1363 // When around is true and including below, include this line
1364 end_row = next_row;
1365 }
1366 break;
1367 }
1368 end_row = next_row;
1369 }
1370
1371 let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1372 let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
1373 let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
1374 Some(start..end)
1375}
1376
1377fn sentence(
1378 map: &DisplaySnapshot,
1379 relative_to: DisplayPoint,
1380 around: bool,
1381) -> Option<Range<DisplayPoint>> {
1382 let mut start = None;
1383 let relative_offset = relative_to.to_offset(map, Bias::Left);
1384 let mut previous_end = relative_offset;
1385
1386 let mut chars = map.buffer_chars_at(previous_end).peekable();
1387
1388 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
1389 for (char, offset) in chars
1390 .peek()
1391 .cloned()
1392 .into_iter()
1393 .chain(map.reverse_buffer_chars_at(previous_end))
1394 {
1395 if is_sentence_end(map, offset) {
1396 break;
1397 }
1398
1399 if is_possible_sentence_start(char) {
1400 start = Some(offset);
1401 }
1402
1403 previous_end = offset;
1404 }
1405
1406 // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1407 let mut end = relative_offset;
1408 for (char, offset) in chars {
1409 if start.is_none() && is_possible_sentence_start(char) {
1410 if around {
1411 start = Some(offset);
1412 continue;
1413 } else {
1414 end = offset;
1415 break;
1416 }
1417 }
1418
1419 if char != '\n' {
1420 end = offset + char.len_utf8();
1421 }
1422
1423 if is_sentence_end(map, end) {
1424 break;
1425 }
1426 }
1427
1428 let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1429 if around {
1430 range = expand_to_include_whitespace(map, range, false);
1431 }
1432
1433 Some(range)
1434}
1435
1436fn is_possible_sentence_start(character: char) -> bool {
1437 !character.is_whitespace() && character != '.'
1438}
1439
1440const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1441const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1442const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1443fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool {
1444 let mut next_chars = map.buffer_chars_at(offset).peekable();
1445 if let Some((char, _)) = next_chars.next() {
1446 // We are at a double newline. This position is a sentence end.
1447 if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1448 return true;
1449 }
1450
1451 // The next text is not a valid whitespace. This is not a sentence end
1452 if !SENTENCE_END_WHITESPACE.contains(&char) {
1453 return false;
1454 }
1455 }
1456
1457 for (char, _) in map.reverse_buffer_chars_at(offset) {
1458 if SENTENCE_END_PUNCTUATION.contains(&char) {
1459 return true;
1460 }
1461
1462 if !SENTENCE_END_FILLERS.contains(&char) {
1463 return false;
1464 }
1465 }
1466
1467 false
1468}
1469
1470/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1471/// whitespace to the end first and falls back to the start if there was none.
1472pub fn expand_to_include_whitespace(
1473 map: &DisplaySnapshot,
1474 range: Range<DisplayPoint>,
1475 stop_at_newline: bool,
1476) -> Range<DisplayPoint> {
1477 let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1478 let mut whitespace_included = false;
1479
1480 let chars = map.buffer_chars_at(range.end).peekable();
1481 for (char, offset) in chars {
1482 if char == '\n' && stop_at_newline {
1483 break;
1484 }
1485
1486 if char.is_whitespace() {
1487 if char != '\n' || !stop_at_newline {
1488 range.end = offset + char.len_utf8();
1489 whitespace_included = true;
1490 }
1491 } else {
1492 // Found non whitespace. Quit out.
1493 break;
1494 }
1495 }
1496
1497 if !whitespace_included {
1498 for (char, point) in map.reverse_buffer_chars_at(range.start) {
1499 if char == '\n' && stop_at_newline {
1500 break;
1501 }
1502
1503 if !char.is_whitespace() {
1504 break;
1505 }
1506
1507 range.start = point;
1508 }
1509 }
1510
1511 range.start.to_display_point(map)..range.end.to_display_point(map)
1512}
1513
1514/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1515/// where `relative_to` is in. If `around`, principally returns the range ending
1516/// at the end of the next paragraph.
1517///
1518/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1519/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1520/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1521/// the trailing newline is not subject to subsequent operations).
1522///
1523/// Edge cases:
1524/// - If `around` and if the current paragraph is the last paragraph of the
1525/// file and is blank, then the selection results in an error.
1526/// - If `around` and if the current paragraph is the last paragraph of the
1527/// file and is not blank, then the returned range starts at the start of the
1528/// previous paragraph, if it exists.
1529fn paragraph(
1530 map: &DisplaySnapshot,
1531 relative_to: DisplayPoint,
1532 around: bool,
1533 times: usize,
1534) -> Option<Range<DisplayPoint>> {
1535 let mut paragraph_start = start_of_paragraph(map, relative_to);
1536 let mut paragraph_end = end_of_paragraph(map, relative_to);
1537
1538 for i in 0..times {
1539 let paragraph_end_row = paragraph_end.row();
1540 let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1541 let point = relative_to.to_point(map);
1542 let current_line_is_empty = map
1543 .buffer_snapshot()
1544 .is_line_blank(MultiBufferRow(point.row));
1545
1546 if around {
1547 if paragraph_ends_with_eof {
1548 if current_line_is_empty {
1549 return None;
1550 }
1551
1552 let paragraph_start_buffer_point = paragraph_start.to_point(map);
1553 if paragraph_start_buffer_point.row != 0 {
1554 let previous_paragraph_last_line_start =
1555 Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
1556 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1557 }
1558 } else {
1559 let paragraph_end_buffer_point = paragraph_end.to_point(map);
1560 let mut start_row = paragraph_end_buffer_point.row + 1;
1561 if i > 0 {
1562 start_row += 1;
1563 }
1564 let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1565 paragraph_end = end_of_paragraph(map, next_paragraph_start);
1566 }
1567 }
1568 }
1569
1570 let range = paragraph_start..paragraph_end;
1571 Some(range)
1572}
1573
1574/// Returns a position of the start of the current paragraph, where a paragraph
1575/// is defined as a run of non-blank lines or a run of blank lines.
1576pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1577 let point = display_point.to_point(map);
1578 if point.row == 0 {
1579 return DisplayPoint::zero();
1580 }
1581
1582 let is_current_line_blank = map
1583 .buffer_snapshot()
1584 .is_line_blank(MultiBufferRow(point.row));
1585
1586 for row in (0..point.row).rev() {
1587 let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1588 if blank != is_current_line_blank {
1589 return Point::new(row + 1, 0).to_display_point(map);
1590 }
1591 }
1592
1593 DisplayPoint::zero()
1594}
1595
1596/// Returns a position of the end of the current paragraph, where a paragraph
1597/// is defined as a run of non-blank lines or a run of blank lines.
1598/// The trailing newline is excluded from the paragraph.
1599pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1600 let point = display_point.to_point(map);
1601 if point.row == map.buffer_snapshot().max_row().0 {
1602 return map.max_point();
1603 }
1604
1605 let is_current_line_blank = map
1606 .buffer_snapshot()
1607 .is_line_blank(MultiBufferRow(point.row));
1608
1609 for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 {
1610 let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1611 if blank != is_current_line_blank {
1612 let previous_row = row - 1;
1613 return Point::new(
1614 previous_row,
1615 map.buffer_snapshot().line_len(MultiBufferRow(previous_row)),
1616 )
1617 .to_display_point(map);
1618 }
1619 }
1620
1621 map.max_point()
1622}
1623
1624pub fn surrounding_markers(
1625 map: &DisplaySnapshot,
1626 relative_to: DisplayPoint,
1627 around: bool,
1628 search_across_lines: bool,
1629 open_marker: char,
1630 close_marker: char,
1631) -> Option<Range<DisplayPoint>> {
1632 let point = relative_to.to_offset(map, Bias::Left);
1633
1634 let mut matched_closes = 0;
1635 let mut opening = None;
1636
1637 let mut before_ch = match movement::chars_before(map, point).next() {
1638 Some((ch, _)) => ch,
1639 _ => '\0',
1640 };
1641 if let Some((ch, range)) = movement::chars_after(map, point).next()
1642 && ch == open_marker
1643 && before_ch != '\\'
1644 {
1645 if open_marker == close_marker {
1646 let mut total = 0;
1647 for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
1648 if ch == '\n' {
1649 break;
1650 }
1651 if ch == open_marker && before_ch != '\\' {
1652 total += 1;
1653 }
1654 }
1655 if total % 2 == 0 {
1656 opening = Some(range)
1657 }
1658 } else {
1659 opening = Some(range)
1660 }
1661 }
1662
1663 if opening.is_none() {
1664 let mut chars_before = movement::chars_before(map, point).peekable();
1665 while let Some((ch, range)) = chars_before.next() {
1666 if ch == '\n' && !search_across_lines {
1667 break;
1668 }
1669
1670 if let Some((before_ch, _)) = chars_before.peek()
1671 && *before_ch == '\\'
1672 {
1673 continue;
1674 }
1675
1676 if ch == open_marker {
1677 if matched_closes == 0 {
1678 opening = Some(range);
1679 break;
1680 }
1681 matched_closes -= 1;
1682 } else if ch == close_marker {
1683 matched_closes += 1
1684 }
1685 }
1686 }
1687 if opening.is_none() {
1688 for (ch, range) in movement::chars_after(map, point) {
1689 if before_ch != '\\' {
1690 if ch == open_marker {
1691 opening = Some(range);
1692 break;
1693 } else if ch == close_marker {
1694 break;
1695 }
1696 }
1697
1698 before_ch = ch;
1699 }
1700 }
1701
1702 let mut opening = opening?;
1703
1704 let mut matched_opens = 0;
1705 let mut closing = None;
1706 before_ch = match movement::chars_before(map, opening.end).next() {
1707 Some((ch, _)) => ch,
1708 _ => '\0',
1709 };
1710 for (ch, range) in movement::chars_after(map, opening.end) {
1711 if ch == '\n' && !search_across_lines {
1712 break;
1713 }
1714
1715 if before_ch != '\\' {
1716 if ch == close_marker {
1717 if matched_opens == 0 {
1718 closing = Some(range);
1719 break;
1720 }
1721 matched_opens -= 1;
1722 } else if ch == open_marker {
1723 matched_opens += 1;
1724 }
1725 }
1726
1727 before_ch = ch;
1728 }
1729
1730 let mut closing = closing?;
1731
1732 if around && !search_across_lines {
1733 let mut found = false;
1734
1735 for (ch, range) in movement::chars_after(map, closing.end) {
1736 if ch.is_whitespace() && ch != '\n' {
1737 found = true;
1738 closing.end = range.end;
1739 } else {
1740 break;
1741 }
1742 }
1743
1744 if !found {
1745 for (ch, range) in movement::chars_before(map, opening.start) {
1746 if ch.is_whitespace() && ch != '\n' {
1747 opening.start = range.start
1748 } else {
1749 break;
1750 }
1751 }
1752 }
1753 }
1754
1755 // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1756 if !around && open_marker != close_marker {
1757 let start_point = opening.end.to_display_point(map);
1758 let end_point = closing.start.to_display_point(map);
1759 let start_offset = start_point.to_offset(map, Bias::Left);
1760 let end_offset = end_point.to_offset(map, Bias::Left);
1761
1762 if start_point.row() != end_point.row()
1763 && map
1764 .buffer_chars_at(start_offset)
1765 .take_while(|(_, offset)| offset < &end_offset)
1766 .any(|(ch, _)| !ch.is_whitespace())
1767 {
1768 let mut first_non_ws = None;
1769 let mut last_non_ws = None;
1770 for (ch, offset) in map.buffer_chars_at(start_offset) {
1771 if !ch.is_whitespace() {
1772 first_non_ws = Some(offset);
1773 break;
1774 }
1775 }
1776 for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1777 if !ch.is_whitespace() {
1778 last_non_ws = Some(offset + ch.len_utf8());
1779 break;
1780 }
1781 }
1782 if let Some(start) = first_non_ws {
1783 opening.end = start;
1784 }
1785 if let Some(end) = last_non_ws {
1786 closing.start = end;
1787 }
1788 }
1789 }
1790
1791 let result = if around {
1792 opening.start..closing.end
1793 } else {
1794 opening.end..closing.start
1795 };
1796
1797 Some(
1798 map.clip_point(result.start.to_display_point(map), Bias::Left)
1799 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1800 )
1801}
1802
1803#[cfg(test)]
1804mod test {
1805 use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
1806 use gpui::KeyBinding;
1807 use indoc::indoc;
1808 use text::Point;
1809
1810 use crate::{
1811 object::{AnyBrackets, AnyQuotes, MiniBrackets},
1812 state::Mode,
1813 test::{NeovimBackedTestContext, VimTestContext},
1814 };
1815
1816 const WORD_LOCATIONS: &str = indoc! {"
1817 The quick ˇbrowˇnˇ•••
1818 fox ˇjuˇmpsˇ over
1819 the lazy dogˇ••
1820 ˇ
1821 ˇ
1822 ˇ
1823 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1824 ˇ••
1825 ˇ••
1826 ˇ fox-jumpˇs over
1827 the lazy dogˇ•
1828 ˇ
1829 "
1830 };
1831
1832 #[gpui::test]
1833 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1834 let mut cx = NeovimBackedTestContext::new(cx).await;
1835
1836 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1837 .await
1838 .assert_matches();
1839 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1840 .await
1841 .assert_matches();
1842 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1843 .await
1844 .assert_matches();
1845 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1846 .await
1847 .assert_matches();
1848 }
1849
1850 #[gpui::test]
1851 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1852 let mut cx = NeovimBackedTestContext::new(cx).await;
1853
1854 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1855 .await
1856 .assert_matches();
1857 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1858 .await
1859 .assert_matches();
1860 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1861 .await
1862 .assert_matches();
1863 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1864 .await
1865 .assert_matches();
1866 }
1867
1868 #[gpui::test]
1869 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1870 let mut cx = NeovimBackedTestContext::new(cx).await;
1871
1872 /*
1873 cx.set_shared_state("The quick ˇbrown\nfox").await;
1874 cx.simulate_shared_keystrokes(["v"]).await;
1875 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1876 cx.simulate_shared_keystrokes(["i", "w"]).await;
1877 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1878 */
1879 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1880 cx.simulate_shared_keystrokes("v").await;
1881 cx.shared_state()
1882 .await
1883 .assert_eq("The quick brown\n«\nˇ»fox");
1884 cx.simulate_shared_keystrokes("i w").await;
1885 cx.shared_state()
1886 .await
1887 .assert_eq("The quick brown\n«\nˇ»fox");
1888
1889 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1890 .await
1891 .assert_matches();
1892 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1893 .await
1894 .assert_matches();
1895 }
1896
1897 #[gpui::test]
1898 async fn test_word_object_with_count(cx: &mut gpui::TestAppContext) {
1899 let mut cx = NeovimBackedTestContext::new(cx).await;
1900
1901 cx.set_shared_state("ˇone two three four").await;
1902 cx.simulate_shared_keystrokes("2 d a w").await;
1903 cx.shared_state().await.assert_matches();
1904
1905 cx.set_shared_state("ˇone two three four").await;
1906 cx.simulate_shared_keystrokes("d 2 a w").await;
1907 cx.shared_state().await.assert_matches();
1908
1909 // WORD (shift-w) ignores punctuation
1910 cx.set_shared_state("ˇone-two three-four five").await;
1911 cx.simulate_shared_keystrokes("2 d a shift-w").await;
1912 cx.shared_state().await.assert_matches();
1913
1914 cx.set_shared_state("ˇone two three four five").await;
1915 cx.simulate_shared_keystrokes("3 d a w").await;
1916 cx.shared_state().await.assert_matches();
1917
1918 // Multiplied counts: 2d2aw deletes 4 words (2*2)
1919 cx.set_shared_state("ˇone two three four five six").await;
1920 cx.simulate_shared_keystrokes("2 d 2 a w").await;
1921 cx.shared_state().await.assert_matches();
1922
1923 cx.set_shared_state("ˇone two three four").await;
1924 cx.simulate_shared_keystrokes("2 c a w").await;
1925 cx.shared_state().await.assert_matches();
1926
1927 cx.set_shared_state("ˇone two three four").await;
1928 cx.simulate_shared_keystrokes("2 y a w p").await;
1929 cx.shared_state().await.assert_matches();
1930
1931 // Punctuation: foo-bar is 3 word units (foo, -, bar), so 2aw selects "foo-"
1932 cx.set_shared_state(" ˇfoo-bar baz").await;
1933 cx.simulate_shared_keystrokes("2 d a w").await;
1934 cx.shared_state().await.assert_matches();
1935
1936 // Trailing whitespace counts as a word unit for iw
1937 cx.set_shared_state("ˇfoo ").await;
1938 cx.simulate_shared_keystrokes("2 d i w").await;
1939 cx.shared_state().await.assert_matches();
1940
1941 // Multi-line: count > 1 crosses line boundaries
1942 cx.set_shared_state("ˇone\ntwo\nthree").await;
1943 cx.simulate_shared_keystrokes("2 d a w").await;
1944 cx.shared_state().await.assert_matches();
1945
1946 cx.set_shared_state("ˇone\ntwo\nthree\nfour").await;
1947 cx.simulate_shared_keystrokes("3 d a w").await;
1948 cx.shared_state().await.assert_matches();
1949
1950 cx.set_shared_state("ˇone\ntwo\nthree").await;
1951 cx.simulate_shared_keystrokes("2 d i w").await;
1952 cx.shared_state().await.assert_matches();
1953
1954 cx.set_shared_state("one ˇtwo\nthree four").await;
1955 cx.simulate_shared_keystrokes("2 d a w").await;
1956 cx.shared_state().await.assert_matches();
1957 }
1958
1959 const PARAGRAPH_EXAMPLES: &[&str] = &[
1960 // Single line
1961 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1962 // Multiple lines without empty lines
1963 indoc! {"
1964 ˇThe quick brownˇ
1965 ˇfox jumps overˇ
1966 the lazy dog.ˇ
1967 "},
1968 // Heading blank paragraph and trailing normal paragraph
1969 indoc! {"
1970 ˇ
1971 ˇ
1972 ˇThe quick brown fox jumps
1973 ˇover the lazy dog.
1974 ˇ
1975 ˇ
1976 ˇThe quick brown fox jumpsˇ
1977 ˇover the lazy dog.ˇ
1978 "},
1979 // Inserted blank paragraph and trailing blank paragraph
1980 indoc! {"
1981 ˇThe quick brown fox jumps
1982 ˇover the lazy dog.
1983 ˇ
1984 ˇ
1985 ˇ
1986 ˇThe quick brown fox jumpsˇ
1987 ˇover the lazy dog.ˇ
1988 ˇ
1989 ˇ
1990 ˇ
1991 "},
1992 // "Blank" paragraph with whitespace characters
1993 indoc! {"
1994 ˇThe quick brown fox jumps
1995 over the lazy dog.
1996
1997 ˇ \t
1998
1999 ˇThe quick brown fox jumps
2000 over the lazy dog.ˇ
2001 ˇ
2002 ˇ \t
2003 \t \t
2004 "},
2005 // Single line "paragraphs", where selection size might be zero.
2006 indoc! {"
2007 ˇThe quick brown fox jumps over the lazy dog.
2008 ˇ
2009 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
2010 ˇ
2011 "},
2012 ];
2013
2014 #[gpui::test]
2015 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
2016 let mut cx = NeovimBackedTestContext::new(cx).await;
2017
2018 for paragraph_example in PARAGRAPH_EXAMPLES {
2019 cx.simulate_at_each_offset("c i p", paragraph_example)
2020 .await
2021 .assert_matches();
2022 cx.simulate_at_each_offset("c a p", paragraph_example)
2023 .await
2024 .assert_matches();
2025 }
2026 }
2027
2028 #[gpui::test]
2029 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
2030 let mut cx = NeovimBackedTestContext::new(cx).await;
2031
2032 for paragraph_example in PARAGRAPH_EXAMPLES {
2033 cx.simulate_at_each_offset("d i p", paragraph_example)
2034 .await
2035 .assert_matches();
2036 cx.simulate_at_each_offset("d a p", paragraph_example)
2037 .await
2038 .assert_matches();
2039 }
2040 }
2041
2042 #[gpui::test]
2043 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
2044 let mut cx = NeovimBackedTestContext::new(cx).await;
2045
2046 const EXAMPLES: &[&str] = &[
2047 indoc! {"
2048 ˇThe quick brown
2049 fox jumps over
2050 the lazy dog.
2051 "},
2052 indoc! {"
2053 ˇ
2054
2055 ˇThe quick brown fox jumps
2056 over the lazy dog.
2057 ˇ
2058
2059 ˇThe quick brown fox jumps
2060 over the lazy dog.
2061 "},
2062 indoc! {"
2063 ˇThe quick brown fox jumps over the lazy dog.
2064 ˇ
2065 ˇThe quick brown fox jumps over the lazy dog.
2066
2067 "},
2068 ];
2069
2070 for paragraph_example in EXAMPLES {
2071 cx.simulate_at_each_offset("v i p", paragraph_example)
2072 .await
2073 .assert_matches();
2074 cx.simulate_at_each_offset("v a p", paragraph_example)
2075 .await
2076 .assert_matches();
2077 }
2078 }
2079
2080 #[gpui::test]
2081 async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2082 let mut cx = NeovimBackedTestContext::new(cx).await;
2083
2084 const WRAPPING_EXAMPLE: &str = indoc! {"
2085 ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2086
2087 ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2088
2089 ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2090 "};
2091
2092 cx.set_shared_wrap(20).await;
2093
2094 cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
2095 .await
2096 .assert_matches();
2097 cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
2098 .await
2099 .assert_matches();
2100 }
2101
2102 #[gpui::test]
2103 async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2104 let mut cx = NeovimBackedTestContext::new(cx).await;
2105
2106 const WRAPPING_EXAMPLE: &str = indoc! {"
2107 ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2108
2109 ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2110
2111 ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2112 "};
2113
2114 cx.set_shared_wrap(20).await;
2115
2116 cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
2117 .await
2118 .assert_matches();
2119 cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
2120 .await
2121 .assert_matches();
2122 }
2123
2124 #[gpui::test]
2125 async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
2126 let mut cx = NeovimBackedTestContext::new(cx).await;
2127
2128 cx.set_shared_state(indoc! {"
2129 a
2130 ˇ•
2131 aaaaaaaaaaaaa
2132 "})
2133 .await;
2134
2135 cx.simulate_shared_keystrokes("d i p").await;
2136 cx.shared_state().await.assert_eq(indoc! {"
2137 a
2138 aaaaaaaˇaaaaaa
2139 "});
2140 }
2141
2142 #[gpui::test]
2143 async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2144 let mut cx = NeovimBackedTestContext::new(cx).await;
2145
2146 const WRAPPING_EXAMPLE: &str = indoc! {"
2147 ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2148
2149 ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2150
2151 ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2152 "};
2153
2154 cx.set_shared_wrap(20).await;
2155
2156 cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
2157 .await
2158 .assert_matches();
2159 cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
2160 .await
2161 .assert_matches();
2162 }
2163
2164 // Test string with "`" for opening surrounders and "'" for closing surrounders
2165 const SURROUNDING_MARKER_STRING: &str = indoc! {"
2166 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
2167 'ˇfox juˇmps ov`ˇer
2168 the ˇlazy d'o`ˇg"};
2169
2170 const SURROUNDING_OBJECTS: &[(char, char)] = &[
2171 ('"', '"'), // Double Quote
2172 ('(', ')'), // Parentheses
2173 ];
2174
2175 #[gpui::test]
2176 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2177 let mut cx = NeovimBackedTestContext::new(cx).await;
2178
2179 for (start, end) in SURROUNDING_OBJECTS {
2180 let marked_string = SURROUNDING_MARKER_STRING
2181 .replace('`', &start.to_string())
2182 .replace('\'', &end.to_string());
2183
2184 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
2185 .await
2186 .assert_matches();
2187 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
2188 .await
2189 .assert_matches();
2190 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
2191 .await
2192 .assert_matches();
2193 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
2194 .await
2195 .assert_matches();
2196 }
2197 }
2198 #[gpui::test]
2199 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2200 let mut cx = NeovimBackedTestContext::new(cx).await;
2201 cx.set_shared_wrap(12).await;
2202
2203 cx.set_shared_state(indoc! {
2204 "\"ˇhello world\"!"
2205 })
2206 .await;
2207 cx.simulate_shared_keystrokes("v i \"").await;
2208 cx.shared_state().await.assert_eq(indoc! {
2209 "\"«hello worldˇ»\"!"
2210 });
2211
2212 cx.set_shared_state(indoc! {
2213 "\"hˇello world\"!"
2214 })
2215 .await;
2216 cx.simulate_shared_keystrokes("v i \"").await;
2217 cx.shared_state().await.assert_eq(indoc! {
2218 "\"«hello worldˇ»\"!"
2219 });
2220
2221 cx.set_shared_state(indoc! {
2222 "helˇlo \"world\"!"
2223 })
2224 .await;
2225 cx.simulate_shared_keystrokes("v i \"").await;
2226 cx.shared_state().await.assert_eq(indoc! {
2227 "hello \"«worldˇ»\"!"
2228 });
2229
2230 cx.set_shared_state(indoc! {
2231 "hello \"wˇorld\"!"
2232 })
2233 .await;
2234 cx.simulate_shared_keystrokes("v i \"").await;
2235 cx.shared_state().await.assert_eq(indoc! {
2236 "hello \"«worldˇ»\"!"
2237 });
2238
2239 cx.set_shared_state(indoc! {
2240 "hello \"wˇorld\"!"
2241 })
2242 .await;
2243 cx.simulate_shared_keystrokes("v a \"").await;
2244 cx.shared_state().await.assert_eq(indoc! {
2245 "hello« \"world\"ˇ»!"
2246 });
2247
2248 cx.set_shared_state(indoc! {
2249 "hello \"wˇorld\" !"
2250 })
2251 .await;
2252 cx.simulate_shared_keystrokes("v a \"").await;
2253 cx.shared_state().await.assert_eq(indoc! {
2254 "hello «\"world\" ˇ»!"
2255 });
2256
2257 cx.set_shared_state(indoc! {
2258 "hello \"wˇorld\"•
2259 goodbye"
2260 })
2261 .await;
2262 cx.simulate_shared_keystrokes("v a \"").await;
2263 cx.shared_state().await.assert_eq(indoc! {
2264 "hello «\"world\" ˇ»
2265 goodbye"
2266 });
2267 }
2268
2269 #[gpui::test]
2270 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2271 let mut cx = VimTestContext::new(cx, true).await;
2272
2273 cx.set_state(
2274 indoc! {
2275 "func empty(a string) bool {
2276 if a == \"\" {
2277 return true
2278 }
2279 ˇreturn false
2280 }"
2281 },
2282 Mode::Normal,
2283 );
2284 cx.simulate_keystrokes("v i {");
2285 cx.assert_state(
2286 indoc! {
2287 "func empty(a string) bool {
2288 «if a == \"\" {
2289 return true
2290 }
2291 return falseˇ»
2292 }"
2293 },
2294 Mode::Visual,
2295 );
2296
2297 cx.set_state(
2298 indoc! {
2299 "func empty(a string) bool {
2300 if a == \"\" {
2301 ˇreturn true
2302 }
2303 return false
2304 }"
2305 },
2306 Mode::Normal,
2307 );
2308 cx.simulate_keystrokes("v i {");
2309 cx.assert_state(
2310 indoc! {
2311 "func empty(a string) bool {
2312 if a == \"\" {
2313 «return trueˇ»
2314 }
2315 return false
2316 }"
2317 },
2318 Mode::Visual,
2319 );
2320
2321 cx.set_state(
2322 indoc! {
2323 "func empty(a string) bool {
2324 if a == \"\" ˇ{
2325 return true
2326 }
2327 return false
2328 }"
2329 },
2330 Mode::Normal,
2331 );
2332 cx.simulate_keystrokes("v i {");
2333 cx.assert_state(
2334 indoc! {
2335 "func empty(a string) bool {
2336 if a == \"\" {
2337 «return trueˇ»
2338 }
2339 return false
2340 }"
2341 },
2342 Mode::Visual,
2343 );
2344
2345 cx.set_state(
2346 indoc! {
2347 "func empty(a string) bool {
2348 if a == \"\" {
2349 return true
2350 }
2351 return false
2352 ˇ}"
2353 },
2354 Mode::Normal,
2355 );
2356 cx.simulate_keystrokes("v i {");
2357 cx.assert_state(
2358 indoc! {
2359 "func empty(a string) bool {
2360 «if a == \"\" {
2361 return true
2362 }
2363 return falseˇ»
2364 }"
2365 },
2366 Mode::Visual,
2367 );
2368
2369 cx.set_state(
2370 indoc! {
2371 "func empty(a string) bool {
2372 if a == \"\" {
2373 ˇ
2374
2375 }"
2376 },
2377 Mode::Normal,
2378 );
2379 cx.simulate_keystrokes("c i {");
2380 cx.assert_state(
2381 indoc! {
2382 "func empty(a string) bool {
2383 if a == \"\" {ˇ}"
2384 },
2385 Mode::Insert,
2386 );
2387 }
2388
2389 #[gpui::test]
2390 async fn test_singleline_surrounding_character_objects_with_escape(
2391 cx: &mut gpui::TestAppContext,
2392 ) {
2393 let mut cx = NeovimBackedTestContext::new(cx).await;
2394 cx.set_shared_state(indoc! {
2395 "h\"e\\\"lˇlo \\\"world\"!"
2396 })
2397 .await;
2398 cx.simulate_shared_keystrokes("v i \"").await;
2399 cx.shared_state().await.assert_eq(indoc! {
2400 "h\"«e\\\"llo \\\"worldˇ»\"!"
2401 });
2402
2403 cx.set_shared_state(indoc! {
2404 "hello \"teˇst \\\"inside\\\" world\""
2405 })
2406 .await;
2407 cx.simulate_shared_keystrokes("v i \"").await;
2408 cx.shared_state().await.assert_eq(indoc! {
2409 "hello \"«test \\\"inside\\\" worldˇ»\""
2410 });
2411 }
2412
2413 #[gpui::test]
2414 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2415 let mut cx = VimTestContext::new(cx, true).await;
2416 cx.set_state(
2417 indoc! {"
2418 fn boop() {
2419 baz(ˇ|a, b| { bar(|j, k| { })})
2420 }"
2421 },
2422 Mode::Normal,
2423 );
2424 cx.simulate_keystrokes("c i |");
2425 cx.assert_state(
2426 indoc! {"
2427 fn boop() {
2428 baz(|ˇ| { bar(|j, k| { })})
2429 }"
2430 },
2431 Mode::Insert,
2432 );
2433 cx.simulate_keystrokes("escape 1 8 |");
2434 cx.assert_state(
2435 indoc! {"
2436 fn boop() {
2437 baz(|| { bar(ˇ|j, k| { })})
2438 }"
2439 },
2440 Mode::Normal,
2441 );
2442
2443 cx.simulate_keystrokes("v a |");
2444 cx.assert_state(
2445 indoc! {"
2446 fn boop() {
2447 baz(|| { bar(«|j, k| ˇ»{ })})
2448 }"
2449 },
2450 Mode::Visual,
2451 );
2452 }
2453
2454 #[gpui::test]
2455 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2456 let mut cx = VimTestContext::new(cx, true).await;
2457
2458 // Generic arguments
2459 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2460 cx.simulate_keystrokes("v i a");
2461 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2462
2463 // Function arguments
2464 cx.set_state(
2465 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2466 Mode::Normal,
2467 );
2468 cx.simulate_keystrokes("d a a");
2469 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2470
2471 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2472 cx.simulate_keystrokes("v a a");
2473 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2474
2475 // Tuple, vec, and array arguments
2476 cx.set_state(
2477 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2478 Mode::Normal,
2479 );
2480 cx.simulate_keystrokes("c i a");
2481 cx.assert_state(
2482 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2483 Mode::Insert,
2484 );
2485
2486 // TODO regressed with the up-to-date Rust grammar.
2487 // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2488 // cx.simulate_keystrokes("c a a");
2489 // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2490
2491 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2492 cx.simulate_keystrokes("c i a");
2493 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2494
2495 cx.set_state(
2496 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2497 Mode::Normal,
2498 );
2499 cx.simulate_keystrokes("c a a");
2500 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2501
2502 // Cursor immediately before / after brackets
2503 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2504 cx.simulate_keystrokes("v i a");
2505 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2506
2507 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2508 cx.simulate_keystrokes("v i a");
2509 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2510 }
2511
2512 #[gpui::test]
2513 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2514 let mut cx = VimTestContext::new(cx, true).await;
2515
2516 // Base use case
2517 cx.set_state(
2518 indoc! {"
2519 fn boop() {
2520 // Comment
2521 baz();ˇ
2522
2523 loop {
2524 bar(1);
2525 bar(2);
2526 }
2527
2528 result
2529 }
2530 "},
2531 Mode::Normal,
2532 );
2533 cx.simulate_keystrokes("v i i");
2534 cx.assert_state(
2535 indoc! {"
2536 fn boop() {
2537 « // Comment
2538 baz();
2539
2540 loop {
2541 bar(1);
2542 bar(2);
2543 }
2544
2545 resultˇ»
2546 }
2547 "},
2548 Mode::Visual,
2549 );
2550
2551 // Around indent (include line above)
2552 cx.set_state(
2553 indoc! {"
2554 const ABOVE: str = true;
2555 fn boop() {
2556
2557 hello();
2558 worˇld()
2559 }
2560 "},
2561 Mode::Normal,
2562 );
2563 cx.simulate_keystrokes("v a i");
2564 cx.assert_state(
2565 indoc! {"
2566 const ABOVE: str = true;
2567 «fn boop() {
2568
2569 hello();
2570 world()ˇ»
2571 }
2572 "},
2573 Mode::Visual,
2574 );
2575
2576 // Around indent (include line above & below)
2577 cx.set_state(
2578 indoc! {"
2579 const ABOVE: str = true;
2580 fn boop() {
2581 hellˇo();
2582 world()
2583
2584 }
2585 const BELOW: str = true;
2586 "},
2587 Mode::Normal,
2588 );
2589 cx.simulate_keystrokes("c a shift-i");
2590 cx.assert_state(
2591 indoc! {"
2592 const ABOVE: str = true;
2593 ˇ
2594 const BELOW: str = true;
2595 "},
2596 Mode::Insert,
2597 );
2598 }
2599
2600 #[gpui::test]
2601 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2602 let mut cx = NeovimBackedTestContext::new(cx).await;
2603
2604 for (start, end) in SURROUNDING_OBJECTS {
2605 let marked_string = SURROUNDING_MARKER_STRING
2606 .replace('`', &start.to_string())
2607 .replace('\'', &end.to_string());
2608
2609 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2610 .await
2611 .assert_matches();
2612 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2613 .await
2614 .assert_matches();
2615 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2616 .await
2617 .assert_matches();
2618 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2619 .await
2620 .assert_matches();
2621 }
2622 }
2623
2624 #[gpui::test]
2625 async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2626 let mut cx = VimTestContext::new(cx, true).await;
2627 cx.update(|_, cx| {
2628 cx.bind_keys([KeyBinding::new(
2629 "q",
2630 AnyQuotes,
2631 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2632 )]);
2633 });
2634
2635 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2636 // the false string in the middle should be considered
2637 (
2638 "c i q",
2639 "'first' false ˇstring 'second'",
2640 "'first'ˇ'second'",
2641 Mode::Insert,
2642 ),
2643 // Single quotes
2644 (
2645 "c i q",
2646 "Thisˇ is a 'quote' example.",
2647 "This is a 'ˇ' example.",
2648 Mode::Insert,
2649 ),
2650 (
2651 "c a q",
2652 "Thisˇ is a 'quote' example.",
2653 "This is a ˇexample.",
2654 Mode::Insert,
2655 ),
2656 (
2657 "c i q",
2658 "This is a \"simple 'qˇuote'\" example.",
2659 "This is a \"simple 'ˇ'\" example.",
2660 Mode::Insert,
2661 ),
2662 (
2663 "c a q",
2664 "This is a \"simple 'qˇuote'\" example.",
2665 "This is a \"simpleˇ\" example.",
2666 Mode::Insert,
2667 ),
2668 (
2669 "c i q",
2670 "This is a 'qˇuote' example.",
2671 "This is a 'ˇ' example.",
2672 Mode::Insert,
2673 ),
2674 (
2675 "c a q",
2676 "This is a 'qˇuote' example.",
2677 "This is a ˇexample.",
2678 Mode::Insert,
2679 ),
2680 (
2681 "d i q",
2682 "This is a 'qˇuote' example.",
2683 "This is a 'ˇ' example.",
2684 Mode::Normal,
2685 ),
2686 (
2687 "d a q",
2688 "This is a 'qˇuote' example.",
2689 "This is a ˇexample.",
2690 Mode::Normal,
2691 ),
2692 // Double quotes
2693 (
2694 "c i q",
2695 "This is a \"qˇuote\" example.",
2696 "This is a \"ˇ\" example.",
2697 Mode::Insert,
2698 ),
2699 (
2700 "c a q",
2701 "This is a \"qˇuote\" example.",
2702 "This is a ˇexample.",
2703 Mode::Insert,
2704 ),
2705 (
2706 "d i q",
2707 "This is a \"qˇuote\" example.",
2708 "This is a \"ˇ\" example.",
2709 Mode::Normal,
2710 ),
2711 (
2712 "d a q",
2713 "This is a \"qˇuote\" example.",
2714 "This is a ˇexample.",
2715 Mode::Normal,
2716 ),
2717 // Back quotes
2718 (
2719 "c i q",
2720 "This is a `qˇuote` example.",
2721 "This is a `ˇ` example.",
2722 Mode::Insert,
2723 ),
2724 (
2725 "c a q",
2726 "This is a `qˇuote` example.",
2727 "This is a ˇexample.",
2728 Mode::Insert,
2729 ),
2730 (
2731 "d i q",
2732 "This is a `qˇuote` example.",
2733 "This is a `ˇ` example.",
2734 Mode::Normal,
2735 ),
2736 (
2737 "d a q",
2738 "This is a `qˇuote` example.",
2739 "This is a ˇexample.",
2740 Mode::Normal,
2741 ),
2742 ];
2743
2744 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2745 cx.set_state(initial_state, Mode::Normal);
2746
2747 cx.simulate_keystrokes(keystrokes);
2748
2749 cx.assert_state(expected_state, *expected_mode);
2750 }
2751
2752 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2753 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2754 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2755 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2756 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2757 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2758 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2759 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2760 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2761 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2762 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2763 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2764 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2765 ];
2766
2767 for (keystrokes, initial_state, mode) in INVALID_CASES {
2768 cx.set_state(initial_state, Mode::Normal);
2769
2770 cx.simulate_keystrokes(keystrokes);
2771
2772 cx.assert_state(initial_state, *mode);
2773 }
2774 }
2775
2776 #[gpui::test]
2777 async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2778 let mut cx = VimTestContext::new_typescript(cx).await;
2779
2780 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2781 // Special cases from mini.ai plugin
2782 // the false string in the middle should not be considered
2783 (
2784 "c i q",
2785 "'first' false ˇstring 'second'",
2786 "'first' false string 'ˇ'",
2787 Mode::Insert,
2788 ),
2789 // Multiline support :)! Same behavior as mini.ai plugin
2790 (
2791 "c i q",
2792 indoc! {"
2793 `
2794 first
2795 middle ˇstring
2796 second
2797 `
2798 "},
2799 indoc! {"
2800 `ˇ`
2801 "},
2802 Mode::Insert,
2803 ),
2804 // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2805 // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2806 // Bug reference: https://github.com/zed-industries/zed/issues/23889
2807 ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2808 // Single quotes
2809 (
2810 "c i q",
2811 "Thisˇ is a 'quote' example.",
2812 "This is a 'ˇ' example.",
2813 Mode::Insert,
2814 ),
2815 (
2816 "c a q",
2817 "Thisˇ is a 'quote' example.",
2818 "This is a ˇ example.", // same mini.ai plugin behavior
2819 Mode::Insert,
2820 ),
2821 (
2822 "c i q",
2823 "This is a \"simple 'qˇuote'\" example.",
2824 "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2825 Mode::Insert,
2826 ),
2827 (
2828 "c a q",
2829 "This is a \"simple 'qˇuote'\" example.",
2830 "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2831 Mode::Insert,
2832 ),
2833 (
2834 "c i q",
2835 "This is a 'qˇuote' example.",
2836 "This is a 'ˇ' example.",
2837 Mode::Insert,
2838 ),
2839 (
2840 "c a q",
2841 "This is a 'qˇuote' example.",
2842 "This is a ˇ example.", // same mini.ai plugin behavior
2843 Mode::Insert,
2844 ),
2845 (
2846 "d i q",
2847 "This is a 'qˇuote' example.",
2848 "This is a 'ˇ' example.",
2849 Mode::Normal,
2850 ),
2851 (
2852 "d a q",
2853 "This is a 'qˇuote' example.",
2854 "This is a ˇ example.", // same mini.ai plugin behavior
2855 Mode::Normal,
2856 ),
2857 // Double quotes
2858 (
2859 "c i q",
2860 "This is a \"qˇuote\" example.",
2861 "This is a \"ˇ\" example.",
2862 Mode::Insert,
2863 ),
2864 (
2865 "c a q",
2866 "This is a \"qˇuote\" example.",
2867 "This is a ˇ example.", // same mini.ai plugin behavior
2868 Mode::Insert,
2869 ),
2870 (
2871 "d i q",
2872 "This is a \"qˇuote\" example.",
2873 "This is a \"ˇ\" example.",
2874 Mode::Normal,
2875 ),
2876 (
2877 "d a q",
2878 "This is a \"qˇuote\" example.",
2879 "This is a ˇ example.", // same mini.ai plugin behavior
2880 Mode::Normal,
2881 ),
2882 // Back quotes
2883 (
2884 "c i q",
2885 "This is a `qˇuote` example.",
2886 "This is a `ˇ` example.",
2887 Mode::Insert,
2888 ),
2889 (
2890 "c a q",
2891 "This is a `qˇuote` example.",
2892 "This is a ˇ example.", // same mini.ai plugin behavior
2893 Mode::Insert,
2894 ),
2895 (
2896 "d i q",
2897 "This is a `qˇuote` example.",
2898 "This is a `ˇ` example.",
2899 Mode::Normal,
2900 ),
2901 (
2902 "d a q",
2903 "This is a `qˇuote` example.",
2904 "This is a ˇ example.", // same mini.ai plugin behavior
2905 Mode::Normal,
2906 ),
2907 ];
2908
2909 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2910 cx.set_state(initial_state, Mode::Normal);
2911 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2912 cx.simulate_keystrokes(keystrokes);
2913 cx.assert_state(expected_state, *expected_mode);
2914 }
2915
2916 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2917 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2918 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2919 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2920 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2921 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2922 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2923 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2924 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2925 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2926 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2927 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2928 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2929 ];
2930
2931 for (keystrokes, initial_state, mode) in INVALID_CASES {
2932 cx.set_state(initial_state, Mode::Normal);
2933 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2934 cx.simulate_keystrokes(keystrokes);
2935 cx.assert_state(initial_state, *mode);
2936 }
2937 }
2938
2939 #[gpui::test]
2940 async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2941 let mut cx = VimTestContext::new(cx, true).await;
2942 cx.update(|_, cx| {
2943 cx.bind_keys([KeyBinding::new(
2944 "b",
2945 AnyBrackets,
2946 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2947 )]);
2948 });
2949
2950 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2951 (
2952 "c i b",
2953 indoc! {"
2954 {
2955 {
2956 ˇprint('hello')
2957 }
2958 }
2959 "},
2960 indoc! {"
2961 {
2962 {
2963 ˇ
2964 }
2965 }
2966 "},
2967 Mode::Insert,
2968 ),
2969 // Bracket (Parentheses)
2970 (
2971 "c i b",
2972 "Thisˇ is a (simple [quote]) example.",
2973 "This is a (ˇ) example.",
2974 Mode::Insert,
2975 ),
2976 (
2977 "c i b",
2978 "This is a [simple (qˇuote)] example.",
2979 "This is a [simple (ˇ)] example.",
2980 Mode::Insert,
2981 ),
2982 (
2983 "c a b",
2984 "This is a [simple (qˇuote)] example.",
2985 "This is a [simple ˇ] example.",
2986 Mode::Insert,
2987 ),
2988 (
2989 "c a b",
2990 "Thisˇ is a (simple [quote]) example.",
2991 "This is a ˇ example.",
2992 Mode::Insert,
2993 ),
2994 (
2995 "c i b",
2996 "This is a (qˇuote) example.",
2997 "This is a (ˇ) example.",
2998 Mode::Insert,
2999 ),
3000 (
3001 "c a b",
3002 "This is a (qˇuote) example.",
3003 "This is a ˇ example.",
3004 Mode::Insert,
3005 ),
3006 (
3007 "d i b",
3008 "This is a (qˇuote) example.",
3009 "This is a (ˇ) example.",
3010 Mode::Normal,
3011 ),
3012 (
3013 "d a b",
3014 "This is a (qˇuote) example.",
3015 "This is a ˇ example.",
3016 Mode::Normal,
3017 ),
3018 // Square brackets
3019 (
3020 "c i b",
3021 "This is a [qˇuote] example.",
3022 "This is a [ˇ] example.",
3023 Mode::Insert,
3024 ),
3025 (
3026 "c a b",
3027 "This is a [qˇuote] example.",
3028 "This is a ˇ example.",
3029 Mode::Insert,
3030 ),
3031 (
3032 "d i b",
3033 "This is a [qˇuote] example.",
3034 "This is a [ˇ] example.",
3035 Mode::Normal,
3036 ),
3037 (
3038 "d a b",
3039 "This is a [qˇuote] example.",
3040 "This is a ˇ example.",
3041 Mode::Normal,
3042 ),
3043 // Curly brackets
3044 (
3045 "c i b",
3046 "This is a {qˇuote} example.",
3047 "This is a {ˇ} example.",
3048 Mode::Insert,
3049 ),
3050 (
3051 "c a b",
3052 "This is a {qˇuote} example.",
3053 "This is a ˇ example.",
3054 Mode::Insert,
3055 ),
3056 (
3057 "d i b",
3058 "This is a {qˇuote} example.",
3059 "This is a {ˇ} example.",
3060 Mode::Normal,
3061 ),
3062 (
3063 "d a b",
3064 "This is a {qˇuote} example.",
3065 "This is a ˇ example.",
3066 Mode::Normal,
3067 ),
3068 ];
3069
3070 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3071 cx.set_state(initial_state, Mode::Normal);
3072
3073 cx.simulate_keystrokes(keystrokes);
3074
3075 cx.assert_state(expected_state, *expected_mode);
3076 }
3077
3078 const INVALID_CASES: &[(&str, &str, Mode)] = &[
3079 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3080 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3081 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3082 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3083 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3084 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3085 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3086 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3087 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3088 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3089 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3090 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3091 ];
3092
3093 for (keystrokes, initial_state, mode) in INVALID_CASES {
3094 cx.set_state(initial_state, Mode::Normal);
3095
3096 cx.simulate_keystrokes(keystrokes);
3097
3098 cx.assert_state(initial_state, *mode);
3099 }
3100 }
3101
3102 #[gpui::test]
3103 async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
3104 let mut cx = VimTestContext::new(cx, true).await;
3105 cx.update(|_, cx| {
3106 cx.bind_keys([KeyBinding::new(
3107 "b",
3108 MiniBrackets,
3109 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
3110 )]);
3111 });
3112
3113 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
3114 // Special cases from mini.ai plugin
3115 // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
3116 // Same behavior as mini.ai plugin
3117 (
3118 "c i b",
3119 indoc! {"
3120 {
3121 {
3122 ˇprint('hello')
3123 }
3124 }
3125 "},
3126 indoc! {"
3127 {
3128 {
3129 print(ˇ)
3130 }
3131 }
3132 "},
3133 Mode::Insert,
3134 ),
3135 // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3136 // Same behavior as mini.ai plugin
3137 (
3138 "c i b",
3139 indoc! {"
3140 {
3141 {
3142 ˇ
3143 print('hello')
3144 }
3145 }
3146 "},
3147 indoc! {"
3148 {
3149 {ˇ}
3150 }
3151 "},
3152 Mode::Insert,
3153 ),
3154 // If you are in the open bracket then it has higher priority
3155 (
3156 "c i b",
3157 indoc! {"
3158 «{ˇ»
3159 {
3160 print('hello')
3161 }
3162 }
3163 "},
3164 indoc! {"
3165 {ˇ}
3166 "},
3167 Mode::Insert,
3168 ),
3169 // If you are in the close bracket then it has higher priority
3170 (
3171 "c i b",
3172 indoc! {"
3173 {
3174 {
3175 print('hello')
3176 }
3177 «}ˇ»
3178 "},
3179 indoc! {"
3180 {ˇ}
3181 "},
3182 Mode::Insert,
3183 ),
3184 // Bracket (Parentheses)
3185 (
3186 "c i b",
3187 "Thisˇ is a (simple [quote]) example.",
3188 "This is a (ˇ) example.",
3189 Mode::Insert,
3190 ),
3191 (
3192 "c i b",
3193 "This is a [simple (qˇuote)] example.",
3194 "This is a [simple (ˇ)] example.",
3195 Mode::Insert,
3196 ),
3197 (
3198 "c a b",
3199 "This is a [simple (qˇuote)] example.",
3200 "This is a [simple ˇ] example.",
3201 Mode::Insert,
3202 ),
3203 (
3204 "c a b",
3205 "Thisˇ is a (simple [quote]) example.",
3206 "This is a ˇ example.",
3207 Mode::Insert,
3208 ),
3209 (
3210 "c i b",
3211 "This is a (qˇuote) example.",
3212 "This is a (ˇ) example.",
3213 Mode::Insert,
3214 ),
3215 (
3216 "c a b",
3217 "This is a (qˇuote) example.",
3218 "This is a ˇ example.",
3219 Mode::Insert,
3220 ),
3221 (
3222 "d i b",
3223 "This is a (qˇuote) example.",
3224 "This is a (ˇ) example.",
3225 Mode::Normal,
3226 ),
3227 (
3228 "d a b",
3229 "This is a (qˇuote) example.",
3230 "This is a ˇ example.",
3231 Mode::Normal,
3232 ),
3233 // Square brackets
3234 (
3235 "c i b",
3236 "This is a [qˇuote] example.",
3237 "This is a [ˇ] example.",
3238 Mode::Insert,
3239 ),
3240 (
3241 "c a b",
3242 "This is a [qˇuote] example.",
3243 "This is a ˇ example.",
3244 Mode::Insert,
3245 ),
3246 (
3247 "d i b",
3248 "This is a [qˇuote] example.",
3249 "This is a [ˇ] example.",
3250 Mode::Normal,
3251 ),
3252 (
3253 "d a b",
3254 "This is a [qˇuote] example.",
3255 "This is a ˇ example.",
3256 Mode::Normal,
3257 ),
3258 // Curly brackets
3259 (
3260 "c i b",
3261 "This is a {qˇuote} example.",
3262 "This is a {ˇ} example.",
3263 Mode::Insert,
3264 ),
3265 (
3266 "c a b",
3267 "This is a {qˇuote} example.",
3268 "This is a ˇ example.",
3269 Mode::Insert,
3270 ),
3271 (
3272 "d i b",
3273 "This is a {qˇuote} example.",
3274 "This is a {ˇ} example.",
3275 Mode::Normal,
3276 ),
3277 (
3278 "d a b",
3279 "This is a {qˇuote} example.",
3280 "This is a ˇ example.",
3281 Mode::Normal,
3282 ),
3283 ];
3284
3285 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3286 cx.set_state(initial_state, Mode::Normal);
3287 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3288 cx.simulate_keystrokes(keystrokes);
3289 cx.assert_state(expected_state, *expected_mode);
3290 }
3291
3292 const INVALID_CASES: &[(&str, &str, Mode)] = &[
3293 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3294 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3295 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3296 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3297 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3298 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3299 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3300 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3301 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3302 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3303 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3304 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3305 ];
3306
3307 for (keystrokes, initial_state, mode) in INVALID_CASES {
3308 cx.set_state(initial_state, Mode::Normal);
3309 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3310 cx.simulate_keystrokes(keystrokes);
3311 cx.assert_state(initial_state, *mode);
3312 }
3313 }
3314
3315 #[gpui::test]
3316 async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
3317 // Initialize test context with the TypeScript language loaded, so we
3318 // can actually get brackets definition.
3319 let mut cx = VimTestContext::new(cx, true).await;
3320
3321 // Update `b` to `MiniBrackets` so we can later use it when simulating
3322 // keystrokes.
3323 cx.update(|_, cx| {
3324 cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
3325 });
3326
3327 let (editor, cx) = cx.add_window_view(|window, cx| {
3328 let multi_buffer = MultiBuffer::build_multi(
3329 [
3330 ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
3331 ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
3332 ],
3333 cx,
3334 );
3335
3336 // In order for the brackets to actually be found, we need to update
3337 // the language used for the second buffer. This is something that
3338 // is handled automatically when simply using `VimTestContext::new`
3339 // but, since this is being set manually, the language isn't
3340 // automatically set.
3341 let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
3342 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
3343 if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
3344 buffer.update(cx, |buffer, cx| {
3345 buffer.set_language(Some(language::rust_lang()), cx);
3346 })
3347 };
3348
3349 editor
3350 });
3351
3352 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3353
3354 cx.assert_excerpts_with_selections(indoc! {"
3355 [EXCERPT]
3356 ˇ111
3357 222
3358 [EXCERPT]
3359 111
3360 a {bracket} example
3361 "
3362 });
3363
3364 cx.simulate_keystrokes("j j j j f r");
3365 cx.assert_excerpts_with_selections(indoc! {"
3366 [EXCERPT]
3367 111
3368 222
3369 [EXCERPT]
3370 111
3371 a {bˇracket} example
3372 "
3373 });
3374
3375 cx.simulate_keystrokes("d i b");
3376 cx.assert_excerpts_with_selections(indoc! {"
3377 [EXCERPT]
3378 111
3379 222
3380 [EXCERPT]
3381 111
3382 a {ˇ} example
3383 "
3384 });
3385 }
3386
3387 #[gpui::test]
3388 async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3389 let mut cx = NeovimBackedTestContext::new(cx).await;
3390 cx.set_shared_state("(trailingˇ whitespace )")
3391 .await;
3392 cx.simulate_shared_keystrokes("v i b").await;
3393 cx.shared_state().await.assert_matches();
3394 cx.simulate_shared_keystrokes("escape y i b").await;
3395 cx.shared_clipboard()
3396 .await
3397 .assert_eq("trailing whitespace ");
3398 }
3399
3400 #[gpui::test]
3401 async fn test_tags(cx: &mut gpui::TestAppContext) {
3402 let mut cx = VimTestContext::new_html(cx).await;
3403
3404 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3405 cx.simulate_keystrokes("v i t");
3406 cx.assert_state(
3407 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3408 Mode::Visual,
3409 );
3410 cx.simulate_keystrokes("a t");
3411 cx.assert_state(
3412 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3413 Mode::Visual,
3414 );
3415 cx.simulate_keystrokes("a t");
3416 cx.assert_state(
3417 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3418 Mode::Visual,
3419 );
3420
3421 // The cursor is before the tag
3422 cx.set_state(
3423 "<html><head></head><body> ˇ <b>hi!</b></body>",
3424 Mode::Normal,
3425 );
3426 cx.simulate_keystrokes("v i t");
3427 cx.assert_state(
3428 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
3429 Mode::Visual,
3430 );
3431 cx.simulate_keystrokes("a t");
3432 cx.assert_state(
3433 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
3434 Mode::Visual,
3435 );
3436
3437 // The cursor is in the open tag
3438 cx.set_state(
3439 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3440 Mode::Normal,
3441 );
3442 cx.simulate_keystrokes("v a t");
3443 cx.assert_state(
3444 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3445 Mode::Visual,
3446 );
3447 cx.simulate_keystrokes("i t");
3448 cx.assert_state(
3449 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3450 Mode::Visual,
3451 );
3452
3453 // current selection length greater than 1
3454 cx.set_state(
3455 "<html><head></head><body><«b>hi!ˇ»</b></body>",
3456 Mode::Visual,
3457 );
3458 cx.simulate_keystrokes("i t");
3459 cx.assert_state(
3460 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3461 Mode::Visual,
3462 );
3463 cx.simulate_keystrokes("a t");
3464 cx.assert_state(
3465 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3466 Mode::Visual,
3467 );
3468
3469 cx.set_state(
3470 "<html><head></head><body><«b>hi!</ˇ»b></body>",
3471 Mode::Visual,
3472 );
3473 cx.simulate_keystrokes("a t");
3474 cx.assert_state(
3475 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3476 Mode::Visual,
3477 );
3478 }
3479 #[gpui::test]
3480 async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3481 let mut cx = NeovimBackedTestContext::new(cx).await;
3482
3483 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3484 .await;
3485 cx.simulate_shared_keystrokes("v a w").await;
3486 cx.shared_state()
3487 .await
3488 .assert_eq(" «const ˇ»f = (x: unknown) => {");
3489
3490 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3491 .await;
3492 cx.simulate_shared_keystrokes("y a w").await;
3493 cx.shared_clipboard().await.assert_eq("const ");
3494
3495 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3496 .await;
3497 cx.simulate_shared_keystrokes("d a w").await;
3498 cx.shared_state()
3499 .await
3500 .assert_eq(" ˇf = (x: unknown) => {");
3501 cx.shared_clipboard().await.assert_eq("const ");
3502
3503 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3504 .await;
3505 cx.simulate_shared_keystrokes("c a w").await;
3506 cx.shared_state()
3507 .await
3508 .assert_eq(" ˇf = (x: unknown) => {");
3509 cx.shared_clipboard().await.assert_eq("const ");
3510 }
3511
3512 #[gpui::test]
3513 async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
3514 let mut cx = VimTestContext::new_typescript(cx).await;
3515
3516 cx.set_state(
3517 indoc! {"
3518 const foo = () => {
3519 return ˇ1;
3520 };
3521 "},
3522 Mode::Normal,
3523 );
3524 cx.simulate_keystrokes("v a f");
3525 cx.assert_state(
3526 indoc! {"
3527 «const foo = () => {
3528 return 1;
3529 };ˇ»
3530 "},
3531 Mode::VisualLine,
3532 );
3533
3534 cx.set_state(
3535 indoc! {"
3536 arr.map(() => {
3537 return ˇ1;
3538 });
3539 "},
3540 Mode::Normal,
3541 );
3542 cx.simulate_keystrokes("v a f");
3543 cx.assert_state(
3544 indoc! {"
3545 arr.map(«() => {
3546 return 1;
3547 }ˇ»);
3548 "},
3549 Mode::VisualLine,
3550 );
3551
3552 cx.set_state(
3553 indoc! {"
3554 const foo = () => {
3555 return ˇ1;
3556 };
3557 "},
3558 Mode::Normal,
3559 );
3560 cx.simulate_keystrokes("v i f");
3561 cx.assert_state(
3562 indoc! {"
3563 const foo = () => {
3564 «return 1;ˇ»
3565 };
3566 "},
3567 Mode::Visual,
3568 );
3569
3570 cx.set_state(
3571 indoc! {"
3572 (() => {
3573 console.log(ˇ1);
3574 })();
3575 "},
3576 Mode::Normal,
3577 );
3578 cx.simulate_keystrokes("v a f");
3579 cx.assert_state(
3580 indoc! {"
3581 («() => {
3582 console.log(1);
3583 }ˇ»)();
3584 "},
3585 Mode::VisualLine,
3586 );
3587
3588 cx.set_state(
3589 indoc! {"
3590 const foo = () => {
3591 return ˇ1;
3592 };
3593 export { foo };
3594 "},
3595 Mode::Normal,
3596 );
3597 cx.simulate_keystrokes("v a f");
3598 cx.assert_state(
3599 indoc! {"
3600 «const foo = () => {
3601 return 1;
3602 };ˇ»
3603 export { foo };
3604 "},
3605 Mode::VisualLine,
3606 );
3607
3608 cx.set_state(
3609 indoc! {"
3610 let bar = () => {
3611 return ˇ2;
3612 };
3613 "},
3614 Mode::Normal,
3615 );
3616 cx.simulate_keystrokes("v a f");
3617 cx.assert_state(
3618 indoc! {"
3619 «let bar = () => {
3620 return 2;
3621 };ˇ»
3622 "},
3623 Mode::VisualLine,
3624 );
3625
3626 cx.set_state(
3627 indoc! {"
3628 var baz = () => {
3629 return ˇ3;
3630 };
3631 "},
3632 Mode::Normal,
3633 );
3634 cx.simulate_keystrokes("v a f");
3635 cx.assert_state(
3636 indoc! {"
3637 «var baz = () => {
3638 return 3;
3639 };ˇ»
3640 "},
3641 Mode::VisualLine,
3642 );
3643
3644 cx.set_state(
3645 indoc! {"
3646 const add = (a, b) => a + ˇb;
3647 "},
3648 Mode::Normal,
3649 );
3650 cx.simulate_keystrokes("v a f");
3651 cx.assert_state(
3652 indoc! {"
3653 «const add = (a, b) => a + b;ˇ»
3654 "},
3655 Mode::VisualLine,
3656 );
3657
3658 cx.set_state(
3659 indoc! {"
3660 const add = ˇ(a, b) => a + b;
3661 "},
3662 Mode::Normal,
3663 );
3664 cx.simulate_keystrokes("v a f");
3665 cx.assert_state(
3666 indoc! {"
3667 «const add = (a, b) => a + b;ˇ»
3668 "},
3669 Mode::VisualLine,
3670 );
3671
3672 cx.set_state(
3673 indoc! {"
3674 const add = (a, b) => a + bˇ;
3675 "},
3676 Mode::Normal,
3677 );
3678 cx.simulate_keystrokes("v a f");
3679 cx.assert_state(
3680 indoc! {"
3681 «const add = (a, b) => a + b;ˇ»
3682 "},
3683 Mode::VisualLine,
3684 );
3685
3686 cx.set_state(
3687 indoc! {"
3688 const add = (a, b) =ˇ> a + b;
3689 "},
3690 Mode::Normal,
3691 );
3692 cx.simulate_keystrokes("v a f");
3693 cx.assert_state(
3694 indoc! {"
3695 «const add = (a, b) => a + b;ˇ»
3696 "},
3697 Mode::VisualLine,
3698 );
3699 }
3700
3701 #[gpui::test]
3702 async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
3703 let mut cx = VimTestContext::new_tsx(cx).await;
3704
3705 cx.set_state(
3706 indoc! {r#"
3707 export const MyComponent = () => {
3708 return (
3709 <div>
3710 <div onClick={() => {
3711 alert("Hello world!");
3712 console.log(ˇ"clicked");
3713 }}>Hello world!</div>
3714 </div>
3715 );
3716 };
3717 "#},
3718 Mode::Normal,
3719 );
3720 cx.simulate_keystrokes("v a f");
3721 cx.assert_state(
3722 indoc! {r#"
3723 export const MyComponent = () => {
3724 return (
3725 <div>
3726 <div onClick={«() => {
3727 alert("Hello world!");
3728 console.log("clicked");
3729 }ˇ»}>Hello world!</div>
3730 </div>
3731 );
3732 };
3733 "#},
3734 Mode::VisualLine,
3735 );
3736
3737 cx.set_state(
3738 indoc! {r#"
3739 export const MyComponent = () => {
3740 return (
3741 <div>
3742 <div onClick={() => console.log("clickˇed")}>Hello world!</div>
3743 </div>
3744 );
3745 };
3746 "#},
3747 Mode::Normal,
3748 );
3749 cx.simulate_keystrokes("v a f");
3750 cx.assert_state(
3751 indoc! {r#"
3752 export const MyComponent = () => {
3753 return (
3754 <div>
3755 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3756 </div>
3757 );
3758 };
3759 "#},
3760 Mode::VisualLine,
3761 );
3762
3763 cx.set_state(
3764 indoc! {r#"
3765 export const MyComponent = () => {
3766 return (
3767 <div>
3768 <div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
3769 </div>
3770 );
3771 };
3772 "#},
3773 Mode::Normal,
3774 );
3775 cx.simulate_keystrokes("v a f");
3776 cx.assert_state(
3777 indoc! {r#"
3778 export const MyComponent = () => {
3779 return (
3780 <div>
3781 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3782 </div>
3783 );
3784 };
3785 "#},
3786 Mode::VisualLine,
3787 );
3788
3789 cx.set_state(
3790 indoc! {r#"
3791 export const MyComponent = () => {
3792 return (
3793 <div>
3794 <div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
3795 </div>
3796 );
3797 };
3798 "#},
3799 Mode::Normal,
3800 );
3801 cx.simulate_keystrokes("v a f");
3802 cx.assert_state(
3803 indoc! {r#"
3804 export const MyComponent = () => {
3805 return (
3806 <div>
3807 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3808 </div>
3809 );
3810 };
3811 "#},
3812 Mode::VisualLine,
3813 );
3814
3815 cx.set_state(
3816 indoc! {r#"
3817 export const MyComponent = () => {
3818 return (
3819 <div>
3820 <div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
3821 </div>
3822 );
3823 };
3824 "#},
3825 Mode::Normal,
3826 );
3827 cx.simulate_keystrokes("v a f");
3828 cx.assert_state(
3829 indoc! {r#"
3830 export const MyComponent = () => {
3831 return (
3832 <div>
3833 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3834 </div>
3835 );
3836 };
3837 "#},
3838 Mode::VisualLine,
3839 );
3840
3841 cx.set_state(
3842 indoc! {r#"
3843 export const MyComponent = () => {
3844 return (
3845 <div>
3846 <div onClick={() => {
3847 console.log("cliˇcked");
3848 }}>Hello world!</div>
3849 </div>
3850 );
3851 };
3852 "#},
3853 Mode::Normal,
3854 );
3855 cx.simulate_keystrokes("v a f");
3856 cx.assert_state(
3857 indoc! {r#"
3858 export const MyComponent = () => {
3859 return (
3860 <div>
3861 <div onClick={«() => {
3862 console.log("clicked");
3863 }ˇ»}>Hello world!</div>
3864 </div>
3865 );
3866 };
3867 "#},
3868 Mode::VisualLine,
3869 );
3870
3871 cx.set_state(
3872 indoc! {r#"
3873 export const MyComponent = () => {
3874 return (
3875 <div>
3876 <div onClick={() => fˇoo()}>Hello world!</div>
3877 </div>
3878 );
3879 };
3880 "#},
3881 Mode::Normal,
3882 );
3883 cx.simulate_keystrokes("v a f");
3884 cx.assert_state(
3885 indoc! {r#"
3886 export const MyComponent = () => {
3887 return (
3888 <div>
3889 <div onClick={«() => foo()ˇ»}>Hello world!</div>
3890 </div>
3891 );
3892 };
3893 "#},
3894 Mode::VisualLine,
3895 );
3896 }
3897
3898 #[gpui::test]
3899 async fn test_subword_object(cx: &mut gpui::TestAppContext) {
3900 let mut cx = VimTestContext::new(cx, true).await;
3901
3902 // Setup custom keybindings for subword object so we can use the
3903 // bindings in `simulate_keystrokes`.
3904 cx.update(|_window, cx| {
3905 cx.bind_keys([KeyBinding::new(
3906 "w",
3907 super::Subword {
3908 ignore_punctuation: false,
3909 },
3910 Some("vim_operator"),
3911 )]);
3912 });
3913
3914 cx.set_state("foo_ˇbar_baz", Mode::Normal);
3915 cx.simulate_keystrokes("c i w");
3916 cx.assert_state("foo_ˇ_baz", Mode::Insert);
3917
3918 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
3919 cx.simulate_keystrokes("c i w");
3920 cx.assert_state("ˇ_bar_baz", Mode::Insert);
3921
3922 cx.set_state("foo_bar_baˇz", Mode::Normal);
3923 cx.simulate_keystrokes("c i w");
3924 cx.assert_state("foo_bar_ˇ", Mode::Insert);
3925
3926 cx.set_state("fooˇBarBaz", Mode::Normal);
3927 cx.simulate_keystrokes("c i w");
3928 cx.assert_state("fooˇBaz", Mode::Insert);
3929
3930 cx.set_state("ˇfooBarBaz", Mode::Normal);
3931 cx.simulate_keystrokes("c i w");
3932 cx.assert_state("ˇBarBaz", Mode::Insert);
3933
3934 cx.set_state("fooBarBaˇz", Mode::Normal);
3935 cx.simulate_keystrokes("c i w");
3936 cx.assert_state("fooBarˇ", Mode::Insert);
3937
3938 cx.set_state("foo.ˇbar.baz", Mode::Normal);
3939 cx.simulate_keystrokes("c i w");
3940 cx.assert_state("foo.ˇ.baz", Mode::Insert);
3941
3942 cx.set_state("foo_ˇbar_baz", Mode::Normal);
3943 cx.simulate_keystrokes("d i w");
3944 cx.assert_state("foo_ˇ_baz", Mode::Normal);
3945
3946 cx.set_state("fooˇBarBaz", Mode::Normal);
3947 cx.simulate_keystrokes("d i w");
3948 cx.assert_state("fooˇBaz", Mode::Normal);
3949
3950 cx.set_state("foo_ˇbar_baz", Mode::Normal);
3951 cx.simulate_keystrokes("c a w");
3952 cx.assert_state("foo_ˇ_baz", Mode::Insert);
3953
3954 cx.set_state("fooˇBarBaz", Mode::Normal);
3955 cx.simulate_keystrokes("c a w");
3956 cx.assert_state("fooˇBaz", Mode::Insert);
3957
3958 cx.set_state("foo_ˇbar_baz", Mode::Normal);
3959 cx.simulate_keystrokes("d a w");
3960 cx.assert_state("foo_ˇ_baz", Mode::Normal);
3961
3962 cx.set_state("fooˇBarBaz", Mode::Normal);
3963 cx.simulate_keystrokes("d a w");
3964 cx.assert_state("fooˇBaz", Mode::Normal);
3965 }
3966}