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 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2386 cx.simulate_keystrokes("c a a");
2387 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2388
2389 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2390 cx.simulate_keystrokes("c i a");
2391 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2392
2393 cx.set_state(
2394 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2395 Mode::Normal,
2396 );
2397 cx.simulate_keystrokes("c a a");
2398 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2399
2400 // Cursor immediately before / after brackets
2401 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2402 cx.simulate_keystrokes("v i a");
2403 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2404
2405 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2406 cx.simulate_keystrokes("v i a");
2407 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2408 }
2409
2410 #[gpui::test]
2411 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2412 let mut cx = VimTestContext::new(cx, true).await;
2413
2414 // Base use case
2415 cx.set_state(
2416 indoc! {"
2417 fn boop() {
2418 // Comment
2419 baz();ˇ
2420
2421 loop {
2422 bar(1);
2423 bar(2);
2424 }
2425
2426 result
2427 }
2428 "},
2429 Mode::Normal,
2430 );
2431 cx.simulate_keystrokes("v i i");
2432 cx.assert_state(
2433 indoc! {"
2434 fn boop() {
2435 « // Comment
2436 baz();
2437
2438 loop {
2439 bar(1);
2440 bar(2);
2441 }
2442
2443 resultˇ»
2444 }
2445 "},
2446 Mode::Visual,
2447 );
2448
2449 // Around indent (include line above)
2450 cx.set_state(
2451 indoc! {"
2452 const ABOVE: str = true;
2453 fn boop() {
2454
2455 hello();
2456 worˇld()
2457 }
2458 "},
2459 Mode::Normal,
2460 );
2461 cx.simulate_keystrokes("v a i");
2462 cx.assert_state(
2463 indoc! {"
2464 const ABOVE: str = true;
2465 «fn boop() {
2466
2467 hello();
2468 world()ˇ»
2469 }
2470 "},
2471 Mode::Visual,
2472 );
2473
2474 // Around indent (include line above & below)
2475 cx.set_state(
2476 indoc! {"
2477 const ABOVE: str = true;
2478 fn boop() {
2479 hellˇo();
2480 world()
2481
2482 }
2483 const BELOW: str = true;
2484 "},
2485 Mode::Normal,
2486 );
2487 cx.simulate_keystrokes("c a shift-i");
2488 cx.assert_state(
2489 indoc! {"
2490 const ABOVE: str = true;
2491 ˇ
2492 const BELOW: str = true;
2493 "},
2494 Mode::Insert,
2495 );
2496 }
2497
2498 #[gpui::test]
2499 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2500 let mut cx = NeovimBackedTestContext::new(cx).await;
2501
2502 for (start, end) in SURROUNDING_OBJECTS {
2503 let marked_string = SURROUNDING_MARKER_STRING
2504 .replace('`', &start.to_string())
2505 .replace('\'', &end.to_string());
2506
2507 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2508 .await
2509 .assert_matches();
2510 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2511 .await
2512 .assert_matches();
2513 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2514 .await
2515 .assert_matches();
2516 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2517 .await
2518 .assert_matches();
2519 }
2520 }
2521
2522 #[gpui::test]
2523 async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2524 let mut cx = VimTestContext::new(cx, true).await;
2525 cx.update(|_, cx| {
2526 cx.bind_keys([KeyBinding::new(
2527 "q",
2528 AnyQuotes,
2529 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2530 )]);
2531 });
2532
2533 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2534 // the false string in the middle should be considered
2535 (
2536 "c i q",
2537 "'first' false ˇstring 'second'",
2538 "'first'ˇ'second'",
2539 Mode::Insert,
2540 ),
2541 // Single quotes
2542 (
2543 "c i q",
2544 "Thisˇ is a 'quote' example.",
2545 "This is a 'ˇ' example.",
2546 Mode::Insert,
2547 ),
2548 (
2549 "c a q",
2550 "Thisˇ is a 'quote' example.",
2551 "This is a ˇexample.",
2552 Mode::Insert,
2553 ),
2554 (
2555 "c i q",
2556 "This is a \"simple 'qˇuote'\" example.",
2557 "This is a \"simple 'ˇ'\" example.",
2558 Mode::Insert,
2559 ),
2560 (
2561 "c a q",
2562 "This is a \"simple 'qˇuote'\" example.",
2563 "This is a \"simpleˇ\" example.",
2564 Mode::Insert,
2565 ),
2566 (
2567 "c i q",
2568 "This is a 'qˇuote' example.",
2569 "This is a 'ˇ' example.",
2570 Mode::Insert,
2571 ),
2572 (
2573 "c a q",
2574 "This is a 'qˇuote' example.",
2575 "This is a ˇexample.",
2576 Mode::Insert,
2577 ),
2578 (
2579 "d i q",
2580 "This is a 'qˇuote' example.",
2581 "This is a 'ˇ' example.",
2582 Mode::Normal,
2583 ),
2584 (
2585 "d a q",
2586 "This is a 'qˇuote' example.",
2587 "This is a ˇexample.",
2588 Mode::Normal,
2589 ),
2590 // Double quotes
2591 (
2592 "c i q",
2593 "This is a \"qˇuote\" example.",
2594 "This is a \"ˇ\" example.",
2595 Mode::Insert,
2596 ),
2597 (
2598 "c a q",
2599 "This is a \"qˇuote\" example.",
2600 "This is a ˇexample.",
2601 Mode::Insert,
2602 ),
2603 (
2604 "d i q",
2605 "This is a \"qˇuote\" example.",
2606 "This is a \"ˇ\" example.",
2607 Mode::Normal,
2608 ),
2609 (
2610 "d a q",
2611 "This is a \"qˇuote\" example.",
2612 "This is a ˇexample.",
2613 Mode::Normal,
2614 ),
2615 // Back quotes
2616 (
2617 "c i q",
2618 "This is a `qˇuote` example.",
2619 "This is a `ˇ` example.",
2620 Mode::Insert,
2621 ),
2622 (
2623 "c a q",
2624 "This is a `qˇuote` example.",
2625 "This is a ˇexample.",
2626 Mode::Insert,
2627 ),
2628 (
2629 "d i q",
2630 "This is a `qˇuote` example.",
2631 "This is a `ˇ` example.",
2632 Mode::Normal,
2633 ),
2634 (
2635 "d a q",
2636 "This is a `qˇuote` example.",
2637 "This is a ˇexample.",
2638 Mode::Normal,
2639 ),
2640 ];
2641
2642 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2643 cx.set_state(initial_state, Mode::Normal);
2644
2645 cx.simulate_keystrokes(keystrokes);
2646
2647 cx.assert_state(expected_state, *expected_mode);
2648 }
2649
2650 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2651 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2652 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2653 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2654 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2655 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2656 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2657 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2658 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2659 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2660 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2661 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2662 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2663 ];
2664
2665 for (keystrokes, initial_state, mode) in INVALID_CASES {
2666 cx.set_state(initial_state, Mode::Normal);
2667
2668 cx.simulate_keystrokes(keystrokes);
2669
2670 cx.assert_state(initial_state, *mode);
2671 }
2672 }
2673
2674 #[gpui::test]
2675 async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2676 let mut cx = VimTestContext::new_typescript(cx).await;
2677
2678 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2679 // Special cases from mini.ai plugin
2680 // the false string in the middle should not be considered
2681 (
2682 "c i q",
2683 "'first' false ˇstring 'second'",
2684 "'first' false string 'ˇ'",
2685 Mode::Insert,
2686 ),
2687 // Multiline support :)! Same behavior as mini.ai plugin
2688 (
2689 "c i q",
2690 indoc! {"
2691 `
2692 first
2693 middle ˇstring
2694 second
2695 `
2696 "},
2697 indoc! {"
2698 `ˇ`
2699 "},
2700 Mode::Insert,
2701 ),
2702 // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2703 // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2704 // Bug reference: https://github.com/zed-industries/zed/issues/23889
2705 ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2706 // Single quotes
2707 (
2708 "c i q",
2709 "Thisˇ is a 'quote' example.",
2710 "This is a 'ˇ' example.",
2711 Mode::Insert,
2712 ),
2713 (
2714 "c a q",
2715 "Thisˇ is a 'quote' example.",
2716 "This is a ˇ example.", // same mini.ai plugin behavior
2717 Mode::Insert,
2718 ),
2719 (
2720 "c i q",
2721 "This is a \"simple 'qˇuote'\" example.",
2722 "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2723 Mode::Insert,
2724 ),
2725 (
2726 "c a q",
2727 "This is a \"simple 'qˇuote'\" example.",
2728 "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2729 Mode::Insert,
2730 ),
2731 (
2732 "c i q",
2733 "This is a 'qˇuote' example.",
2734 "This is a 'ˇ' example.",
2735 Mode::Insert,
2736 ),
2737 (
2738 "c a q",
2739 "This is a 'qˇuote' example.",
2740 "This is a ˇ example.", // same mini.ai plugin behavior
2741 Mode::Insert,
2742 ),
2743 (
2744 "d i q",
2745 "This is a 'qˇuote' example.",
2746 "This is a 'ˇ' example.",
2747 Mode::Normal,
2748 ),
2749 (
2750 "d a q",
2751 "This is a 'qˇuote' example.",
2752 "This is a ˇ example.", // same mini.ai plugin behavior
2753 Mode::Normal,
2754 ),
2755 // Double quotes
2756 (
2757 "c i q",
2758 "This is a \"qˇuote\" example.",
2759 "This is a \"ˇ\" example.",
2760 Mode::Insert,
2761 ),
2762 (
2763 "c a q",
2764 "This is a \"qˇuote\" example.",
2765 "This is a ˇ example.", // same mini.ai plugin behavior
2766 Mode::Insert,
2767 ),
2768 (
2769 "d i q",
2770 "This is a \"qˇuote\" example.",
2771 "This is a \"ˇ\" example.",
2772 Mode::Normal,
2773 ),
2774 (
2775 "d a q",
2776 "This is a \"qˇuote\" example.",
2777 "This is a ˇ example.", // same mini.ai plugin behavior
2778 Mode::Normal,
2779 ),
2780 // Back quotes
2781 (
2782 "c i q",
2783 "This is a `qˇuote` example.",
2784 "This is a `ˇ` example.",
2785 Mode::Insert,
2786 ),
2787 (
2788 "c a q",
2789 "This is a `qˇuote` example.",
2790 "This is a ˇ example.", // same mini.ai plugin behavior
2791 Mode::Insert,
2792 ),
2793 (
2794 "d i q",
2795 "This is a `qˇuote` example.",
2796 "This is a `ˇ` example.",
2797 Mode::Normal,
2798 ),
2799 (
2800 "d a q",
2801 "This is a `qˇuote` example.",
2802 "This is a ˇ example.", // same mini.ai plugin behavior
2803 Mode::Normal,
2804 ),
2805 ];
2806
2807 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2808 cx.set_state(initial_state, Mode::Normal);
2809
2810 cx.simulate_keystrokes(keystrokes);
2811
2812 cx.assert_state(expected_state, *expected_mode);
2813 }
2814
2815 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2816 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2817 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2818 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2819 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2820 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2821 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2822 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2823 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2824 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2825 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2826 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2827 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2828 ];
2829
2830 for (keystrokes, initial_state, mode) in INVALID_CASES {
2831 cx.set_state(initial_state, Mode::Normal);
2832
2833 cx.simulate_keystrokes(keystrokes);
2834
2835 cx.assert_state(initial_state, *mode);
2836 }
2837 }
2838
2839 #[gpui::test]
2840 async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2841 let mut cx = VimTestContext::new(cx, true).await;
2842 cx.update(|_, cx| {
2843 cx.bind_keys([KeyBinding::new(
2844 "b",
2845 AnyBrackets,
2846 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2847 )]);
2848 });
2849
2850 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2851 (
2852 "c i b",
2853 indoc! {"
2854 {
2855 {
2856 ˇprint('hello')
2857 }
2858 }
2859 "},
2860 indoc! {"
2861 {
2862 {
2863 ˇ
2864 }
2865 }
2866 "},
2867 Mode::Insert,
2868 ),
2869 // Bracket (Parentheses)
2870 (
2871 "c i b",
2872 "Thisˇ is a (simple [quote]) example.",
2873 "This is a (ˇ) example.",
2874 Mode::Insert,
2875 ),
2876 (
2877 "c i b",
2878 "This is a [simple (qˇuote)] example.",
2879 "This is a [simple (ˇ)] example.",
2880 Mode::Insert,
2881 ),
2882 (
2883 "c a b",
2884 "This is a [simple (qˇuote)] example.",
2885 "This is a [simple ˇ] example.",
2886 Mode::Insert,
2887 ),
2888 (
2889 "c a b",
2890 "Thisˇ is a (simple [quote]) example.",
2891 "This is a ˇ example.",
2892 Mode::Insert,
2893 ),
2894 (
2895 "c i b",
2896 "This is a (qˇuote) example.",
2897 "This is a (ˇ) example.",
2898 Mode::Insert,
2899 ),
2900 (
2901 "c a b",
2902 "This is a (qˇuote) example.",
2903 "This is a ˇ example.",
2904 Mode::Insert,
2905 ),
2906 (
2907 "d i b",
2908 "This is a (qˇuote) example.",
2909 "This is a (ˇ) example.",
2910 Mode::Normal,
2911 ),
2912 (
2913 "d a b",
2914 "This is a (qˇuote) example.",
2915 "This is a ˇ example.",
2916 Mode::Normal,
2917 ),
2918 // Square brackets
2919 (
2920 "c i b",
2921 "This is a [qˇuote] example.",
2922 "This is a [ˇ] example.",
2923 Mode::Insert,
2924 ),
2925 (
2926 "c a b",
2927 "This is a [qˇuote] example.",
2928 "This is a ˇ example.",
2929 Mode::Insert,
2930 ),
2931 (
2932 "d i b",
2933 "This is a [qˇuote] example.",
2934 "This is a [ˇ] example.",
2935 Mode::Normal,
2936 ),
2937 (
2938 "d a b",
2939 "This is a [qˇuote] example.",
2940 "This is a ˇ example.",
2941 Mode::Normal,
2942 ),
2943 // Curly brackets
2944 (
2945 "c i b",
2946 "This is a {qˇuote} example.",
2947 "This is a {ˇ} example.",
2948 Mode::Insert,
2949 ),
2950 (
2951 "c a b",
2952 "This is a {qˇuote} example.",
2953 "This is a ˇ example.",
2954 Mode::Insert,
2955 ),
2956 (
2957 "d i b",
2958 "This is a {qˇuote} example.",
2959 "This is a {ˇ} example.",
2960 Mode::Normal,
2961 ),
2962 (
2963 "d a b",
2964 "This is a {qˇuote} example.",
2965 "This is a ˇ example.",
2966 Mode::Normal,
2967 ),
2968 ];
2969
2970 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2971 cx.set_state(initial_state, Mode::Normal);
2972
2973 cx.simulate_keystrokes(keystrokes);
2974
2975 cx.assert_state(expected_state, *expected_mode);
2976 }
2977
2978 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2979 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2980 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2981 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2982 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2983 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2984 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2985 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2986 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2987 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2988 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2989 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2990 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2991 ];
2992
2993 for (keystrokes, initial_state, mode) in INVALID_CASES {
2994 cx.set_state(initial_state, Mode::Normal);
2995
2996 cx.simulate_keystrokes(keystrokes);
2997
2998 cx.assert_state(initial_state, *mode);
2999 }
3000 }
3001
3002 #[gpui::test]
3003 async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
3004 let mut cx = VimTestContext::new(cx, true).await;
3005 cx.update(|_, cx| {
3006 cx.bind_keys([KeyBinding::new(
3007 "b",
3008 MiniBrackets,
3009 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
3010 )]);
3011 });
3012
3013 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
3014 // Special cases from mini.ai plugin
3015 // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
3016 // Same behavior as mini.ai plugin
3017 (
3018 "c i b",
3019 indoc! {"
3020 {
3021 {
3022 ˇprint('hello')
3023 }
3024 }
3025 "},
3026 indoc! {"
3027 {
3028 {
3029 print(ˇ)
3030 }
3031 }
3032 "},
3033 Mode::Insert,
3034 ),
3035 // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3036 // Same behavior as mini.ai plugin
3037 (
3038 "c i b",
3039 indoc! {"
3040 {
3041 {
3042 ˇ
3043 print('hello')
3044 }
3045 }
3046 "},
3047 indoc! {"
3048 {
3049 {ˇ}
3050 }
3051 "},
3052 Mode::Insert,
3053 ),
3054 // If you are in the open bracket then it has higher priority
3055 (
3056 "c i b",
3057 indoc! {"
3058 «{ˇ»
3059 {
3060 print('hello')
3061 }
3062 }
3063 "},
3064 indoc! {"
3065 {ˇ}
3066 "},
3067 Mode::Insert,
3068 ),
3069 // If you are in the close bracket then it has higher priority
3070 (
3071 "c i b",
3072 indoc! {"
3073 {
3074 {
3075 print('hello')
3076 }
3077 «}ˇ»
3078 "},
3079 indoc! {"
3080 {ˇ}
3081 "},
3082 Mode::Insert,
3083 ),
3084 // Bracket (Parentheses)
3085 (
3086 "c i b",
3087 "Thisˇ is a (simple [quote]) example.",
3088 "This is a (ˇ) example.",
3089 Mode::Insert,
3090 ),
3091 (
3092 "c i b",
3093 "This is a [simple (qˇuote)] example.",
3094 "This is a [simple (ˇ)] example.",
3095 Mode::Insert,
3096 ),
3097 (
3098 "c a b",
3099 "This is a [simple (qˇuote)] example.",
3100 "This is a [simple ˇ] example.",
3101 Mode::Insert,
3102 ),
3103 (
3104 "c a b",
3105 "Thisˇ is a (simple [quote]) example.",
3106 "This is a ˇ example.",
3107 Mode::Insert,
3108 ),
3109 (
3110 "c i b",
3111 "This is a (qˇuote) example.",
3112 "This is a (ˇ) example.",
3113 Mode::Insert,
3114 ),
3115 (
3116 "c a b",
3117 "This is a (qˇuote) example.",
3118 "This is a ˇ example.",
3119 Mode::Insert,
3120 ),
3121 (
3122 "d i b",
3123 "This is a (qˇuote) example.",
3124 "This is a (ˇ) example.",
3125 Mode::Normal,
3126 ),
3127 (
3128 "d a b",
3129 "This is a (qˇuote) example.",
3130 "This is a ˇ example.",
3131 Mode::Normal,
3132 ),
3133 // Square brackets
3134 (
3135 "c i b",
3136 "This is a [qˇuote] example.",
3137 "This is a [ˇ] example.",
3138 Mode::Insert,
3139 ),
3140 (
3141 "c a b",
3142 "This is a [qˇuote] example.",
3143 "This is a ˇ example.",
3144 Mode::Insert,
3145 ),
3146 (
3147 "d i b",
3148 "This is a [qˇuote] example.",
3149 "This is a [ˇ] example.",
3150 Mode::Normal,
3151 ),
3152 (
3153 "d a b",
3154 "This is a [qˇuote] example.",
3155 "This is a ˇ example.",
3156 Mode::Normal,
3157 ),
3158 // Curly brackets
3159 (
3160 "c i b",
3161 "This is a {qˇuote} example.",
3162 "This is a {ˇ} example.",
3163 Mode::Insert,
3164 ),
3165 (
3166 "c a b",
3167 "This is a {qˇuote} example.",
3168 "This is a ˇ example.",
3169 Mode::Insert,
3170 ),
3171 (
3172 "d i b",
3173 "This is a {qˇuote} example.",
3174 "This is a {ˇ} example.",
3175 Mode::Normal,
3176 ),
3177 (
3178 "d a b",
3179 "This is a {qˇuote} example.",
3180 "This is a ˇ example.",
3181 Mode::Normal,
3182 ),
3183 ];
3184
3185 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3186 cx.set_state(initial_state, Mode::Normal);
3187
3188 cx.simulate_keystrokes(keystrokes);
3189
3190 cx.assert_state(expected_state, *expected_mode);
3191 }
3192
3193 const INVALID_CASES: &[(&str, &str, Mode)] = &[
3194 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3195 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3196 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3197 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3198 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3199 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3200 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3201 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3202 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3203 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3204 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3205 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3206 ];
3207
3208 for (keystrokes, initial_state, mode) in INVALID_CASES {
3209 cx.set_state(initial_state, Mode::Normal);
3210
3211 cx.simulate_keystrokes(keystrokes);
3212
3213 cx.assert_state(initial_state, *mode);
3214 }
3215 }
3216
3217 #[gpui::test]
3218 async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
3219 // Initialize test context with the TypeScript language loaded, so we
3220 // can actually get brackets definition.
3221 let mut cx = VimTestContext::new(cx, true).await;
3222
3223 // Update `b` to `MiniBrackets` so we can later use it when simulating
3224 // keystrokes.
3225 cx.update(|_, cx| {
3226 cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
3227 });
3228
3229 let (editor, cx) = cx.add_window_view(|window, cx| {
3230 let multi_buffer = MultiBuffer::build_multi(
3231 [
3232 ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
3233 ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
3234 ],
3235 cx,
3236 );
3237
3238 // In order for the brackets to actually be found, we need to update
3239 // the language used for the second buffer. This is something that
3240 // is handled automatically when simply using `VimTestContext::new`
3241 // but, since this is being set manually, the language isn't
3242 // automatically set.
3243 let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
3244 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
3245 if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
3246 buffer.update(cx, |buffer, cx| {
3247 buffer.set_language(Some(language::rust_lang()), cx);
3248 })
3249 };
3250
3251 editor
3252 });
3253
3254 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3255
3256 cx.assert_excerpts_with_selections(indoc! {"
3257 [EXCERPT]
3258 ˇ111
3259 222
3260 [EXCERPT]
3261 111
3262 a {bracket} example
3263 "
3264 });
3265
3266 cx.simulate_keystrokes("j j j j f r");
3267 cx.assert_excerpts_with_selections(indoc! {"
3268 [EXCERPT]
3269 111
3270 222
3271 [EXCERPT]
3272 111
3273 a {bˇracket} example
3274 "
3275 });
3276
3277 cx.simulate_keystrokes("d i b");
3278 cx.assert_excerpts_with_selections(indoc! {"
3279 [EXCERPT]
3280 111
3281 222
3282 [EXCERPT]
3283 111
3284 a {ˇ} example
3285 "
3286 });
3287 }
3288
3289 #[gpui::test]
3290 async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3291 let mut cx = NeovimBackedTestContext::new(cx).await;
3292 cx.set_shared_state("(trailingˇ whitespace )")
3293 .await;
3294 cx.simulate_shared_keystrokes("v i b").await;
3295 cx.shared_state().await.assert_matches();
3296 cx.simulate_shared_keystrokes("escape y i b").await;
3297 cx.shared_clipboard()
3298 .await
3299 .assert_eq("trailing whitespace ");
3300 }
3301
3302 #[gpui::test]
3303 async fn test_tags(cx: &mut gpui::TestAppContext) {
3304 let mut cx = VimTestContext::new_html(cx).await;
3305
3306 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3307 cx.simulate_keystrokes("v i t");
3308 cx.assert_state(
3309 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3310 Mode::Visual,
3311 );
3312 cx.simulate_keystrokes("a t");
3313 cx.assert_state(
3314 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3315 Mode::Visual,
3316 );
3317 cx.simulate_keystrokes("a t");
3318 cx.assert_state(
3319 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3320 Mode::Visual,
3321 );
3322
3323 // The cursor is before the tag
3324 cx.set_state(
3325 "<html><head></head><body> ˇ <b>hi!</b></body>",
3326 Mode::Normal,
3327 );
3328 cx.simulate_keystrokes("v i t");
3329 cx.assert_state(
3330 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
3331 Mode::Visual,
3332 );
3333 cx.simulate_keystrokes("a t");
3334 cx.assert_state(
3335 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
3336 Mode::Visual,
3337 );
3338
3339 // The cursor is in the open tag
3340 cx.set_state(
3341 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3342 Mode::Normal,
3343 );
3344 cx.simulate_keystrokes("v a t");
3345 cx.assert_state(
3346 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3347 Mode::Visual,
3348 );
3349 cx.simulate_keystrokes("i t");
3350 cx.assert_state(
3351 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3352 Mode::Visual,
3353 );
3354
3355 // current selection length greater than 1
3356 cx.set_state(
3357 "<html><head></head><body><«b>hi!ˇ»</b></body>",
3358 Mode::Visual,
3359 );
3360 cx.simulate_keystrokes("i t");
3361 cx.assert_state(
3362 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3363 Mode::Visual,
3364 );
3365 cx.simulate_keystrokes("a t");
3366 cx.assert_state(
3367 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3368 Mode::Visual,
3369 );
3370
3371 cx.set_state(
3372 "<html><head></head><body><«b>hi!</ˇ»b></body>",
3373 Mode::Visual,
3374 );
3375 cx.simulate_keystrokes("a t");
3376 cx.assert_state(
3377 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3378 Mode::Visual,
3379 );
3380 }
3381 #[gpui::test]
3382 async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3383 let mut cx = NeovimBackedTestContext::new(cx).await;
3384
3385 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3386 .await;
3387 cx.simulate_shared_keystrokes("v a w").await;
3388 cx.shared_state()
3389 .await
3390 .assert_eq(" «const ˇ»f = (x: unknown) => {");
3391
3392 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3393 .await;
3394 cx.simulate_shared_keystrokes("y a w").await;
3395 cx.shared_clipboard().await.assert_eq("const ");
3396
3397 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3398 .await;
3399 cx.simulate_shared_keystrokes("d a w").await;
3400 cx.shared_state()
3401 .await
3402 .assert_eq(" ˇf = (x: unknown) => {");
3403 cx.shared_clipboard().await.assert_eq("const ");
3404
3405 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3406 .await;
3407 cx.simulate_shared_keystrokes("c a w").await;
3408 cx.shared_state()
3409 .await
3410 .assert_eq(" ˇf = (x: unknown) => {");
3411 cx.shared_clipboard().await.assert_eq("const ");
3412 }
3413}