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