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