1use std::ops::Range;
2
3use crate::{
4 Vim,
5 motion::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 if classifier.is_word('-') {
866 !classifier.is_whitespace(c) && c != '_' && c != '-'
867 } else {
868 !classifier.is_whitespace(c) && c != '_'
869 }
870 })
871 .unwrap_or(false);
872
873 let start = if in_subword {
874 movement::find_preceding_boundary_display_point(
875 map,
876 right(map, relative_to, 1),
877 movement::FindRange::SingleLine,
878 |left, right| {
879 let is_word_start = classifier.kind(left) != classifier.kind(right);
880 let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
881 || left == '_' && right != '_'
882 || left.is_lowercase() && right.is_uppercase();
883 is_word_start || is_subword_start
884 },
885 )
886 } else {
887 movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
888 let is_word_start = classifier.kind(left) != classifier.kind(right);
889 let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
890 || left == '_' && right != '_'
891 || left.is_lowercase() && right.is_uppercase();
892 is_word_start || is_subword_start
893 })
894 };
895
896 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
897 let is_word_end = classifier.kind(left) != classifier.kind(right);
898 let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
899 || left != '_' && right == '_'
900 || left.is_lowercase() && right.is_uppercase();
901 is_word_end || is_subword_end
902 });
903
904 Some(start..end)
905}
906
907pub fn surrounding_html_tag(
908 map: &DisplaySnapshot,
909 head: DisplayPoint,
910 range: Range<DisplayPoint>,
911 around: bool,
912) -> Option<Range<DisplayPoint>> {
913 fn read_tag(chars: impl Iterator<Item = char>) -> String {
914 chars
915 .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
916 .collect()
917 }
918 fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
919 if Some('<') != chars.next() {
920 return None;
921 }
922 Some(read_tag(chars))
923 }
924 fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
925 if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
926 return None;
927 }
928 Some(read_tag(chars))
929 }
930
931 let snapshot = &map.buffer_snapshot();
932 let offset = head.to_offset(map, Bias::Left);
933 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
934 let buffer = excerpt.buffer();
935 let offset = excerpt.map_offset_to_buffer(offset);
936
937 // Find the most closest to current offset
938 let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
939 let mut last_child_node = cursor.node();
940 while cursor.goto_first_child_for_byte(offset.0).is_some() {
941 last_child_node = cursor.node();
942 }
943
944 let mut last_child_node = Some(last_child_node);
945 while let Some(cur_node) = last_child_node {
946 if cur_node.child_count() >= 2 {
947 let first_child = cur_node.child(0);
948 let last_child = cur_node.child(cur_node.child_count() as u32 - 1);
949 if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
950 let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
951 let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
952 // It needs to be handled differently according to the selection length
953 let is_valid = if range.end.to_offset(map, Bias::Left)
954 - range.start.to_offset(map, Bias::Left)
955 <= 1
956 {
957 offset.0 <= last_child.end_byte()
958 } else {
959 excerpt
960 .map_offset_to_buffer(range.start.to_offset(map, Bias::Left))
961 .0
962 >= first_child.start_byte()
963 && excerpt
964 .map_offset_to_buffer(range.end.to_offset(map, Bias::Left))
965 .0
966 <= last_child.start_byte() + 1
967 };
968 if open_tag.is_some() && open_tag == close_tag && is_valid {
969 let range = if around {
970 first_child.byte_range().start..last_child.byte_range().end
971 } else {
972 first_child.byte_range().end..last_child.byte_range().start
973 };
974 let range = BufferOffset(range.start)..BufferOffset(range.end);
975 if excerpt.contains_buffer_range(range.clone()) {
976 let result = excerpt.map_range_from_buffer(range);
977 return Some(
978 result.start.to_display_point(map)..result.end.to_display_point(map),
979 );
980 }
981 }
982 }
983 }
984 last_child_node = cur_node.parent();
985 }
986 None
987}
988
989/// Returns a range that surrounds the word and following whitespace
990/// relative_to is in.
991///
992/// If `relative_to` is at the start of a word, return the word and following whitespace.
993/// If `relative_to` is between words, return the whitespace back and the following word.
994///
995/// if in word
996/// delete that word
997/// if there is whitespace following the word, delete that as well
998/// otherwise, delete any preceding whitespace
999/// otherwise
1000/// delete whitespace around cursor
1001/// delete word following the cursor
1002/// If `times` > 1, extend to include additional words.
1003fn around_word(
1004 map: &DisplaySnapshot,
1005 relative_to: DisplayPoint,
1006 ignore_punctuation: bool,
1007 times: usize,
1008) -> Option<Range<DisplayPoint>> {
1009 let offset = relative_to.to_offset(map, Bias::Left);
1010 let classifier = map
1011 .buffer_snapshot()
1012 .char_classifier_at(offset)
1013 .ignore_punctuation(ignore_punctuation);
1014 let in_word = map
1015 .buffer_chars_at(offset)
1016 .next()
1017 .map(|(c, _)| !classifier.is_whitespace(c))
1018 .unwrap_or(false);
1019
1020 if in_word {
1021 around_containing_word(map, relative_to, ignore_punctuation, times)
1022 } else {
1023 around_next_word(map, relative_to, ignore_punctuation, times)
1024 }
1025}
1026
1027fn around_subword(
1028 map: &DisplaySnapshot,
1029 relative_to: DisplayPoint,
1030 ignore_punctuation: bool,
1031) -> Option<Range<DisplayPoint>> {
1032 // Use motion::right so that we consider the character under the cursor when looking for the start
1033 let classifier = map
1034 .buffer_snapshot()
1035 .char_classifier_at(relative_to.to_point(map))
1036 .ignore_punctuation(ignore_punctuation);
1037 let start = movement::find_preceding_boundary_display_point(
1038 map,
1039 right(map, relative_to, 1),
1040 movement::FindRange::SingleLine,
1041 |left, right| {
1042 let is_word_start = classifier.kind(left) != classifier.kind(right);
1043 let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
1044 || left != '_' && right == '_'
1045 || left.is_lowercase() && right.is_uppercase();
1046 is_word_start || is_subword_start
1047 },
1048 );
1049
1050 let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
1051 let is_word_end = classifier.kind(left) != classifier.kind(right);
1052 let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
1053 || left != '_' && right == '_'
1054 || left.is_lowercase() && right.is_uppercase();
1055 is_word_end || is_subword_end
1056 });
1057
1058 Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
1059}
1060
1061fn around_containing_word(
1062 map: &DisplaySnapshot,
1063 relative_to: DisplayPoint,
1064 ignore_punctuation: bool,
1065 times: usize,
1066) -> Option<Range<DisplayPoint>> {
1067 in_word(map, relative_to, ignore_punctuation, times).map(|range| {
1068 let spans_multiple_lines = range.start.row() != range.end.row();
1069 let stop_at_newline = !spans_multiple_lines;
1070
1071 let line_start = DisplayPoint::new(range.start.row(), 0);
1072 let is_first_word = map
1073 .buffer_chars_at(line_start.to_offset(map, Bias::Left))
1074 .take_while(|(ch, offset)| {
1075 offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace()
1076 })
1077 .count()
1078 > 0;
1079
1080 if is_first_word {
1081 // For first word on line, trim indentation
1082 let mut expanded = expand_to_include_whitespace(map, range.clone(), stop_at_newline);
1083 expanded.start = range.start;
1084 expanded
1085 } else {
1086 expand_to_include_whitespace(map, range, stop_at_newline)
1087 }
1088 })
1089}
1090
1091fn around_next_word(
1092 map: &DisplaySnapshot,
1093 relative_to: DisplayPoint,
1094 ignore_punctuation: bool,
1095 times: usize,
1096) -> Option<Range<DisplayPoint>> {
1097 let classifier = map
1098 .buffer_snapshot()
1099 .char_classifier_at(relative_to.to_point(map))
1100 .ignore_punctuation(ignore_punctuation);
1101 let start = movement::find_preceding_boundary_display_point(
1102 map,
1103 right(map, relative_to, 1),
1104 FindRange::SingleLine,
1105 |left, right| classifier.kind(left) != classifier.kind(right),
1106 );
1107
1108 let mut word_found = false;
1109 let mut end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
1110 let left_kind = classifier.kind(left);
1111 let right_kind = classifier.kind(right);
1112
1113 let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
1114
1115 if right_kind != CharKind::Whitespace {
1116 word_found = true;
1117 }
1118
1119 found
1120 });
1121
1122 for _ in 1..times {
1123 let next_end = movement::find_boundary(map, end, FindRange::MultiLine, |left, right| {
1124 let left_kind = classifier.kind(left);
1125 let right_kind = classifier.kind(right);
1126
1127 let in_word_unit = left_kind != CharKind::Whitespace;
1128 (in_word_unit && left_kind != right_kind) || right == '\n' && left == '\n'
1129 });
1130 if next_end == end {
1131 break;
1132 }
1133 end = next_end;
1134 }
1135
1136 Some(start..end)
1137}
1138
1139fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
1140 Some(DisplayPoint::zero()..map.max_point())
1141}
1142
1143fn text_object(
1144 map: &DisplaySnapshot,
1145 relative_to: DisplayPoint,
1146 target: TextObject,
1147) -> Option<Range<DisplayPoint>> {
1148 let snapshot = &map.buffer_snapshot();
1149 let offset = relative_to.to_offset(map, Bias::Left);
1150
1151 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1152 let buffer = excerpt.buffer();
1153 let offset = excerpt.map_offset_to_buffer(offset);
1154
1155 let mut matches: Vec<Range<usize>> = buffer
1156 .text_object_ranges(offset..offset, TreeSitterOptions::default())
1157 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1158 .collect();
1159 matches.sort_by_key(|r| r.end - r.start);
1160 if let Some(buffer_range) = matches.first() {
1161 let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1162 let range = excerpt.map_range_from_buffer(buffer_range);
1163 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1164 }
1165
1166 let around = target.around()?;
1167 let mut matches: Vec<Range<usize>> = buffer
1168 .text_object_ranges(offset..offset, TreeSitterOptions::default())
1169 .filter_map(|(r, m)| if m == around { Some(r) } else { None })
1170 .collect();
1171 matches.sort_by_key(|r| r.end - r.start);
1172 let around_range = matches.first()?;
1173
1174 let mut matches: Vec<Range<usize>> = buffer
1175 .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
1176 .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1177 .collect();
1178 matches.sort_by_key(|r| r.start);
1179 if let Some(buffer_range) = matches.first()
1180 && !buffer_range.is_empty()
1181 {
1182 let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1183 let range = excerpt.map_range_from_buffer(buffer_range);
1184 return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1185 }
1186 let around_range = BufferOffset(around_range.start)..BufferOffset(around_range.end);
1187 let buffer_range = excerpt.map_range_from_buffer(around_range);
1188 return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
1189}
1190
1191fn argument(
1192 map: &DisplaySnapshot,
1193 relative_to: DisplayPoint,
1194 around: bool,
1195) -> Option<Range<DisplayPoint>> {
1196 let snapshot = &map.buffer_snapshot();
1197 let offset = relative_to.to_offset(map, Bias::Left);
1198
1199 // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
1200 let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1201 let buffer = excerpt.buffer();
1202
1203 fn comma_delimited_range_at(
1204 buffer: &BufferSnapshot,
1205 mut offset: BufferOffset,
1206 include_comma: bool,
1207 ) -> Option<Range<BufferOffset>> {
1208 // Seek to the first non-whitespace character
1209 offset += buffer
1210 .chars_at(offset)
1211 .take_while(|c| c.is_whitespace())
1212 .map(char::len_utf8)
1213 .sum::<usize>();
1214
1215 let bracket_filter = |open: Range<usize>, close: Range<usize>| {
1216 // Filter out empty ranges
1217 if open.end == close.start {
1218 return false;
1219 }
1220
1221 // If the cursor is outside the brackets, ignore them
1222 if open.start == offset.0 || close.end == offset.0 {
1223 return false;
1224 }
1225
1226 // TODO: Is there any better way to filter out string brackets?
1227 // Used to filter out string brackets
1228 matches!(
1229 buffer.chars_at(open.start).next(),
1230 Some('(' | '[' | '{' | '<' | '|')
1231 )
1232 };
1233
1234 // Find the brackets containing the cursor
1235 let (open_bracket, close_bracket) =
1236 buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
1237
1238 let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start);
1239
1240 let layer = buffer.syntax_layer_at(offset)?;
1241 let node = layer.node();
1242 let mut cursor = node.walk();
1243
1244 // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
1245 let mut parent_covers_bracket_range = false;
1246 loop {
1247 let node = cursor.node();
1248 let range = node.byte_range();
1249 let covers_bracket_range =
1250 range.start == open_bracket.start && range.end == close_bracket.end;
1251 if parent_covers_bracket_range && !covers_bracket_range {
1252 break;
1253 }
1254 parent_covers_bracket_range = covers_bracket_range;
1255
1256 // Unable to find a child node with a parent that covers the bracket range, so no argument to select
1257 cursor.goto_first_child_for_byte(offset.0)?;
1258 }
1259
1260 let mut argument_node = cursor.node();
1261
1262 // If the child node is the open bracket, move to the next sibling.
1263 if argument_node.byte_range() == open_bracket {
1264 if !cursor.goto_next_sibling() {
1265 return Some(inner_bracket_range);
1266 }
1267 argument_node = cursor.node();
1268 }
1269 // While the child node is the close bracket or a comma, move to the previous sibling
1270 while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
1271 if !cursor.goto_previous_sibling() {
1272 return Some(inner_bracket_range);
1273 }
1274 argument_node = cursor.node();
1275 if argument_node.byte_range() == open_bracket {
1276 return Some(inner_bracket_range);
1277 }
1278 }
1279
1280 // The start and end of the argument range, defaulting to the start and end of the argument node
1281 let mut start = argument_node.start_byte();
1282 let mut end = argument_node.end_byte();
1283
1284 let mut needs_surrounding_comma = include_comma;
1285
1286 // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
1287 // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
1288 while cursor.goto_previous_sibling() {
1289 let prev = cursor.node();
1290
1291 if prev.start_byte() < open_bracket.end {
1292 start = open_bracket.end;
1293 break;
1294 } else if prev.kind() == "," {
1295 if needs_surrounding_comma {
1296 start = prev.start_byte();
1297 needs_surrounding_comma = false;
1298 }
1299 break;
1300 } else if prev.start_byte() < start {
1301 start = prev.start_byte();
1302 }
1303 }
1304
1305 // Do the same for the end of the argument, extending to next comma or the end of the argument list
1306 while cursor.goto_next_sibling() {
1307 let next = cursor.node();
1308
1309 if next.end_byte() > close_bracket.start {
1310 end = close_bracket.start;
1311 break;
1312 } else if next.kind() == "," {
1313 if needs_surrounding_comma {
1314 // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
1315 if let Some(next_arg) = next.next_sibling() {
1316 end = next_arg.start_byte();
1317 } else {
1318 end = next.end_byte();
1319 }
1320 }
1321 break;
1322 } else if next.end_byte() > end {
1323 end = next.end_byte();
1324 }
1325 }
1326
1327 Some(BufferOffset(start)..BufferOffset(end))
1328 }
1329
1330 let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
1331
1332 if excerpt.contains_buffer_range(result.clone()) {
1333 let result = excerpt.map_range_from_buffer(result);
1334 Some(result.start.to_display_point(map)..result.end.to_display_point(map))
1335 } else {
1336 None
1337 }
1338}
1339
1340fn indent(
1341 map: &DisplaySnapshot,
1342 relative_to: DisplayPoint,
1343 around: bool,
1344 include_below: bool,
1345) -> Option<Range<DisplayPoint>> {
1346 let point = relative_to.to_point(map);
1347 let row = point.row;
1348
1349 let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
1350
1351 // Loop backwards until we find a non-blank line with less indent
1352 let mut start_row = row;
1353 for prev_row in (0..row).rev() {
1354 let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
1355 if indent.is_line_empty() {
1356 continue;
1357 }
1358 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1359 if around {
1360 // When around is true, include the first line with less indent
1361 start_row = prev_row;
1362 }
1363 break;
1364 }
1365 start_row = prev_row;
1366 }
1367
1368 // Loop forwards until we find a non-blank line with less indent
1369 let mut end_row = row;
1370 let max_rows = map.buffer_snapshot().max_row().0;
1371 for next_row in (row + 1)..=max_rows {
1372 let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
1373 if indent.is_line_empty() {
1374 continue;
1375 }
1376 if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1377 if around && include_below {
1378 // When around is true and including below, include this line
1379 end_row = next_row;
1380 }
1381 break;
1382 }
1383 end_row = next_row;
1384 }
1385
1386 let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1387 let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
1388 let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
1389 Some(start..end)
1390}
1391
1392fn sentence(
1393 map: &DisplaySnapshot,
1394 relative_to: DisplayPoint,
1395 around: bool,
1396) -> Option<Range<DisplayPoint>> {
1397 let mut start = None;
1398 let relative_offset = relative_to.to_offset(map, Bias::Left);
1399 let mut previous_end = relative_offset;
1400
1401 let mut chars = map.buffer_chars_at(previous_end).peekable();
1402
1403 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
1404 for (char, offset) in chars
1405 .peek()
1406 .cloned()
1407 .into_iter()
1408 .chain(map.reverse_buffer_chars_at(previous_end))
1409 {
1410 if is_sentence_end(map, offset) {
1411 break;
1412 }
1413
1414 if is_possible_sentence_start(char) {
1415 start = Some(offset);
1416 }
1417
1418 previous_end = offset;
1419 }
1420
1421 // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1422 let mut end = relative_offset;
1423 for (char, offset) in chars {
1424 if start.is_none() && is_possible_sentence_start(char) {
1425 if around {
1426 start = Some(offset);
1427 continue;
1428 } else {
1429 end = offset;
1430 break;
1431 }
1432 }
1433
1434 if char != '\n' {
1435 end = offset + char.len_utf8();
1436 }
1437
1438 if is_sentence_end(map, end) {
1439 break;
1440 }
1441 }
1442
1443 let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1444 if around {
1445 range = expand_to_include_whitespace(map, range, false);
1446 }
1447
1448 Some(range)
1449}
1450
1451fn is_possible_sentence_start(character: char) -> bool {
1452 !character.is_whitespace() && character != '.'
1453}
1454
1455const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1456const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1457const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1458fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool {
1459 let mut next_chars = map.buffer_chars_at(offset).peekable();
1460 if let Some((char, _)) = next_chars.next() {
1461 // We are at a double newline. This position is a sentence end.
1462 if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1463 return true;
1464 }
1465
1466 // The next text is not a valid whitespace. This is not a sentence end
1467 if !SENTENCE_END_WHITESPACE.contains(&char) {
1468 return false;
1469 }
1470 }
1471
1472 for (char, _) in map.reverse_buffer_chars_at(offset) {
1473 if SENTENCE_END_PUNCTUATION.contains(&char) {
1474 return true;
1475 }
1476
1477 if !SENTENCE_END_FILLERS.contains(&char) {
1478 return false;
1479 }
1480 }
1481
1482 false
1483}
1484
1485/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1486/// whitespace to the end first and falls back to the start if there was none.
1487pub fn expand_to_include_whitespace(
1488 map: &DisplaySnapshot,
1489 range: Range<DisplayPoint>,
1490 stop_at_newline: bool,
1491) -> Range<DisplayPoint> {
1492 let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1493 let mut whitespace_included = false;
1494
1495 let chars = map.buffer_chars_at(range.end).peekable();
1496 for (char, offset) in chars {
1497 if char == '\n' && stop_at_newline {
1498 break;
1499 }
1500
1501 if char.is_whitespace() {
1502 if char != '\n' || !stop_at_newline {
1503 range.end = offset + char.len_utf8();
1504 whitespace_included = true;
1505 }
1506 } else {
1507 // Found non whitespace. Quit out.
1508 break;
1509 }
1510 }
1511
1512 if !whitespace_included {
1513 for (char, point) in map.reverse_buffer_chars_at(range.start) {
1514 if char == '\n' && stop_at_newline {
1515 break;
1516 }
1517
1518 if !char.is_whitespace() {
1519 break;
1520 }
1521
1522 range.start = point;
1523 }
1524 }
1525
1526 range.start.to_display_point(map)..range.end.to_display_point(map)
1527}
1528
1529/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1530/// where `relative_to` is in. If `around`, principally returns the range ending
1531/// at the end of the next paragraph.
1532///
1533/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1534/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1535/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1536/// the trailing newline is not subject to subsequent operations).
1537///
1538/// Edge cases:
1539/// - If `around` and if the current paragraph is the last paragraph of the
1540/// file and is blank, then the selection results in an error.
1541/// - If `around` and if the current paragraph is the last paragraph of the
1542/// file and is not blank, then the returned range starts at the start of the
1543/// previous paragraph, if it exists.
1544fn paragraph(
1545 map: &DisplaySnapshot,
1546 relative_to: DisplayPoint,
1547 around: bool,
1548 times: usize,
1549) -> Option<Range<DisplayPoint>> {
1550 let mut paragraph_start = start_of_paragraph(map, relative_to);
1551 let mut paragraph_end = end_of_paragraph(map, relative_to);
1552
1553 for i in 0..times {
1554 let paragraph_end_row = paragraph_end.row();
1555 let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1556 let point = relative_to.to_point(map);
1557 let current_line_is_empty = map
1558 .buffer_snapshot()
1559 .is_line_blank(MultiBufferRow(point.row));
1560
1561 if around {
1562 if paragraph_ends_with_eof {
1563 if current_line_is_empty {
1564 return None;
1565 }
1566
1567 let paragraph_start_buffer_point = paragraph_start.to_point(map);
1568 if paragraph_start_buffer_point.row != 0 {
1569 let previous_paragraph_last_line_start =
1570 Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
1571 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1572 }
1573 } else {
1574 let paragraph_end_buffer_point = paragraph_end.to_point(map);
1575 let mut start_row = paragraph_end_buffer_point.row + 1;
1576 if i > 0 {
1577 start_row += 1;
1578 }
1579 let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1580 paragraph_end = end_of_paragraph(map, next_paragraph_start);
1581 }
1582 }
1583 }
1584
1585 let range = paragraph_start..paragraph_end;
1586 Some(range)
1587}
1588
1589/// Returns a position of the start of the current paragraph, where a paragraph
1590/// is defined as a run of non-blank lines or a run of blank lines.
1591pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1592 let point = display_point.to_point(map);
1593 if point.row == 0 {
1594 return DisplayPoint::zero();
1595 }
1596
1597 let is_current_line_blank = map
1598 .buffer_snapshot()
1599 .is_line_blank(MultiBufferRow(point.row));
1600
1601 for row in (0..point.row).rev() {
1602 let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1603 if blank != is_current_line_blank {
1604 return Point::new(row + 1, 0).to_display_point(map);
1605 }
1606 }
1607
1608 DisplayPoint::zero()
1609}
1610
1611/// Returns a position of the end of the current paragraph, where a paragraph
1612/// is defined as a run of non-blank lines or a run of blank lines.
1613/// The trailing newline is excluded from the paragraph.
1614pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1615 let point = display_point.to_point(map);
1616 if point.row == map.buffer_snapshot().max_row().0 {
1617 return map.max_point();
1618 }
1619
1620 let is_current_line_blank = map
1621 .buffer_snapshot()
1622 .is_line_blank(MultiBufferRow(point.row));
1623
1624 for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 {
1625 let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1626 if blank != is_current_line_blank {
1627 let previous_row = row - 1;
1628 return Point::new(
1629 previous_row,
1630 map.buffer_snapshot().line_len(MultiBufferRow(previous_row)),
1631 )
1632 .to_display_point(map);
1633 }
1634 }
1635
1636 map.max_point()
1637}
1638
1639pub fn surrounding_markers(
1640 map: &DisplaySnapshot,
1641 relative_to: DisplayPoint,
1642 around: bool,
1643 search_across_lines: bool,
1644 open_marker: char,
1645 close_marker: char,
1646) -> Option<Range<DisplayPoint>> {
1647 let point = relative_to.to_offset(map, Bias::Left);
1648
1649 let mut matched_closes = 0;
1650 let mut opening = None;
1651
1652 let mut before_ch = match movement::chars_before(map, point).next() {
1653 Some((ch, _)) => ch,
1654 _ => '\0',
1655 };
1656 if let Some((ch, range)) = movement::chars_after(map, point).next()
1657 && ch == open_marker
1658 && before_ch != '\\'
1659 {
1660 if open_marker == close_marker {
1661 let mut total = 0;
1662 for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
1663 if ch == '\n' {
1664 break;
1665 }
1666 if ch == open_marker && before_ch != '\\' {
1667 total += 1;
1668 }
1669 }
1670 if total % 2 == 0 {
1671 opening = Some(range)
1672 }
1673 } else {
1674 opening = Some(range)
1675 }
1676 }
1677
1678 if opening.is_none() {
1679 let mut chars_before = movement::chars_before(map, point).peekable();
1680 while let Some((ch, range)) = chars_before.next() {
1681 if ch == '\n' && !search_across_lines {
1682 break;
1683 }
1684
1685 if let Some((before_ch, _)) = chars_before.peek()
1686 && *before_ch == '\\'
1687 {
1688 continue;
1689 }
1690
1691 if ch == open_marker {
1692 if matched_closes == 0 {
1693 opening = Some(range);
1694 break;
1695 }
1696 matched_closes -= 1;
1697 } else if ch == close_marker {
1698 matched_closes += 1
1699 }
1700 }
1701 }
1702 if opening.is_none() {
1703 for (ch, range) in movement::chars_after(map, point) {
1704 if before_ch != '\\' {
1705 if ch == open_marker {
1706 opening = Some(range);
1707 break;
1708 } else if ch == close_marker {
1709 break;
1710 }
1711 }
1712
1713 before_ch = ch;
1714 }
1715 }
1716
1717 let mut opening = opening?;
1718
1719 let mut matched_opens = 0;
1720 let mut closing = None;
1721 before_ch = match movement::chars_before(map, opening.end).next() {
1722 Some((ch, _)) => ch,
1723 _ => '\0',
1724 };
1725 for (ch, range) in movement::chars_after(map, opening.end) {
1726 if ch == '\n' && !search_across_lines {
1727 break;
1728 }
1729
1730 if before_ch != '\\' {
1731 if ch == close_marker {
1732 if matched_opens == 0 {
1733 closing = Some(range);
1734 break;
1735 }
1736 matched_opens -= 1;
1737 } else if ch == open_marker {
1738 matched_opens += 1;
1739 }
1740 }
1741
1742 before_ch = ch;
1743 }
1744
1745 let mut closing = closing?;
1746
1747 if around && !search_across_lines {
1748 let mut found = false;
1749
1750 for (ch, range) in movement::chars_after(map, closing.end) {
1751 if ch.is_whitespace() && ch != '\n' {
1752 found = true;
1753 closing.end = range.end;
1754 } else {
1755 break;
1756 }
1757 }
1758
1759 if !found {
1760 for (ch, range) in movement::chars_before(map, opening.start) {
1761 if ch.is_whitespace() && ch != '\n' {
1762 opening.start = range.start
1763 } else {
1764 break;
1765 }
1766 }
1767 }
1768 }
1769
1770 // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1771 if !around && open_marker != close_marker {
1772 let start_point = opening.end.to_display_point(map);
1773 let end_point = closing.start.to_display_point(map);
1774 let start_offset = start_point.to_offset(map, Bias::Left);
1775 let end_offset = end_point.to_offset(map, Bias::Left);
1776
1777 if start_point.row() != end_point.row()
1778 && map
1779 .buffer_chars_at(start_offset)
1780 .take_while(|(_, offset)| offset < &end_offset)
1781 .any(|(ch, _)| !ch.is_whitespace())
1782 {
1783 let mut first_non_ws = None;
1784 let mut last_non_ws = None;
1785 for (ch, offset) in map.buffer_chars_at(start_offset) {
1786 if !ch.is_whitespace() {
1787 first_non_ws = Some(offset);
1788 break;
1789 }
1790 }
1791 for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1792 if !ch.is_whitespace() {
1793 last_non_ws = Some(offset + ch.len_utf8());
1794 break;
1795 }
1796 }
1797 if let Some(start) = first_non_ws {
1798 opening.end = start;
1799 }
1800 if let Some(end) = last_non_ws {
1801 closing.start = end;
1802 }
1803 }
1804 }
1805
1806 let result = if around {
1807 opening.start..closing.end
1808 } else {
1809 opening.end..closing.start
1810 };
1811
1812 Some(
1813 map.clip_point(result.start.to_display_point(map), Bias::Left)
1814 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1815 )
1816}
1817
1818#[cfg(test)]
1819mod test {
1820 use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
1821 use gpui::KeyBinding;
1822 use indoc::indoc;
1823 use text::Point;
1824
1825 use crate::{
1826 object::{AnyBrackets, AnyQuotes, MiniBrackets},
1827 state::Mode,
1828 test::{NeovimBackedTestContext, VimTestContext},
1829 };
1830
1831 const WORD_LOCATIONS: &str = indoc! {"
1832 The quick ˇbrowˇnˇ•••
1833 fox ˇjuˇmpsˇ over
1834 the lazy dogˇ••
1835 ˇ
1836 ˇ
1837 ˇ
1838 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1839 ˇ••
1840 ˇ••
1841 ˇ fox-jumpˇs over
1842 the lazy dogˇ•
1843 ˇ
1844 "
1845 };
1846
1847 #[gpui::test]
1848 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1849 let mut cx = NeovimBackedTestContext::new(cx).await;
1850
1851 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1852 .await
1853 .assert_matches();
1854 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1855 .await
1856 .assert_matches();
1857 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1858 .await
1859 .assert_matches();
1860 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1861 .await
1862 .assert_matches();
1863 }
1864
1865 #[gpui::test]
1866 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1867 let mut cx = NeovimBackedTestContext::new(cx).await;
1868
1869 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1870 .await
1871 .assert_matches();
1872 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1873 .await
1874 .assert_matches();
1875 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1876 .await
1877 .assert_matches();
1878 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1879 .await
1880 .assert_matches();
1881 }
1882
1883 #[gpui::test]
1884 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1885 let mut cx = NeovimBackedTestContext::new(cx).await;
1886
1887 /*
1888 cx.set_shared_state("The quick ˇbrown\nfox").await;
1889 cx.simulate_shared_keystrokes(["v"]).await;
1890 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1891 cx.simulate_shared_keystrokes(["i", "w"]).await;
1892 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1893 */
1894 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1895 cx.simulate_shared_keystrokes("v").await;
1896 cx.shared_state()
1897 .await
1898 .assert_eq("The quick brown\n«\nˇ»fox");
1899 cx.simulate_shared_keystrokes("i w").await;
1900 cx.shared_state()
1901 .await
1902 .assert_eq("The quick brown\n«\nˇ»fox");
1903
1904 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1905 .await
1906 .assert_matches();
1907 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1908 .await
1909 .assert_matches();
1910 }
1911
1912 #[gpui::test]
1913 async fn test_word_object_with_count(cx: &mut gpui::TestAppContext) {
1914 let mut cx = NeovimBackedTestContext::new(cx).await;
1915
1916 cx.set_shared_state("ˇone two three four").await;
1917 cx.simulate_shared_keystrokes("2 d a w").await;
1918 cx.shared_state().await.assert_matches();
1919
1920 cx.set_shared_state("ˇone two three four").await;
1921 cx.simulate_shared_keystrokes("d 2 a w").await;
1922 cx.shared_state().await.assert_matches();
1923
1924 // WORD (shift-w) ignores punctuation
1925 cx.set_shared_state("ˇone-two three-four five").await;
1926 cx.simulate_shared_keystrokes("2 d a shift-w").await;
1927 cx.shared_state().await.assert_matches();
1928
1929 cx.set_shared_state("ˇone two three four five").await;
1930 cx.simulate_shared_keystrokes("3 d a w").await;
1931 cx.shared_state().await.assert_matches();
1932
1933 // Multiplied counts: 2d2aw deletes 4 words (2*2)
1934 cx.set_shared_state("ˇone two three four five six").await;
1935 cx.simulate_shared_keystrokes("2 d 2 a w").await;
1936 cx.shared_state().await.assert_matches();
1937
1938 cx.set_shared_state("ˇone two three four").await;
1939 cx.simulate_shared_keystrokes("2 c a w").await;
1940 cx.shared_state().await.assert_matches();
1941
1942 cx.set_shared_state("ˇone two three four").await;
1943 cx.simulate_shared_keystrokes("2 y a w p").await;
1944 cx.shared_state().await.assert_matches();
1945
1946 // Punctuation: foo-bar is 3 word units (foo, -, bar), so 2aw selects "foo-"
1947 cx.set_shared_state(" ˇfoo-bar baz").await;
1948 cx.simulate_shared_keystrokes("2 d a w").await;
1949 cx.shared_state().await.assert_matches();
1950
1951 // Trailing whitespace counts as a word unit for iw
1952 cx.set_shared_state("ˇfoo ").await;
1953 cx.simulate_shared_keystrokes("2 d i w").await;
1954 cx.shared_state().await.assert_matches();
1955
1956 // Multi-line: count > 1 crosses line boundaries
1957 cx.set_shared_state("ˇone\ntwo\nthree").await;
1958 cx.simulate_shared_keystrokes("2 d a w").await;
1959 cx.shared_state().await.assert_matches();
1960
1961 cx.set_shared_state("ˇone\ntwo\nthree\nfour").await;
1962 cx.simulate_shared_keystrokes("3 d a w").await;
1963 cx.shared_state().await.assert_matches();
1964
1965 cx.set_shared_state("ˇone\ntwo\nthree").await;
1966 cx.simulate_shared_keystrokes("2 d i w").await;
1967 cx.shared_state().await.assert_matches();
1968
1969 cx.set_shared_state("one ˇtwo\nthree four").await;
1970 cx.simulate_shared_keystrokes("2 d a w").await;
1971 cx.shared_state().await.assert_matches();
1972 }
1973
1974 const PARAGRAPH_EXAMPLES: &[&str] = &[
1975 // Single line
1976 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1977 // Multiple lines without empty lines
1978 indoc! {"
1979 ˇThe quick brownˇ
1980 ˇfox jumps overˇ
1981 the lazy dog.ˇ
1982 "},
1983 // Heading blank paragraph and trailing normal paragraph
1984 indoc! {"
1985 ˇ
1986 ˇ
1987 ˇThe quick brown fox jumps
1988 ˇover the lazy dog.
1989 ˇ
1990 ˇ
1991 ˇThe quick brown fox jumpsˇ
1992 ˇover the lazy dog.ˇ
1993 "},
1994 // Inserted blank paragraph and trailing blank paragraph
1995 indoc! {"
1996 ˇThe quick brown fox jumps
1997 ˇover the lazy dog.
1998 ˇ
1999 ˇ
2000 ˇ
2001 ˇThe quick brown fox jumpsˇ
2002 ˇover the lazy dog.ˇ
2003 ˇ
2004 ˇ
2005 ˇ
2006 "},
2007 // "Blank" paragraph with whitespace characters
2008 indoc! {"
2009 ˇThe quick brown fox jumps
2010 over the lazy dog.
2011
2012 ˇ \t
2013
2014 ˇThe quick brown fox jumps
2015 over the lazy dog.ˇ
2016 ˇ
2017 ˇ \t
2018 \t \t
2019 "},
2020 // Single line "paragraphs", where selection size might be zero.
2021 indoc! {"
2022 ˇThe quick brown fox jumps over the lazy dog.
2023 ˇ
2024 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
2025 ˇ
2026 "},
2027 ];
2028
2029 #[gpui::test]
2030 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
2031 let mut cx = NeovimBackedTestContext::new(cx).await;
2032
2033 for paragraph_example in PARAGRAPH_EXAMPLES {
2034 cx.simulate_at_each_offset("c i p", paragraph_example)
2035 .await
2036 .assert_matches();
2037 cx.simulate_at_each_offset("c a p", paragraph_example)
2038 .await
2039 .assert_matches();
2040 }
2041 }
2042
2043 #[gpui::test]
2044 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
2045 let mut cx = NeovimBackedTestContext::new(cx).await;
2046
2047 for paragraph_example in PARAGRAPH_EXAMPLES {
2048 cx.simulate_at_each_offset("d i p", paragraph_example)
2049 .await
2050 .assert_matches();
2051 cx.simulate_at_each_offset("d a p", paragraph_example)
2052 .await
2053 .assert_matches();
2054 }
2055 }
2056
2057 #[gpui::test]
2058 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
2059 let mut cx = NeovimBackedTestContext::new(cx).await;
2060
2061 const EXAMPLES: &[&str] = &[
2062 indoc! {"
2063 ˇThe quick brown
2064 fox jumps over
2065 the lazy dog.
2066 "},
2067 indoc! {"
2068 ˇ
2069
2070 ˇThe quick brown fox jumps
2071 over the lazy dog.
2072 ˇ
2073
2074 ˇThe quick brown fox jumps
2075 over the lazy dog.
2076 "},
2077 indoc! {"
2078 ˇThe quick brown fox jumps over the lazy dog.
2079 ˇ
2080 ˇThe quick brown fox jumps over the lazy dog.
2081
2082 "},
2083 ];
2084
2085 for paragraph_example in EXAMPLES {
2086 cx.simulate_at_each_offset("v i p", paragraph_example)
2087 .await
2088 .assert_matches();
2089 cx.simulate_at_each_offset("v a p", paragraph_example)
2090 .await
2091 .assert_matches();
2092 }
2093 }
2094
2095 #[gpui::test]
2096 async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2097 let mut cx = NeovimBackedTestContext::new(cx).await;
2098
2099 const WRAPPING_EXAMPLE: &str = indoc! {"
2100 ˇ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.
2101
2102 ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2103
2104 ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2105 "};
2106
2107 cx.set_shared_wrap(20).await;
2108
2109 cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
2110 .await
2111 .assert_matches();
2112 cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
2113 .await
2114 .assert_matches();
2115 }
2116
2117 #[gpui::test]
2118 async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2119 let mut cx = NeovimBackedTestContext::new(cx).await;
2120
2121 const WRAPPING_EXAMPLE: &str = indoc! {"
2122 ˇ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.
2123
2124 ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2125
2126 ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2127 "};
2128
2129 cx.set_shared_wrap(20).await;
2130
2131 cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
2132 .await
2133 .assert_matches();
2134 cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
2135 .await
2136 .assert_matches();
2137 }
2138
2139 #[gpui::test]
2140 async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
2141 let mut cx = NeovimBackedTestContext::new(cx).await;
2142
2143 cx.set_shared_state(indoc! {"
2144 a
2145 ˇ•
2146 aaaaaaaaaaaaa
2147 "})
2148 .await;
2149
2150 cx.simulate_shared_keystrokes("d i p").await;
2151 cx.shared_state().await.assert_eq(indoc! {"
2152 a
2153 aaaaaaaˇaaaaaa
2154 "});
2155 }
2156
2157 #[gpui::test]
2158 async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2159 let mut cx = NeovimBackedTestContext::new(cx).await;
2160
2161 const WRAPPING_EXAMPLE: &str = indoc! {"
2162 ˇ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.
2163
2164 ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2165
2166 ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2167 "};
2168
2169 cx.set_shared_wrap(20).await;
2170
2171 cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
2172 .await
2173 .assert_matches();
2174 cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
2175 .await
2176 .assert_matches();
2177 }
2178
2179 // Test string with "`" for opening surrounders and "'" for closing surrounders
2180 const SURROUNDING_MARKER_STRING: &str = indoc! {"
2181 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
2182 'ˇfox juˇmps ov`ˇer
2183 the ˇlazy d'o`ˇg"};
2184
2185 const SURROUNDING_OBJECTS: &[(char, char)] = &[
2186 ('"', '"'), // Double Quote
2187 ('(', ')'), // Parentheses
2188 ];
2189
2190 #[gpui::test]
2191 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2192 let mut cx = NeovimBackedTestContext::new(cx).await;
2193
2194 for (start, end) in SURROUNDING_OBJECTS {
2195 let marked_string = SURROUNDING_MARKER_STRING
2196 .replace('`', &start.to_string())
2197 .replace('\'', &end.to_string());
2198
2199 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
2200 .await
2201 .assert_matches();
2202 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
2203 .await
2204 .assert_matches();
2205 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
2206 .await
2207 .assert_matches();
2208 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
2209 .await
2210 .assert_matches();
2211 }
2212 }
2213 #[gpui::test]
2214 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2215 let mut cx = NeovimBackedTestContext::new(cx).await;
2216 cx.set_shared_wrap(12).await;
2217
2218 cx.set_shared_state(indoc! {
2219 "\"ˇhello world\"!"
2220 })
2221 .await;
2222 cx.simulate_shared_keystrokes("v i \"").await;
2223 cx.shared_state().await.assert_eq(indoc! {
2224 "\"«hello worldˇ»\"!"
2225 });
2226
2227 cx.set_shared_state(indoc! {
2228 "\"hˇello world\"!"
2229 })
2230 .await;
2231 cx.simulate_shared_keystrokes("v i \"").await;
2232 cx.shared_state().await.assert_eq(indoc! {
2233 "\"«hello worldˇ»\"!"
2234 });
2235
2236 cx.set_shared_state(indoc! {
2237 "helˇlo \"world\"!"
2238 })
2239 .await;
2240 cx.simulate_shared_keystrokes("v i \"").await;
2241 cx.shared_state().await.assert_eq(indoc! {
2242 "hello \"«worldˇ»\"!"
2243 });
2244
2245 cx.set_shared_state(indoc! {
2246 "hello \"wˇorld\"!"
2247 })
2248 .await;
2249 cx.simulate_shared_keystrokes("v i \"").await;
2250 cx.shared_state().await.assert_eq(indoc! {
2251 "hello \"«worldˇ»\"!"
2252 });
2253
2254 cx.set_shared_state(indoc! {
2255 "hello \"wˇorld\"!"
2256 })
2257 .await;
2258 cx.simulate_shared_keystrokes("v a \"").await;
2259 cx.shared_state().await.assert_eq(indoc! {
2260 "hello« \"world\"ˇ»!"
2261 });
2262
2263 cx.set_shared_state(indoc! {
2264 "hello \"wˇorld\" !"
2265 })
2266 .await;
2267 cx.simulate_shared_keystrokes("v a \"").await;
2268 cx.shared_state().await.assert_eq(indoc! {
2269 "hello «\"world\" ˇ»!"
2270 });
2271
2272 cx.set_shared_state(indoc! {
2273 "hello \"wˇorld\"•
2274 goodbye"
2275 })
2276 .await;
2277 cx.simulate_shared_keystrokes("v a \"").await;
2278 cx.shared_state().await.assert_eq(indoc! {
2279 "hello «\"world\" ˇ»
2280 goodbye"
2281 });
2282 }
2283
2284 #[gpui::test]
2285 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2286 let mut cx = VimTestContext::new(cx, true).await;
2287
2288 cx.set_state(
2289 indoc! {
2290 "func empty(a string) bool {
2291 if a == \"\" {
2292 return true
2293 }
2294 ˇreturn false
2295 }"
2296 },
2297 Mode::Normal,
2298 );
2299 cx.simulate_keystrokes("v i {");
2300 cx.assert_state(
2301 indoc! {
2302 "func empty(a string) bool {
2303 «if a == \"\" {
2304 return true
2305 }
2306 return falseˇ»
2307 }"
2308 },
2309 Mode::Visual,
2310 );
2311
2312 cx.set_state(
2313 indoc! {
2314 "func empty(a string) bool {
2315 if a == \"\" {
2316 ˇreturn true
2317 }
2318 return false
2319 }"
2320 },
2321 Mode::Normal,
2322 );
2323 cx.simulate_keystrokes("v i {");
2324 cx.assert_state(
2325 indoc! {
2326 "func empty(a string) bool {
2327 if a == \"\" {
2328 «return trueˇ»
2329 }
2330 return false
2331 }"
2332 },
2333 Mode::Visual,
2334 );
2335
2336 cx.set_state(
2337 indoc! {
2338 "func empty(a string) bool {
2339 if a == \"\" ˇ{
2340 return true
2341 }
2342 return false
2343 }"
2344 },
2345 Mode::Normal,
2346 );
2347 cx.simulate_keystrokes("v i {");
2348 cx.assert_state(
2349 indoc! {
2350 "func empty(a string) bool {
2351 if a == \"\" {
2352 «return trueˇ»
2353 }
2354 return false
2355 }"
2356 },
2357 Mode::Visual,
2358 );
2359
2360 cx.set_state(
2361 indoc! {
2362 "func empty(a string) bool {
2363 if a == \"\" {
2364 return true
2365 }
2366 return false
2367 ˇ}"
2368 },
2369 Mode::Normal,
2370 );
2371 cx.simulate_keystrokes("v i {");
2372 cx.assert_state(
2373 indoc! {
2374 "func empty(a string) bool {
2375 «if a == \"\" {
2376 return true
2377 }
2378 return falseˇ»
2379 }"
2380 },
2381 Mode::Visual,
2382 );
2383
2384 cx.set_state(
2385 indoc! {
2386 "func empty(a string) bool {
2387 if a == \"\" {
2388 ˇ
2389
2390 }"
2391 },
2392 Mode::Normal,
2393 );
2394 cx.simulate_keystrokes("c i {");
2395 cx.assert_state(
2396 indoc! {
2397 "func empty(a string) bool {
2398 if a == \"\" {ˇ}"
2399 },
2400 Mode::Insert,
2401 );
2402 }
2403
2404 #[gpui::test]
2405 async fn test_singleline_surrounding_character_objects_with_escape(
2406 cx: &mut gpui::TestAppContext,
2407 ) {
2408 let mut cx = NeovimBackedTestContext::new(cx).await;
2409 cx.set_shared_state(indoc! {
2410 "h\"e\\\"lˇlo \\\"world\"!"
2411 })
2412 .await;
2413 cx.simulate_shared_keystrokes("v i \"").await;
2414 cx.shared_state().await.assert_eq(indoc! {
2415 "h\"«e\\\"llo \\\"worldˇ»\"!"
2416 });
2417
2418 cx.set_shared_state(indoc! {
2419 "hello \"teˇst \\\"inside\\\" world\""
2420 })
2421 .await;
2422 cx.simulate_shared_keystrokes("v i \"").await;
2423 cx.shared_state().await.assert_eq(indoc! {
2424 "hello \"«test \\\"inside\\\" worldˇ»\""
2425 });
2426 }
2427
2428 #[gpui::test]
2429 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2430 let mut cx = VimTestContext::new(cx, true).await;
2431 cx.set_state(
2432 indoc! {"
2433 fn boop() {
2434 baz(ˇ|a, b| { bar(|j, k| { })})
2435 }"
2436 },
2437 Mode::Normal,
2438 );
2439 cx.simulate_keystrokes("c i |");
2440 cx.assert_state(
2441 indoc! {"
2442 fn boop() {
2443 baz(|ˇ| { bar(|j, k| { })})
2444 }"
2445 },
2446 Mode::Insert,
2447 );
2448 cx.simulate_keystrokes("escape 1 8 |");
2449 cx.assert_state(
2450 indoc! {"
2451 fn boop() {
2452 baz(|| { bar(ˇ|j, k| { })})
2453 }"
2454 },
2455 Mode::Normal,
2456 );
2457
2458 cx.simulate_keystrokes("v a |");
2459 cx.assert_state(
2460 indoc! {"
2461 fn boop() {
2462 baz(|| { bar(«|j, k| ˇ»{ })})
2463 }"
2464 },
2465 Mode::Visual,
2466 );
2467 }
2468
2469 #[gpui::test]
2470 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2471 let mut cx = VimTestContext::new(cx, true).await;
2472
2473 // Generic arguments
2474 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2475 cx.simulate_keystrokes("v i a");
2476 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2477
2478 // Function arguments
2479 cx.set_state(
2480 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2481 Mode::Normal,
2482 );
2483 cx.simulate_keystrokes("d a a");
2484 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2485
2486 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2487 cx.simulate_keystrokes("v a a");
2488 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2489
2490 // Tuple, vec, and array arguments
2491 cx.set_state(
2492 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2493 Mode::Normal,
2494 );
2495 cx.simulate_keystrokes("c i a");
2496 cx.assert_state(
2497 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2498 Mode::Insert,
2499 );
2500
2501 // TODO regressed with the up-to-date Rust grammar.
2502 // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2503 // cx.simulate_keystrokes("c a a");
2504 // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2505
2506 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2507 cx.simulate_keystrokes("c i a");
2508 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2509
2510 cx.set_state(
2511 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2512 Mode::Normal,
2513 );
2514 cx.simulate_keystrokes("c a a");
2515 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2516
2517 // Cursor immediately before / after brackets
2518 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2519 cx.simulate_keystrokes("v i a");
2520 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2521
2522 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2523 cx.simulate_keystrokes("v i a");
2524 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2525 }
2526
2527 #[gpui::test]
2528 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2529 let mut cx = VimTestContext::new(cx, true).await;
2530
2531 // Base use case
2532 cx.set_state(
2533 indoc! {"
2534 fn boop() {
2535 // Comment
2536 baz();ˇ
2537
2538 loop {
2539 bar(1);
2540 bar(2);
2541 }
2542
2543 result
2544 }
2545 "},
2546 Mode::Normal,
2547 );
2548 cx.simulate_keystrokes("v i i");
2549 cx.assert_state(
2550 indoc! {"
2551 fn boop() {
2552 « // Comment
2553 baz();
2554
2555 loop {
2556 bar(1);
2557 bar(2);
2558 }
2559
2560 resultˇ»
2561 }
2562 "},
2563 Mode::Visual,
2564 );
2565
2566 // Around indent (include line above)
2567 cx.set_state(
2568 indoc! {"
2569 const ABOVE: str = true;
2570 fn boop() {
2571
2572 hello();
2573 worˇld()
2574 }
2575 "},
2576 Mode::Normal,
2577 );
2578 cx.simulate_keystrokes("v a i");
2579 cx.assert_state(
2580 indoc! {"
2581 const ABOVE: str = true;
2582 «fn boop() {
2583
2584 hello();
2585 world()ˇ»
2586 }
2587 "},
2588 Mode::Visual,
2589 );
2590
2591 // Around indent (include line above & below)
2592 cx.set_state(
2593 indoc! {"
2594 const ABOVE: str = true;
2595 fn boop() {
2596 hellˇo();
2597 world()
2598
2599 }
2600 const BELOW: str = true;
2601 "},
2602 Mode::Normal,
2603 );
2604 cx.simulate_keystrokes("c a shift-i");
2605 cx.assert_state(
2606 indoc! {"
2607 const ABOVE: str = true;
2608 ˇ
2609 const BELOW: str = true;
2610 "},
2611 Mode::Insert,
2612 );
2613 }
2614
2615 #[gpui::test]
2616 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2617 let mut cx = NeovimBackedTestContext::new(cx).await;
2618
2619 for (start, end) in SURROUNDING_OBJECTS {
2620 let marked_string = SURROUNDING_MARKER_STRING
2621 .replace('`', &start.to_string())
2622 .replace('\'', &end.to_string());
2623
2624 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2625 .await
2626 .assert_matches();
2627 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2628 .await
2629 .assert_matches();
2630 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2631 .await
2632 .assert_matches();
2633 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2634 .await
2635 .assert_matches();
2636 }
2637 }
2638
2639 #[gpui::test]
2640 async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2641 let mut cx = VimTestContext::new(cx, true).await;
2642 cx.update(|_, cx| {
2643 cx.bind_keys([KeyBinding::new(
2644 "q",
2645 AnyQuotes,
2646 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2647 )]);
2648 });
2649
2650 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2651 // the false string in the middle should be considered
2652 (
2653 "c i q",
2654 "'first' false ˇstring 'second'",
2655 "'first'ˇ'second'",
2656 Mode::Insert,
2657 ),
2658 // Single quotes
2659 (
2660 "c i q",
2661 "Thisˇ is a 'quote' example.",
2662 "This is a 'ˇ' example.",
2663 Mode::Insert,
2664 ),
2665 (
2666 "c a q",
2667 "Thisˇ is a 'quote' example.",
2668 "This is a ˇexample.",
2669 Mode::Insert,
2670 ),
2671 (
2672 "c i q",
2673 "This is a \"simple 'qˇuote'\" example.",
2674 "This is a \"simple 'ˇ'\" example.",
2675 Mode::Insert,
2676 ),
2677 (
2678 "c a q",
2679 "This is a \"simple 'qˇuote'\" example.",
2680 "This is a \"simpleˇ\" example.",
2681 Mode::Insert,
2682 ),
2683 (
2684 "c i q",
2685 "This is a 'qˇuote' example.",
2686 "This is a 'ˇ' example.",
2687 Mode::Insert,
2688 ),
2689 (
2690 "c a q",
2691 "This is a 'qˇuote' example.",
2692 "This is a ˇexample.",
2693 Mode::Insert,
2694 ),
2695 (
2696 "d i q",
2697 "This is a 'qˇuote' example.",
2698 "This is a 'ˇ' example.",
2699 Mode::Normal,
2700 ),
2701 (
2702 "d a q",
2703 "This is a 'qˇuote' example.",
2704 "This is a ˇexample.",
2705 Mode::Normal,
2706 ),
2707 // Double quotes
2708 (
2709 "c i q",
2710 "This is a \"qˇuote\" example.",
2711 "This is a \"ˇ\" example.",
2712 Mode::Insert,
2713 ),
2714 (
2715 "c a q",
2716 "This is a \"qˇuote\" example.",
2717 "This is a ˇexample.",
2718 Mode::Insert,
2719 ),
2720 (
2721 "d i q",
2722 "This is a \"qˇuote\" example.",
2723 "This is a \"ˇ\" example.",
2724 Mode::Normal,
2725 ),
2726 (
2727 "d a q",
2728 "This is a \"qˇuote\" example.",
2729 "This is a ˇexample.",
2730 Mode::Normal,
2731 ),
2732 // Back quotes
2733 (
2734 "c i q",
2735 "This is a `qˇuote` example.",
2736 "This is a `ˇ` example.",
2737 Mode::Insert,
2738 ),
2739 (
2740 "c a q",
2741 "This is a `qˇuote` example.",
2742 "This is a ˇexample.",
2743 Mode::Insert,
2744 ),
2745 (
2746 "d i q",
2747 "This is a `qˇuote` example.",
2748 "This is a `ˇ` example.",
2749 Mode::Normal,
2750 ),
2751 (
2752 "d a q",
2753 "This is a `qˇuote` example.",
2754 "This is a ˇexample.",
2755 Mode::Normal,
2756 ),
2757 ];
2758
2759 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2760 cx.set_state(initial_state, Mode::Normal);
2761
2762 cx.simulate_keystrokes(keystrokes);
2763
2764 cx.assert_state(expected_state, *expected_mode);
2765 }
2766
2767 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2768 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2769 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2770 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2771 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2772 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2773 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2774 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2775 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2776 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2777 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2778 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2779 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2780 ];
2781
2782 for (keystrokes, initial_state, mode) in INVALID_CASES {
2783 cx.set_state(initial_state, Mode::Normal);
2784
2785 cx.simulate_keystrokes(keystrokes);
2786
2787 cx.assert_state(initial_state, *mode);
2788 }
2789 }
2790
2791 #[gpui::test]
2792 async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2793 let mut cx = VimTestContext::new_typescript(cx).await;
2794
2795 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2796 // Special cases from mini.ai plugin
2797 // the false string in the middle should not be considered
2798 (
2799 "c i q",
2800 "'first' false ˇstring 'second'",
2801 "'first' false string 'ˇ'",
2802 Mode::Insert,
2803 ),
2804 // Multiline support :)! Same behavior as mini.ai plugin
2805 (
2806 "c i q",
2807 indoc! {"
2808 `
2809 first
2810 middle ˇstring
2811 second
2812 `
2813 "},
2814 indoc! {"
2815 `ˇ`
2816 "},
2817 Mode::Insert,
2818 ),
2819 // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2820 // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2821 // Bug reference: https://github.com/zed-industries/zed/issues/23889
2822 ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2823 // Single quotes
2824 (
2825 "c i q",
2826 "Thisˇ is a 'quote' example.",
2827 "This is a 'ˇ' example.",
2828 Mode::Insert,
2829 ),
2830 (
2831 "c a q",
2832 "Thisˇ is a 'quote' example.",
2833 "This is a ˇ example.", // same mini.ai plugin behavior
2834 Mode::Insert,
2835 ),
2836 (
2837 "c i q",
2838 "This is a \"simple 'qˇuote'\" example.",
2839 "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2840 Mode::Insert,
2841 ),
2842 (
2843 "c a q",
2844 "This is a \"simple 'qˇuote'\" example.",
2845 "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2846 Mode::Insert,
2847 ),
2848 (
2849 "c i q",
2850 "This is a 'qˇuote' example.",
2851 "This is a 'ˇ' example.",
2852 Mode::Insert,
2853 ),
2854 (
2855 "c a q",
2856 "This is a 'qˇuote' example.",
2857 "This is a ˇ example.", // same mini.ai plugin behavior
2858 Mode::Insert,
2859 ),
2860 (
2861 "d i q",
2862 "This is a 'qˇuote' example.",
2863 "This is a 'ˇ' example.",
2864 Mode::Normal,
2865 ),
2866 (
2867 "d a q",
2868 "This is a 'qˇuote' example.",
2869 "This is a ˇ example.", // same mini.ai plugin behavior
2870 Mode::Normal,
2871 ),
2872 // Double quotes
2873 (
2874 "c i q",
2875 "This is a \"qˇuote\" example.",
2876 "This is a \"ˇ\" example.",
2877 Mode::Insert,
2878 ),
2879 (
2880 "c a q",
2881 "This is a \"qˇuote\" example.",
2882 "This is a ˇ example.", // same mini.ai plugin behavior
2883 Mode::Insert,
2884 ),
2885 (
2886 "d i q",
2887 "This is a \"qˇuote\" example.",
2888 "This is a \"ˇ\" example.",
2889 Mode::Normal,
2890 ),
2891 (
2892 "d a q",
2893 "This is a \"qˇuote\" example.",
2894 "This is a ˇ example.", // same mini.ai plugin behavior
2895 Mode::Normal,
2896 ),
2897 // Back quotes
2898 (
2899 "c i q",
2900 "This is a `qˇuote` example.",
2901 "This is a `ˇ` example.",
2902 Mode::Insert,
2903 ),
2904 (
2905 "c a q",
2906 "This is a `qˇuote` example.",
2907 "This is a ˇ example.", // same mini.ai plugin behavior
2908 Mode::Insert,
2909 ),
2910 (
2911 "d i q",
2912 "This is a `qˇuote` example.",
2913 "This is a `ˇ` example.",
2914 Mode::Normal,
2915 ),
2916 (
2917 "d a q",
2918 "This is a `qˇuote` example.",
2919 "This is a ˇ example.", // same mini.ai plugin behavior
2920 Mode::Normal,
2921 ),
2922 ];
2923
2924 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2925 cx.set_state(initial_state, Mode::Normal);
2926 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2927 cx.simulate_keystrokes(keystrokes);
2928 cx.assert_state(expected_state, *expected_mode);
2929 }
2930
2931 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2932 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2933 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2934 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2935 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2936 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2937 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2938 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2939 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2940 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2941 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2942 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2943 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2944 ];
2945
2946 for (keystrokes, initial_state, mode) in INVALID_CASES {
2947 cx.set_state(initial_state, Mode::Normal);
2948 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2949 cx.simulate_keystrokes(keystrokes);
2950 cx.assert_state(initial_state, *mode);
2951 }
2952 }
2953
2954 #[gpui::test]
2955 async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2956 let mut cx = VimTestContext::new(cx, true).await;
2957 cx.update(|_, cx| {
2958 cx.bind_keys([KeyBinding::new(
2959 "b",
2960 AnyBrackets,
2961 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2962 )]);
2963 });
2964
2965 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2966 (
2967 "c i b",
2968 indoc! {"
2969 {
2970 {
2971 ˇprint('hello')
2972 }
2973 }
2974 "},
2975 indoc! {"
2976 {
2977 {
2978 ˇ
2979 }
2980 }
2981 "},
2982 Mode::Insert,
2983 ),
2984 // Bracket (Parentheses)
2985 (
2986 "c i b",
2987 "Thisˇ is a (simple [quote]) example.",
2988 "This is a (ˇ) example.",
2989 Mode::Insert,
2990 ),
2991 (
2992 "c i b",
2993 "This is a [simple (qˇuote)] example.",
2994 "This is a [simple (ˇ)] example.",
2995 Mode::Insert,
2996 ),
2997 (
2998 "c a b",
2999 "This is a [simple (qˇuote)] example.",
3000 "This is a [simple ˇ] example.",
3001 Mode::Insert,
3002 ),
3003 (
3004 "c a b",
3005 "Thisˇ is a (simple [quote]) example.",
3006 "This is a ˇ example.",
3007 Mode::Insert,
3008 ),
3009 (
3010 "c i b",
3011 "This is a (qˇuote) example.",
3012 "This is a (ˇ) example.",
3013 Mode::Insert,
3014 ),
3015 (
3016 "c a b",
3017 "This is a (qˇuote) example.",
3018 "This is a ˇ example.",
3019 Mode::Insert,
3020 ),
3021 (
3022 "d i b",
3023 "This is a (qˇuote) example.",
3024 "This is a (ˇ) example.",
3025 Mode::Normal,
3026 ),
3027 (
3028 "d a b",
3029 "This is a (qˇuote) example.",
3030 "This is a ˇ example.",
3031 Mode::Normal,
3032 ),
3033 // Square brackets
3034 (
3035 "c i b",
3036 "This is a [qˇuote] example.",
3037 "This is a [ˇ] example.",
3038 Mode::Insert,
3039 ),
3040 (
3041 "c a b",
3042 "This is a [qˇuote] example.",
3043 "This is a ˇ example.",
3044 Mode::Insert,
3045 ),
3046 (
3047 "d i b",
3048 "This is a [qˇuote] example.",
3049 "This is a [ˇ] example.",
3050 Mode::Normal,
3051 ),
3052 (
3053 "d a b",
3054 "This is a [qˇuote] example.",
3055 "This is a ˇ example.",
3056 Mode::Normal,
3057 ),
3058 // Curly brackets
3059 (
3060 "c i b",
3061 "This is a {qˇuote} example.",
3062 "This is a {ˇ} example.",
3063 Mode::Insert,
3064 ),
3065 (
3066 "c a b",
3067 "This is a {qˇuote} example.",
3068 "This is a ˇ example.",
3069 Mode::Insert,
3070 ),
3071 (
3072 "d i b",
3073 "This is a {qˇuote} example.",
3074 "This is a {ˇ} example.",
3075 Mode::Normal,
3076 ),
3077 (
3078 "d a b",
3079 "This is a {qˇuote} example.",
3080 "This is a ˇ example.",
3081 Mode::Normal,
3082 ),
3083 ];
3084
3085 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3086 cx.set_state(initial_state, Mode::Normal);
3087
3088 cx.simulate_keystrokes(keystrokes);
3089
3090 cx.assert_state(expected_state, *expected_mode);
3091 }
3092
3093 const INVALID_CASES: &[(&str, &str, Mode)] = &[
3094 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3095 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3096 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3097 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3098 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3099 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3100 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3101 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3102 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3103 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3104 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3105 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3106 ];
3107
3108 for (keystrokes, initial_state, mode) in INVALID_CASES {
3109 cx.set_state(initial_state, Mode::Normal);
3110
3111 cx.simulate_keystrokes(keystrokes);
3112
3113 cx.assert_state(initial_state, *mode);
3114 }
3115 }
3116
3117 #[gpui::test]
3118 async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
3119 let mut cx = VimTestContext::new(cx, true).await;
3120 cx.update(|_, cx| {
3121 cx.bind_keys([KeyBinding::new(
3122 "b",
3123 MiniBrackets,
3124 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
3125 )]);
3126 });
3127
3128 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
3129 // Special cases from mini.ai plugin
3130 // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
3131 // Same behavior as mini.ai plugin
3132 (
3133 "c i b",
3134 indoc! {"
3135 {
3136 {
3137 ˇprint('hello')
3138 }
3139 }
3140 "},
3141 indoc! {"
3142 {
3143 {
3144 print(ˇ)
3145 }
3146 }
3147 "},
3148 Mode::Insert,
3149 ),
3150 // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3151 // Same behavior as mini.ai plugin
3152 (
3153 "c i b",
3154 indoc! {"
3155 {
3156 {
3157 ˇ
3158 print('hello')
3159 }
3160 }
3161 "},
3162 indoc! {"
3163 {
3164 {ˇ}
3165 }
3166 "},
3167 Mode::Insert,
3168 ),
3169 // If you are in the open 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 // If you are in the close bracket then it has higher priority
3185 (
3186 "c i b",
3187 indoc! {"
3188 {
3189 {
3190 print('hello')
3191 }
3192 «}ˇ»
3193 "},
3194 indoc! {"
3195 {ˇ}
3196 "},
3197 Mode::Insert,
3198 ),
3199 // Bracket (Parentheses)
3200 (
3201 "c i b",
3202 "Thisˇ is a (simple [quote]) example.",
3203 "This is a (ˇ) example.",
3204 Mode::Insert,
3205 ),
3206 (
3207 "c i b",
3208 "This is a [simple (qˇuote)] example.",
3209 "This is a [simple (ˇ)] example.",
3210 Mode::Insert,
3211 ),
3212 (
3213 "c a b",
3214 "This is a [simple (qˇuote)] example.",
3215 "This is a [simple ˇ] example.",
3216 Mode::Insert,
3217 ),
3218 (
3219 "c a b",
3220 "Thisˇ is a (simple [quote]) example.",
3221 "This is a ˇ example.",
3222 Mode::Insert,
3223 ),
3224 (
3225 "c i b",
3226 "This is a (qˇuote) example.",
3227 "This is a (ˇ) example.",
3228 Mode::Insert,
3229 ),
3230 (
3231 "c a b",
3232 "This is a (qˇuote) example.",
3233 "This is a ˇ example.",
3234 Mode::Insert,
3235 ),
3236 (
3237 "d i b",
3238 "This is a (qˇuote) example.",
3239 "This is a (ˇ) example.",
3240 Mode::Normal,
3241 ),
3242 (
3243 "d a b",
3244 "This is a (qˇuote) example.",
3245 "This is a ˇ example.",
3246 Mode::Normal,
3247 ),
3248 // Square brackets
3249 (
3250 "c i b",
3251 "This is a [qˇuote] example.",
3252 "This is a [ˇ] example.",
3253 Mode::Insert,
3254 ),
3255 (
3256 "c a b",
3257 "This is a [qˇuote] example.",
3258 "This is a ˇ example.",
3259 Mode::Insert,
3260 ),
3261 (
3262 "d i b",
3263 "This is a [qˇuote] example.",
3264 "This is a [ˇ] example.",
3265 Mode::Normal,
3266 ),
3267 (
3268 "d a b",
3269 "This is a [qˇuote] example.",
3270 "This is a ˇ example.",
3271 Mode::Normal,
3272 ),
3273 // Curly brackets
3274 (
3275 "c i b",
3276 "This is a {qˇuote} example.",
3277 "This is a {ˇ} example.",
3278 Mode::Insert,
3279 ),
3280 (
3281 "c a b",
3282 "This is a {qˇuote} example.",
3283 "This is a ˇ example.",
3284 Mode::Insert,
3285 ),
3286 (
3287 "d i b",
3288 "This is a {qˇuote} example.",
3289 "This is a {ˇ} example.",
3290 Mode::Normal,
3291 ),
3292 (
3293 "d a b",
3294 "This is a {qˇuote} example.",
3295 "This is a ˇ example.",
3296 Mode::Normal,
3297 ),
3298 ];
3299
3300 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3301 cx.set_state(initial_state, Mode::Normal);
3302 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3303 cx.simulate_keystrokes(keystrokes);
3304 cx.assert_state(expected_state, *expected_mode);
3305 }
3306
3307 const INVALID_CASES: &[(&str, &str, Mode)] = &[
3308 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3309 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3310 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3311 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3312 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3313 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3314 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3315 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3316 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3317 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3318 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3319 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3320 ];
3321
3322 for (keystrokes, initial_state, mode) in INVALID_CASES {
3323 cx.set_state(initial_state, Mode::Normal);
3324 cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3325 cx.simulate_keystrokes(keystrokes);
3326 cx.assert_state(initial_state, *mode);
3327 }
3328 }
3329
3330 #[gpui::test]
3331 async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
3332 // Initialize test context with the TypeScript language loaded, so we
3333 // can actually get brackets definition.
3334 let mut cx = VimTestContext::new(cx, true).await;
3335
3336 // Update `b` to `MiniBrackets` so we can later use it when simulating
3337 // keystrokes.
3338 cx.update(|_, cx| {
3339 cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
3340 });
3341
3342 let (editor, cx) = cx.add_window_view(|window, cx| {
3343 let multi_buffer = MultiBuffer::build_multi(
3344 [
3345 ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
3346 ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
3347 ],
3348 cx,
3349 );
3350
3351 // In order for the brackets to actually be found, we need to update
3352 // the language used for the second buffer. This is something that
3353 // is handled automatically when simply using `VimTestContext::new`
3354 // but, since this is being set manually, the language isn't
3355 // automatically set.
3356 let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
3357 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
3358 if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
3359 buffer.update(cx, |buffer, cx| {
3360 buffer.set_language(Some(language::rust_lang()), cx);
3361 })
3362 };
3363
3364 editor
3365 });
3366
3367 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3368
3369 cx.assert_excerpts_with_selections(indoc! {"
3370 [EXCERPT]
3371 ˇ111
3372 222
3373 [EXCERPT]
3374 111
3375 a {bracket} example
3376 "
3377 });
3378
3379 cx.simulate_keystrokes("j j j j f r");
3380 cx.assert_excerpts_with_selections(indoc! {"
3381 [EXCERPT]
3382 111
3383 222
3384 [EXCERPT]
3385 111
3386 a {bˇracket} example
3387 "
3388 });
3389
3390 cx.simulate_keystrokes("d i b");
3391 cx.assert_excerpts_with_selections(indoc! {"
3392 [EXCERPT]
3393 111
3394 222
3395 [EXCERPT]
3396 111
3397 a {ˇ} example
3398 "
3399 });
3400 }
3401
3402 #[gpui::test]
3403 async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3404 let mut cx = NeovimBackedTestContext::new(cx).await;
3405 cx.set_shared_state("(trailingˇ whitespace )")
3406 .await;
3407 cx.simulate_shared_keystrokes("v i b").await;
3408 cx.shared_state().await.assert_matches();
3409 cx.simulate_shared_keystrokes("escape y i b").await;
3410 cx.shared_clipboard()
3411 .await
3412 .assert_eq("trailing whitespace ");
3413 }
3414
3415 #[gpui::test]
3416 async fn test_tags(cx: &mut gpui::TestAppContext) {
3417 let mut cx = VimTestContext::new_html(cx).await;
3418
3419 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3420 cx.simulate_keystrokes("v i t");
3421 cx.assert_state(
3422 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3423 Mode::Visual,
3424 );
3425 cx.simulate_keystrokes("a t");
3426 cx.assert_state(
3427 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3428 Mode::Visual,
3429 );
3430 cx.simulate_keystrokes("a t");
3431 cx.assert_state(
3432 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3433 Mode::Visual,
3434 );
3435
3436 // The cursor is before the tag
3437 cx.set_state(
3438 "<html><head></head><body> ˇ <b>hi!</b></body>",
3439 Mode::Normal,
3440 );
3441 cx.simulate_keystrokes("v i t");
3442 cx.assert_state(
3443 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
3444 Mode::Visual,
3445 );
3446 cx.simulate_keystrokes("a t");
3447 cx.assert_state(
3448 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
3449 Mode::Visual,
3450 );
3451
3452 // The cursor is in the open tag
3453 cx.set_state(
3454 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3455 Mode::Normal,
3456 );
3457 cx.simulate_keystrokes("v a t");
3458 cx.assert_state(
3459 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3460 Mode::Visual,
3461 );
3462 cx.simulate_keystrokes("i t");
3463 cx.assert_state(
3464 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3465 Mode::Visual,
3466 );
3467
3468 // current selection length greater than 1
3469 cx.set_state(
3470 "<html><head></head><body><«b>hi!ˇ»</b></body>",
3471 Mode::Visual,
3472 );
3473 cx.simulate_keystrokes("i t");
3474 cx.assert_state(
3475 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3476 Mode::Visual,
3477 );
3478 cx.simulate_keystrokes("a t");
3479 cx.assert_state(
3480 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3481 Mode::Visual,
3482 );
3483
3484 cx.set_state(
3485 "<html><head></head><body><«b>hi!</ˇ»b></body>",
3486 Mode::Visual,
3487 );
3488 cx.simulate_keystrokes("a t");
3489 cx.assert_state(
3490 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3491 Mode::Visual,
3492 );
3493 }
3494 #[gpui::test]
3495 async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3496 let mut cx = NeovimBackedTestContext::new(cx).await;
3497
3498 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3499 .await;
3500 cx.simulate_shared_keystrokes("v a w").await;
3501 cx.shared_state()
3502 .await
3503 .assert_eq(" «const ˇ»f = (x: unknown) => {");
3504
3505 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3506 .await;
3507 cx.simulate_shared_keystrokes("y a w").await;
3508 cx.shared_clipboard().await.assert_eq("const ");
3509
3510 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3511 .await;
3512 cx.simulate_shared_keystrokes("d a w").await;
3513 cx.shared_state()
3514 .await
3515 .assert_eq(" ˇf = (x: unknown) => {");
3516 cx.shared_clipboard().await.assert_eq("const ");
3517
3518 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3519 .await;
3520 cx.simulate_shared_keystrokes("c a w").await;
3521 cx.shared_state()
3522 .await
3523 .assert_eq(" ˇf = (x: unknown) => {");
3524 cx.shared_clipboard().await.assert_eq("const ");
3525 }
3526
3527 #[gpui::test]
3528 async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
3529 let mut cx = VimTestContext::new_typescript(cx).await;
3530
3531 cx.set_state(
3532 indoc! {"
3533 const foo = () => {
3534 return ˇ1;
3535 };
3536 "},
3537 Mode::Normal,
3538 );
3539 cx.simulate_keystrokes("v a f");
3540 cx.assert_state(
3541 indoc! {"
3542 «const foo = () => {
3543 return 1;
3544 };ˇ»
3545 "},
3546 Mode::VisualLine,
3547 );
3548
3549 cx.set_state(
3550 indoc! {"
3551 arr.map(() => {
3552 return ˇ1;
3553 });
3554 "},
3555 Mode::Normal,
3556 );
3557 cx.simulate_keystrokes("v a f");
3558 cx.assert_state(
3559 indoc! {"
3560 arr.map(«() => {
3561 return 1;
3562 }ˇ»);
3563 "},
3564 Mode::VisualLine,
3565 );
3566
3567 cx.set_state(
3568 indoc! {"
3569 const foo = () => {
3570 return ˇ1;
3571 };
3572 "},
3573 Mode::Normal,
3574 );
3575 cx.simulate_keystrokes("v i f");
3576 cx.assert_state(
3577 indoc! {"
3578 const foo = () => {
3579 «return 1;ˇ»
3580 };
3581 "},
3582 Mode::Visual,
3583 );
3584
3585 cx.set_state(
3586 indoc! {"
3587 (() => {
3588 console.log(ˇ1);
3589 })();
3590 "},
3591 Mode::Normal,
3592 );
3593 cx.simulate_keystrokes("v a f");
3594 cx.assert_state(
3595 indoc! {"
3596 («() => {
3597 console.log(1);
3598 }ˇ»)();
3599 "},
3600 Mode::VisualLine,
3601 );
3602
3603 cx.set_state(
3604 indoc! {"
3605 const foo = () => {
3606 return ˇ1;
3607 };
3608 export { foo };
3609 "},
3610 Mode::Normal,
3611 );
3612 cx.simulate_keystrokes("v a f");
3613 cx.assert_state(
3614 indoc! {"
3615 «const foo = () => {
3616 return 1;
3617 };ˇ»
3618 export { foo };
3619 "},
3620 Mode::VisualLine,
3621 );
3622
3623 cx.set_state(
3624 indoc! {"
3625 let bar = () => {
3626 return ˇ2;
3627 };
3628 "},
3629 Mode::Normal,
3630 );
3631 cx.simulate_keystrokes("v a f");
3632 cx.assert_state(
3633 indoc! {"
3634 «let bar = () => {
3635 return 2;
3636 };ˇ»
3637 "},
3638 Mode::VisualLine,
3639 );
3640
3641 cx.set_state(
3642 indoc! {"
3643 var baz = () => {
3644 return ˇ3;
3645 };
3646 "},
3647 Mode::Normal,
3648 );
3649 cx.simulate_keystrokes("v a f");
3650 cx.assert_state(
3651 indoc! {"
3652 «var baz = () => {
3653 return 3;
3654 };ˇ»
3655 "},
3656 Mode::VisualLine,
3657 );
3658
3659 cx.set_state(
3660 indoc! {"
3661 const add = (a, b) => a + ˇb;
3662 "},
3663 Mode::Normal,
3664 );
3665 cx.simulate_keystrokes("v a f");
3666 cx.assert_state(
3667 indoc! {"
3668 «const add = (a, b) => a + b;ˇ»
3669 "},
3670 Mode::VisualLine,
3671 );
3672
3673 cx.set_state(
3674 indoc! {"
3675 const add = ˇ(a, b) => a + b;
3676 "},
3677 Mode::Normal,
3678 );
3679 cx.simulate_keystrokes("v a f");
3680 cx.assert_state(
3681 indoc! {"
3682 «const add = (a, b) => a + b;ˇ»
3683 "},
3684 Mode::VisualLine,
3685 );
3686
3687 cx.set_state(
3688 indoc! {"
3689 const add = (a, b) => a + bˇ;
3690 "},
3691 Mode::Normal,
3692 );
3693 cx.simulate_keystrokes("v a f");
3694 cx.assert_state(
3695 indoc! {"
3696 «const add = (a, b) => a + b;ˇ»
3697 "},
3698 Mode::VisualLine,
3699 );
3700
3701 cx.set_state(
3702 indoc! {"
3703 const add = (a, b) =ˇ> a + b;
3704 "},
3705 Mode::Normal,
3706 );
3707 cx.simulate_keystrokes("v a f");
3708 cx.assert_state(
3709 indoc! {"
3710 «const add = (a, b) => a + b;ˇ»
3711 "},
3712 Mode::VisualLine,
3713 );
3714 }
3715
3716 #[gpui::test]
3717 async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
3718 let mut cx = VimTestContext::new_tsx(cx).await;
3719
3720 cx.set_state(
3721 indoc! {r#"
3722 export const MyComponent = () => {
3723 return (
3724 <div>
3725 <div onClick={() => {
3726 alert("Hello world!");
3727 console.log(ˇ"clicked");
3728 }}>Hello world!</div>
3729 </div>
3730 );
3731 };
3732 "#},
3733 Mode::Normal,
3734 );
3735 cx.simulate_keystrokes("v a f");
3736 cx.assert_state(
3737 indoc! {r#"
3738 export const MyComponent = () => {
3739 return (
3740 <div>
3741 <div onClick={«() => {
3742 alert("Hello world!");
3743 console.log("clicked");
3744 }ˇ»}>Hello world!</div>
3745 </div>
3746 );
3747 };
3748 "#},
3749 Mode::VisualLine,
3750 );
3751
3752 cx.set_state(
3753 indoc! {r#"
3754 export const MyComponent = () => {
3755 return (
3756 <div>
3757 <div onClick={() => console.log("clickˇed")}>Hello world!</div>
3758 </div>
3759 );
3760 };
3761 "#},
3762 Mode::Normal,
3763 );
3764 cx.simulate_keystrokes("v a f");
3765 cx.assert_state(
3766 indoc! {r#"
3767 export const MyComponent = () => {
3768 return (
3769 <div>
3770 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3771 </div>
3772 );
3773 };
3774 "#},
3775 Mode::VisualLine,
3776 );
3777
3778 cx.set_state(
3779 indoc! {r#"
3780 export const MyComponent = () => {
3781 return (
3782 <div>
3783 <div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
3784 </div>
3785 );
3786 };
3787 "#},
3788 Mode::Normal,
3789 );
3790 cx.simulate_keystrokes("v a f");
3791 cx.assert_state(
3792 indoc! {r#"
3793 export const MyComponent = () => {
3794 return (
3795 <div>
3796 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3797 </div>
3798 );
3799 };
3800 "#},
3801 Mode::VisualLine,
3802 );
3803
3804 cx.set_state(
3805 indoc! {r#"
3806 export const MyComponent = () => {
3807 return (
3808 <div>
3809 <div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
3810 </div>
3811 );
3812 };
3813 "#},
3814 Mode::Normal,
3815 );
3816 cx.simulate_keystrokes("v a f");
3817 cx.assert_state(
3818 indoc! {r#"
3819 export const MyComponent = () => {
3820 return (
3821 <div>
3822 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3823 </div>
3824 );
3825 };
3826 "#},
3827 Mode::VisualLine,
3828 );
3829
3830 cx.set_state(
3831 indoc! {r#"
3832 export const MyComponent = () => {
3833 return (
3834 <div>
3835 <div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
3836 </div>
3837 );
3838 };
3839 "#},
3840 Mode::Normal,
3841 );
3842 cx.simulate_keystrokes("v a f");
3843 cx.assert_state(
3844 indoc! {r#"
3845 export const MyComponent = () => {
3846 return (
3847 <div>
3848 <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3849 </div>
3850 );
3851 };
3852 "#},
3853 Mode::VisualLine,
3854 );
3855
3856 cx.set_state(
3857 indoc! {r#"
3858 export const MyComponent = () => {
3859 return (
3860 <div>
3861 <div onClick={() => {
3862 console.log("cliˇcked");
3863 }}>Hello world!</div>
3864 </div>
3865 );
3866 };
3867 "#},
3868 Mode::Normal,
3869 );
3870 cx.simulate_keystrokes("v a f");
3871 cx.assert_state(
3872 indoc! {r#"
3873 export const MyComponent = () => {
3874 return (
3875 <div>
3876 <div onClick={«() => {
3877 console.log("clicked");
3878 }ˇ»}>Hello world!</div>
3879 </div>
3880 );
3881 };
3882 "#},
3883 Mode::VisualLine,
3884 );
3885
3886 cx.set_state(
3887 indoc! {r#"
3888 export const MyComponent = () => {
3889 return (
3890 <div>
3891 <div onClick={() => fˇoo()}>Hello world!</div>
3892 </div>
3893 );
3894 };
3895 "#},
3896 Mode::Normal,
3897 );
3898 cx.simulate_keystrokes("v a f");
3899 cx.assert_state(
3900 indoc! {r#"
3901 export const MyComponent = () => {
3902 return (
3903 <div>
3904 <div onClick={«() => foo()ˇ»}>Hello world!</div>
3905 </div>
3906 );
3907 };
3908 "#},
3909 Mode::VisualLine,
3910 );
3911 }
3912}