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