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