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