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