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