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