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