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