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_row = paragraph_start.row();
1448 if paragraph_start_row.0 != 0 {
1449 let previous_paragraph_last_line_start =
1450 Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map);
1451 paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1452 }
1453 } else {
1454 let mut start_row = paragraph_end_row.0 + 1;
1455 if i > 0 {
1456 start_row += 1;
1457 }
1458 let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1459 paragraph_end = end_of_paragraph(map, next_paragraph_start);
1460 }
1461 }
1462 }
1463
1464 let range = paragraph_start..paragraph_end;
1465 Some(range)
1466}
1467
1468/// Returns a position of the start of the current paragraph, where a paragraph
1469/// is defined as a run of non-blank lines or a run of blank lines.
1470pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1471 let point = display_point.to_point(map);
1472 if point.row == 0 {
1473 return DisplayPoint::zero();
1474 }
1475
1476 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1477
1478 for row in (0..point.row).rev() {
1479 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1480 if blank != is_current_line_blank {
1481 return Point::new(row + 1, 0).to_display_point(map);
1482 }
1483 }
1484
1485 DisplayPoint::zero()
1486}
1487
1488/// Returns a position of the end of the current paragraph, where a paragraph
1489/// is defined as a run of non-blank lines or a run of blank lines.
1490/// The trailing newline is excluded from the paragraph.
1491pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1492 let point = display_point.to_point(map);
1493 if point.row == map.buffer_snapshot.max_row().0 {
1494 return map.max_point();
1495 }
1496
1497 let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1498
1499 for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
1500 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1501 if blank != is_current_line_blank {
1502 let previous_row = row - 1;
1503 return Point::new(
1504 previous_row,
1505 map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
1506 )
1507 .to_display_point(map);
1508 }
1509 }
1510
1511 map.max_point()
1512}
1513
1514fn surrounding_markers(
1515 map: &DisplaySnapshot,
1516 relative_to: DisplayPoint,
1517 around: bool,
1518 search_across_lines: bool,
1519 open_marker: char,
1520 close_marker: char,
1521) -> Option<Range<DisplayPoint>> {
1522 let point = relative_to.to_offset(map, Bias::Left);
1523
1524 let mut matched_closes = 0;
1525 let mut opening = None;
1526
1527 let mut before_ch = match movement::chars_before(map, point).next() {
1528 Some((ch, _)) => ch,
1529 _ => '\0',
1530 };
1531 if let Some((ch, range)) = movement::chars_after(map, point).next() {
1532 if ch == open_marker && before_ch != '\\' {
1533 if open_marker == close_marker {
1534 let mut total = 0;
1535 for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
1536 {
1537 if ch == '\n' {
1538 break;
1539 }
1540 if ch == open_marker && before_ch != '\\' {
1541 total += 1;
1542 }
1543 }
1544 if total % 2 == 0 {
1545 opening = Some(range)
1546 }
1547 } else {
1548 opening = Some(range)
1549 }
1550 }
1551 }
1552
1553 if opening.is_none() {
1554 let mut chars_before = movement::chars_before(map, point).peekable();
1555 while let Some((ch, range)) = chars_before.next() {
1556 if ch == '\n' && !search_across_lines {
1557 break;
1558 }
1559
1560 if let Some((before_ch, _)) = chars_before.peek() {
1561 if *before_ch == '\\' {
1562 continue;
1563 }
1564 }
1565
1566 if ch == open_marker {
1567 if matched_closes == 0 {
1568 opening = Some(range);
1569 break;
1570 }
1571 matched_closes -= 1;
1572 } else if ch == close_marker {
1573 matched_closes += 1
1574 }
1575 }
1576 }
1577 if opening.is_none() {
1578 for (ch, range) in movement::chars_after(map, point) {
1579 if before_ch != '\\' {
1580 if ch == open_marker {
1581 opening = Some(range);
1582 break;
1583 } else if ch == close_marker {
1584 break;
1585 }
1586 }
1587
1588 before_ch = ch;
1589 }
1590 }
1591
1592 let mut opening = opening?;
1593
1594 let mut matched_opens = 0;
1595 let mut closing = None;
1596 before_ch = match movement::chars_before(map, opening.end).next() {
1597 Some((ch, _)) => ch,
1598 _ => '\0',
1599 };
1600 for (ch, range) in movement::chars_after(map, opening.end) {
1601 if ch == '\n' && !search_across_lines {
1602 break;
1603 }
1604
1605 if before_ch != '\\' {
1606 if ch == close_marker {
1607 if matched_opens == 0 {
1608 closing = Some(range);
1609 break;
1610 }
1611 matched_opens -= 1;
1612 } else if ch == open_marker {
1613 matched_opens += 1;
1614 }
1615 }
1616
1617 before_ch = ch;
1618 }
1619
1620 let mut closing = closing?;
1621
1622 if around && !search_across_lines {
1623 let mut found = false;
1624
1625 for (ch, range) in movement::chars_after(map, closing.end) {
1626 if ch.is_whitespace() && ch != '\n' {
1627 found = true;
1628 closing.end = range.end;
1629 } else {
1630 break;
1631 }
1632 }
1633
1634 if !found {
1635 for (ch, range) in movement::chars_before(map, opening.start) {
1636 if ch.is_whitespace() && ch != '\n' {
1637 opening.start = range.start
1638 } else {
1639 break;
1640 }
1641 }
1642 }
1643 }
1644
1645 // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1646 if !around && open_marker != close_marker {
1647 let start_point = opening.end.to_display_point(map);
1648 let end_point = closing.start.to_display_point(map);
1649 let start_offset = start_point.to_offset(map, Bias::Left);
1650 let end_offset = end_point.to_offset(map, Bias::Left);
1651
1652 if start_point.row() != end_point.row()
1653 && map
1654 .buffer_chars_at(start_offset)
1655 .take_while(|(_, offset)| offset < &end_offset)
1656 .any(|(ch, _)| !ch.is_whitespace())
1657 {
1658 let mut first_non_ws = None;
1659 let mut last_non_ws = None;
1660 for (ch, offset) in map.buffer_chars_at(start_offset) {
1661 if !ch.is_whitespace() {
1662 first_non_ws = Some(offset);
1663 break;
1664 }
1665 }
1666 for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1667 if !ch.is_whitespace() {
1668 last_non_ws = Some(offset + ch.len_utf8());
1669 break;
1670 }
1671 }
1672 if let Some(start) = first_non_ws {
1673 opening.end = start;
1674 }
1675 if let Some(end) = last_non_ws {
1676 closing.start = end;
1677 }
1678 }
1679 }
1680
1681 let result = if around {
1682 opening.start..closing.end
1683 } else {
1684 opening.end..closing.start
1685 };
1686
1687 Some(
1688 map.clip_point(result.start.to_display_point(map), Bias::Left)
1689 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1690 )
1691}
1692
1693#[cfg(test)]
1694mod test {
1695 use gpui::KeyBinding;
1696 use indoc::indoc;
1697
1698 use crate::{
1699 object::{AnyBrackets, AnyQuotes, MiniBrackets},
1700 state::Mode,
1701 test::{NeovimBackedTestContext, VimTestContext},
1702 };
1703
1704 const WORD_LOCATIONS: &str = indoc! {"
1705 The quick ˇbrowˇnˇ•••
1706 fox ˇjuˇmpsˇ over
1707 the lazy dogˇ••
1708 ˇ
1709 ˇ
1710 ˇ
1711 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1712 ˇ••
1713 ˇ••
1714 ˇ fox-jumpˇs over
1715 the lazy dogˇ•
1716 ˇ
1717 "
1718 };
1719
1720 #[gpui::test]
1721 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1722 let mut cx = NeovimBackedTestContext::new(cx).await;
1723
1724 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1725 .await
1726 .assert_matches();
1727 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1728 .await
1729 .assert_matches();
1730 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1731 .await
1732 .assert_matches();
1733 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1734 .await
1735 .assert_matches();
1736 }
1737
1738 #[gpui::test]
1739 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1740 let mut cx = NeovimBackedTestContext::new(cx).await;
1741
1742 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1743 .await
1744 .assert_matches();
1745 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1746 .await
1747 .assert_matches();
1748 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1749 .await
1750 .assert_matches();
1751 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1752 .await
1753 .assert_matches();
1754 }
1755
1756 #[gpui::test]
1757 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1758 let mut cx = NeovimBackedTestContext::new(cx).await;
1759
1760 /*
1761 cx.set_shared_state("The quick ˇbrown\nfox").await;
1762 cx.simulate_shared_keystrokes(["v"]).await;
1763 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1764 cx.simulate_shared_keystrokes(["i", "w"]).await;
1765 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1766 */
1767 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1768 cx.simulate_shared_keystrokes("v").await;
1769 cx.shared_state()
1770 .await
1771 .assert_eq("The quick brown\n«\nˇ»fox");
1772 cx.simulate_shared_keystrokes("i w").await;
1773 cx.shared_state()
1774 .await
1775 .assert_eq("The quick brown\n«\nˇ»fox");
1776
1777 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1778 .await
1779 .assert_matches();
1780 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1781 .await
1782 .assert_matches();
1783 }
1784
1785 const PARAGRAPH_EXAMPLES: &[&str] = &[
1786 // Single line
1787 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1788 // Multiple lines without empty lines
1789 indoc! {"
1790 ˇThe quick brownˇ
1791 ˇfox jumps overˇ
1792 the lazy dog.ˇ
1793 "},
1794 // Heading blank paragraph and trailing normal paragraph
1795 indoc! {"
1796 ˇ
1797 ˇ
1798 ˇThe quick brown fox jumps
1799 ˇover the lazy dog.
1800 ˇ
1801 ˇ
1802 ˇThe quick brown fox jumpsˇ
1803 ˇover the lazy dog.ˇ
1804 "},
1805 // Inserted blank paragraph and trailing blank paragraph
1806 indoc! {"
1807 ˇThe quick brown fox jumps
1808 ˇover the lazy dog.
1809 ˇ
1810 ˇ
1811 ˇ
1812 ˇThe quick brown fox jumpsˇ
1813 ˇover the lazy dog.ˇ
1814 ˇ
1815 ˇ
1816 ˇ
1817 "},
1818 // "Blank" paragraph with whitespace characters
1819 indoc! {"
1820 ˇThe quick brown fox jumps
1821 over the lazy dog.
1822
1823 ˇ \t
1824
1825 ˇThe quick brown fox jumps
1826 over the lazy dog.ˇ
1827 ˇ
1828 ˇ \t
1829 \t \t
1830 "},
1831 // Single line "paragraphs", where selection size might be zero.
1832 indoc! {"
1833 ˇThe quick brown fox jumps over the lazy dog.
1834 ˇ
1835 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1836 ˇ
1837 "},
1838 ];
1839
1840 #[gpui::test]
1841 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1842 let mut cx = NeovimBackedTestContext::new(cx).await;
1843
1844 for paragraph_example in PARAGRAPH_EXAMPLES {
1845 cx.simulate_at_each_offset("c i p", paragraph_example)
1846 .await
1847 .assert_matches();
1848 cx.simulate_at_each_offset("c a p", paragraph_example)
1849 .await
1850 .assert_matches();
1851 }
1852 }
1853
1854 #[gpui::test]
1855 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1856 let mut cx = NeovimBackedTestContext::new(cx).await;
1857
1858 for paragraph_example in PARAGRAPH_EXAMPLES {
1859 cx.simulate_at_each_offset("d i p", paragraph_example)
1860 .await
1861 .assert_matches();
1862 cx.simulate_at_each_offset("d a p", paragraph_example)
1863 .await
1864 .assert_matches();
1865 }
1866 }
1867
1868 #[gpui::test]
1869 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1870 let mut cx = NeovimBackedTestContext::new(cx).await;
1871
1872 const EXAMPLES: &[&str] = &[
1873 indoc! {"
1874 ˇThe quick brown
1875 fox jumps over
1876 the lazy dog.
1877 "},
1878 indoc! {"
1879 ˇ
1880
1881 ˇThe quick brown fox jumps
1882 over the lazy dog.
1883 ˇ
1884
1885 ˇThe quick brown fox jumps
1886 over the lazy dog.
1887 "},
1888 indoc! {"
1889 ˇThe quick brown fox jumps over the lazy dog.
1890 ˇ
1891 ˇThe quick brown fox jumps over the lazy dog.
1892
1893 "},
1894 ];
1895
1896 for paragraph_example in EXAMPLES {
1897 cx.simulate_at_each_offset("v i p", paragraph_example)
1898 .await
1899 .assert_matches();
1900 cx.simulate_at_each_offset("v a p", paragraph_example)
1901 .await
1902 .assert_matches();
1903 }
1904 }
1905
1906 // Test string with "`" for opening surrounders and "'" for closing surrounders
1907 const SURROUNDING_MARKER_STRING: &str = indoc! {"
1908 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1909 'ˇfox juˇmps ov`ˇer
1910 the ˇlazy d'o`ˇg"};
1911
1912 const SURROUNDING_OBJECTS: &[(char, char)] = &[
1913 ('"', '"'), // Double Quote
1914 ('(', ')'), // Parentheses
1915 ];
1916
1917 #[gpui::test]
1918 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1919 let mut cx = NeovimBackedTestContext::new(cx).await;
1920
1921 for (start, end) in SURROUNDING_OBJECTS {
1922 let marked_string = SURROUNDING_MARKER_STRING
1923 .replace('`', &start.to_string())
1924 .replace('\'', &end.to_string());
1925
1926 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1927 .await
1928 .assert_matches();
1929 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1930 .await
1931 .assert_matches();
1932 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1933 .await
1934 .assert_matches();
1935 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1936 .await
1937 .assert_matches();
1938 }
1939 }
1940 #[gpui::test]
1941 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1942 let mut cx = NeovimBackedTestContext::new(cx).await;
1943 cx.set_shared_wrap(12).await;
1944
1945 cx.set_shared_state(indoc! {
1946 "\"ˇhello world\"!"
1947 })
1948 .await;
1949 cx.simulate_shared_keystrokes("v i \"").await;
1950 cx.shared_state().await.assert_eq(indoc! {
1951 "\"«hello worldˇ»\"!"
1952 });
1953
1954 cx.set_shared_state(indoc! {
1955 "\"hˇello world\"!"
1956 })
1957 .await;
1958 cx.simulate_shared_keystrokes("v i \"").await;
1959 cx.shared_state().await.assert_eq(indoc! {
1960 "\"«hello worldˇ»\"!"
1961 });
1962
1963 cx.set_shared_state(indoc! {
1964 "helˇlo \"world\"!"
1965 })
1966 .await;
1967 cx.simulate_shared_keystrokes("v i \"").await;
1968 cx.shared_state().await.assert_eq(indoc! {
1969 "hello \"«worldˇ»\"!"
1970 });
1971
1972 cx.set_shared_state(indoc! {
1973 "hello \"wˇorld\"!"
1974 })
1975 .await;
1976 cx.simulate_shared_keystrokes("v i \"").await;
1977 cx.shared_state().await.assert_eq(indoc! {
1978 "hello \"«worldˇ»\"!"
1979 });
1980
1981 cx.set_shared_state(indoc! {
1982 "hello \"wˇorld\"!"
1983 })
1984 .await;
1985 cx.simulate_shared_keystrokes("v a \"").await;
1986 cx.shared_state().await.assert_eq(indoc! {
1987 "hello« \"world\"ˇ»!"
1988 });
1989
1990 cx.set_shared_state(indoc! {
1991 "hello \"wˇorld\" !"
1992 })
1993 .await;
1994 cx.simulate_shared_keystrokes("v a \"").await;
1995 cx.shared_state().await.assert_eq(indoc! {
1996 "hello «\"world\" ˇ»!"
1997 });
1998
1999 cx.set_shared_state(indoc! {
2000 "hello \"wˇorld\"•
2001 goodbye"
2002 })
2003 .await;
2004 cx.simulate_shared_keystrokes("v a \"").await;
2005 cx.shared_state().await.assert_eq(indoc! {
2006 "hello «\"world\" ˇ»
2007 goodbye"
2008 });
2009 }
2010
2011 #[gpui::test]
2012 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2013 let mut cx = VimTestContext::new(cx, true).await;
2014
2015 cx.set_state(
2016 indoc! {
2017 "func empty(a string) bool {
2018 if a == \"\" {
2019 return true
2020 }
2021 ˇreturn false
2022 }"
2023 },
2024 Mode::Normal,
2025 );
2026 cx.simulate_keystrokes("v i {");
2027 cx.assert_state(
2028 indoc! {
2029 "func empty(a string) bool {
2030 «if a == \"\" {
2031 return true
2032 }
2033 return falseˇ»
2034 }"
2035 },
2036 Mode::Visual,
2037 );
2038
2039 cx.set_state(
2040 indoc! {
2041 "func empty(a string) bool {
2042 if a == \"\" {
2043 ˇreturn true
2044 }
2045 return false
2046 }"
2047 },
2048 Mode::Normal,
2049 );
2050 cx.simulate_keystrokes("v i {");
2051 cx.assert_state(
2052 indoc! {
2053 "func empty(a string) bool {
2054 if a == \"\" {
2055 «return trueˇ»
2056 }
2057 return false
2058 }"
2059 },
2060 Mode::Visual,
2061 );
2062
2063 cx.set_state(
2064 indoc! {
2065 "func empty(a string) bool {
2066 if a == \"\" ˇ{
2067 return true
2068 }
2069 return false
2070 }"
2071 },
2072 Mode::Normal,
2073 );
2074 cx.simulate_keystrokes("v i {");
2075 cx.assert_state(
2076 indoc! {
2077 "func empty(a string) bool {
2078 if a == \"\" {
2079 «return trueˇ»
2080 }
2081 return false
2082 }"
2083 },
2084 Mode::Visual,
2085 );
2086
2087 cx.set_state(
2088 indoc! {
2089 "func empty(a string) bool {
2090 if a == \"\" {
2091 return true
2092 }
2093 return false
2094 ˇ}"
2095 },
2096 Mode::Normal,
2097 );
2098 cx.simulate_keystrokes("v i {");
2099 cx.assert_state(
2100 indoc! {
2101 "func empty(a string) bool {
2102 «if a == \"\" {
2103 return true
2104 }
2105 return falseˇ»
2106 }"
2107 },
2108 Mode::Visual,
2109 );
2110
2111 cx.set_state(
2112 indoc! {
2113 "func empty(a string) bool {
2114 if a == \"\" {
2115 ˇ
2116
2117 }"
2118 },
2119 Mode::Normal,
2120 );
2121 cx.simulate_keystrokes("c i {");
2122 cx.assert_state(
2123 indoc! {
2124 "func empty(a string) bool {
2125 if a == \"\" {ˇ}"
2126 },
2127 Mode::Insert,
2128 );
2129 }
2130
2131 #[gpui::test]
2132 async fn test_singleline_surrounding_character_objects_with_escape(
2133 cx: &mut gpui::TestAppContext,
2134 ) {
2135 let mut cx = NeovimBackedTestContext::new(cx).await;
2136 cx.set_shared_state(indoc! {
2137 "h\"e\\\"lˇlo \\\"world\"!"
2138 })
2139 .await;
2140 cx.simulate_shared_keystrokes("v i \"").await;
2141 cx.shared_state().await.assert_eq(indoc! {
2142 "h\"«e\\\"llo \\\"worldˇ»\"!"
2143 });
2144
2145 cx.set_shared_state(indoc! {
2146 "hello \"teˇst \\\"inside\\\" world\""
2147 })
2148 .await;
2149 cx.simulate_shared_keystrokes("v i \"").await;
2150 cx.shared_state().await.assert_eq(indoc! {
2151 "hello \"«test \\\"inside\\\" worldˇ»\""
2152 });
2153 }
2154
2155 #[gpui::test]
2156 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2157 let mut cx = VimTestContext::new(cx, true).await;
2158 cx.set_state(
2159 indoc! {"
2160 fn boop() {
2161 baz(ˇ|a, b| { bar(|j, k| { })})
2162 }"
2163 },
2164 Mode::Normal,
2165 );
2166 cx.simulate_keystrokes("c i |");
2167 cx.assert_state(
2168 indoc! {"
2169 fn boop() {
2170 baz(|ˇ| { bar(|j, k| { })})
2171 }"
2172 },
2173 Mode::Insert,
2174 );
2175 cx.simulate_keystrokes("escape 1 8 |");
2176 cx.assert_state(
2177 indoc! {"
2178 fn boop() {
2179 baz(|| { bar(ˇ|j, k| { })})
2180 }"
2181 },
2182 Mode::Normal,
2183 );
2184
2185 cx.simulate_keystrokes("v a |");
2186 cx.assert_state(
2187 indoc! {"
2188 fn boop() {
2189 baz(|| { bar(«|j, k| ˇ»{ })})
2190 }"
2191 },
2192 Mode::Visual,
2193 );
2194 }
2195
2196 #[gpui::test]
2197 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2198 let mut cx = VimTestContext::new(cx, true).await;
2199
2200 // Generic arguments
2201 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2202 cx.simulate_keystrokes("v i a");
2203 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2204
2205 // Function arguments
2206 cx.set_state(
2207 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2208 Mode::Normal,
2209 );
2210 cx.simulate_keystrokes("d a a");
2211 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2212
2213 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2214 cx.simulate_keystrokes("v a a");
2215 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2216
2217 // Tuple, vec, and array arguments
2218 cx.set_state(
2219 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2220 Mode::Normal,
2221 );
2222 cx.simulate_keystrokes("c i a");
2223 cx.assert_state(
2224 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2225 Mode::Insert,
2226 );
2227
2228 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2229 cx.simulate_keystrokes("c a a");
2230 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2231
2232 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2233 cx.simulate_keystrokes("c i a");
2234 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2235
2236 cx.set_state(
2237 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2238 Mode::Normal,
2239 );
2240 cx.simulate_keystrokes("c a a");
2241 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2242
2243 // Cursor immediately before / after brackets
2244 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2245 cx.simulate_keystrokes("v i a");
2246 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2247
2248 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2249 cx.simulate_keystrokes("v i a");
2250 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2251 }
2252
2253 #[gpui::test]
2254 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2255 let mut cx = VimTestContext::new(cx, true).await;
2256
2257 // Base use case
2258 cx.set_state(
2259 indoc! {"
2260 fn boop() {
2261 // Comment
2262 baz();ˇ
2263
2264 loop {
2265 bar(1);
2266 bar(2);
2267 }
2268
2269 result
2270 }
2271 "},
2272 Mode::Normal,
2273 );
2274 cx.simulate_keystrokes("v i i");
2275 cx.assert_state(
2276 indoc! {"
2277 fn boop() {
2278 « // Comment
2279 baz();
2280
2281 loop {
2282 bar(1);
2283 bar(2);
2284 }
2285
2286 resultˇ»
2287 }
2288 "},
2289 Mode::Visual,
2290 );
2291
2292 // Around indent (include line above)
2293 cx.set_state(
2294 indoc! {"
2295 const ABOVE: str = true;
2296 fn boop() {
2297
2298 hello();
2299 worˇld()
2300 }
2301 "},
2302 Mode::Normal,
2303 );
2304 cx.simulate_keystrokes("v a i");
2305 cx.assert_state(
2306 indoc! {"
2307 const ABOVE: str = true;
2308 «fn boop() {
2309
2310 hello();
2311 world()ˇ»
2312 }
2313 "},
2314 Mode::Visual,
2315 );
2316
2317 // Around indent (include line above & below)
2318 cx.set_state(
2319 indoc! {"
2320 const ABOVE: str = true;
2321 fn boop() {
2322 hellˇo();
2323 world()
2324
2325 }
2326 const BELOW: str = true;
2327 "},
2328 Mode::Normal,
2329 );
2330 cx.simulate_keystrokes("c a shift-i");
2331 cx.assert_state(
2332 indoc! {"
2333 const ABOVE: str = true;
2334 ˇ
2335 const BELOW: str = true;
2336 "},
2337 Mode::Insert,
2338 );
2339 }
2340
2341 #[gpui::test]
2342 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2343 let mut cx = NeovimBackedTestContext::new(cx).await;
2344
2345 for (start, end) in SURROUNDING_OBJECTS {
2346 let marked_string = SURROUNDING_MARKER_STRING
2347 .replace('`', &start.to_string())
2348 .replace('\'', &end.to_string());
2349
2350 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2351 .await
2352 .assert_matches();
2353 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2354 .await
2355 .assert_matches();
2356 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2357 .await
2358 .assert_matches();
2359 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2360 .await
2361 .assert_matches();
2362 }
2363 }
2364
2365 #[gpui::test]
2366 async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2367 let mut cx = VimTestContext::new(cx, true).await;
2368 cx.update(|_, cx| {
2369 cx.bind_keys([KeyBinding::new(
2370 "q",
2371 AnyQuotes,
2372 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2373 )]);
2374 });
2375
2376 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2377 // the false string in the middle should be considered
2378 (
2379 "c i q",
2380 "'first' false ˇstring 'second'",
2381 "'first'ˇ'second'",
2382 Mode::Insert,
2383 ),
2384 // Single quotes
2385 (
2386 "c i q",
2387 "Thisˇ is a 'quote' example.",
2388 "This is a 'ˇ' example.",
2389 Mode::Insert,
2390 ),
2391 (
2392 "c a q",
2393 "Thisˇ is a 'quote' example.",
2394 "This is a ˇexample.",
2395 Mode::Insert,
2396 ),
2397 (
2398 "c i q",
2399 "This is a \"simple 'qˇuote'\" example.",
2400 "This is a \"simple 'ˇ'\" example.",
2401 Mode::Insert,
2402 ),
2403 (
2404 "c a q",
2405 "This is a \"simple 'qˇuote'\" example.",
2406 "This is a \"simpleˇ\" example.",
2407 Mode::Insert,
2408 ),
2409 (
2410 "c i q",
2411 "This is a 'qˇuote' example.",
2412 "This is a 'ˇ' example.",
2413 Mode::Insert,
2414 ),
2415 (
2416 "c a q",
2417 "This is a 'qˇuote' example.",
2418 "This is a ˇexample.",
2419 Mode::Insert,
2420 ),
2421 (
2422 "d i q",
2423 "This is a 'qˇuote' example.",
2424 "This is a 'ˇ' example.",
2425 Mode::Normal,
2426 ),
2427 (
2428 "d a q",
2429 "This is a 'qˇuote' example.",
2430 "This is a ˇexample.",
2431 Mode::Normal,
2432 ),
2433 // Double quotes
2434 (
2435 "c i q",
2436 "This is a \"qˇuote\" example.",
2437 "This is a \"ˇ\" example.",
2438 Mode::Insert,
2439 ),
2440 (
2441 "c a q",
2442 "This is a \"qˇuote\" example.",
2443 "This is a ˇexample.",
2444 Mode::Insert,
2445 ),
2446 (
2447 "d i q",
2448 "This is a \"qˇuote\" example.",
2449 "This is a \"ˇ\" example.",
2450 Mode::Normal,
2451 ),
2452 (
2453 "d a q",
2454 "This is a \"qˇuote\" example.",
2455 "This is a ˇexample.",
2456 Mode::Normal,
2457 ),
2458 // Back quotes
2459 (
2460 "c i q",
2461 "This is a `qˇuote` example.",
2462 "This is a `ˇ` example.",
2463 Mode::Insert,
2464 ),
2465 (
2466 "c a q",
2467 "This is a `qˇuote` example.",
2468 "This is a ˇexample.",
2469 Mode::Insert,
2470 ),
2471 (
2472 "d i q",
2473 "This is a `qˇuote` example.",
2474 "This is a `ˇ` example.",
2475 Mode::Normal,
2476 ),
2477 (
2478 "d a q",
2479 "This is a `qˇuote` example.",
2480 "This is a ˇexample.",
2481 Mode::Normal,
2482 ),
2483 ];
2484
2485 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2486 cx.set_state(initial_state, Mode::Normal);
2487
2488 cx.simulate_keystrokes(keystrokes);
2489
2490 cx.assert_state(expected_state, *expected_mode);
2491 }
2492
2493 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2494 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2495 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2496 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2497 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2498 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2499 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2500 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2501 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2502 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2503 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2504 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2505 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2506 ];
2507
2508 for (keystrokes, initial_state, mode) in INVALID_CASES {
2509 cx.set_state(initial_state, Mode::Normal);
2510
2511 cx.simulate_keystrokes(keystrokes);
2512
2513 cx.assert_state(initial_state, *mode);
2514 }
2515 }
2516
2517 #[gpui::test]
2518 async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2519 let mut cx = VimTestContext::new_typescript(cx).await;
2520
2521 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2522 // Special cases from mini.ai plugin
2523 // the false string in the middle should not be considered
2524 (
2525 "c i q",
2526 "'first' false ˇstring 'second'",
2527 "'first' false string 'ˇ'",
2528 Mode::Insert,
2529 ),
2530 // Multiline support :)! Same behavior as mini.ai plugin
2531 (
2532 "c i q",
2533 indoc! {"
2534 `
2535 first
2536 middle ˇstring
2537 second
2538 `
2539 "},
2540 indoc! {"
2541 `ˇ`
2542 "},
2543 Mode::Insert,
2544 ),
2545 // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2546 // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2547 // Bug reference: https://github.com/zed-industries/zed/issues/23889
2548 ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2549 // Single quotes
2550 (
2551 "c i q",
2552 "Thisˇ is a 'quote' example.",
2553 "This is a 'ˇ' example.",
2554 Mode::Insert,
2555 ),
2556 (
2557 "c a q",
2558 "Thisˇ is a 'quote' example.",
2559 "This is a ˇ example.", // same mini.ai plugin behavior
2560 Mode::Insert,
2561 ),
2562 (
2563 "c i q",
2564 "This is a \"simple 'qˇuote'\" example.",
2565 "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2566 Mode::Insert,
2567 ),
2568 (
2569 "c a q",
2570 "This is a \"simple 'qˇuote'\" example.",
2571 "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2572 Mode::Insert,
2573 ),
2574 (
2575 "c i q",
2576 "This is a 'qˇuote' example.",
2577 "This is a 'ˇ' example.",
2578 Mode::Insert,
2579 ),
2580 (
2581 "c a q",
2582 "This is a 'qˇuote' example.",
2583 "This is a ˇ example.", // same mini.ai plugin behavior
2584 Mode::Insert,
2585 ),
2586 (
2587 "d i q",
2588 "This is a 'qˇuote' example.",
2589 "This is a 'ˇ' example.",
2590 Mode::Normal,
2591 ),
2592 (
2593 "d a q",
2594 "This is a 'qˇuote' example.",
2595 "This is a ˇ example.", // same mini.ai plugin behavior
2596 Mode::Normal,
2597 ),
2598 // Double quotes
2599 (
2600 "c i q",
2601 "This is a \"qˇuote\" example.",
2602 "This is a \"ˇ\" example.",
2603 Mode::Insert,
2604 ),
2605 (
2606 "c a q",
2607 "This is a \"qˇuote\" example.",
2608 "This is a ˇ example.", // same mini.ai plugin behavior
2609 Mode::Insert,
2610 ),
2611 (
2612 "d i q",
2613 "This is a \"qˇuote\" example.",
2614 "This is a \"ˇ\" example.",
2615 Mode::Normal,
2616 ),
2617 (
2618 "d a q",
2619 "This is a \"qˇuote\" example.",
2620 "This is a ˇ example.", // same mini.ai plugin behavior
2621 Mode::Normal,
2622 ),
2623 // Back quotes
2624 (
2625 "c i q",
2626 "This is a `qˇuote` example.",
2627 "This is a `ˇ` example.",
2628 Mode::Insert,
2629 ),
2630 (
2631 "c a q",
2632 "This is a `qˇuote` example.",
2633 "This is a ˇ example.", // same mini.ai plugin behavior
2634 Mode::Insert,
2635 ),
2636 (
2637 "d i q",
2638 "This is a `qˇuote` example.",
2639 "This is a `ˇ` example.",
2640 Mode::Normal,
2641 ),
2642 (
2643 "d a q",
2644 "This is a `qˇuote` example.",
2645 "This is a ˇ example.", // same mini.ai plugin behavior
2646 Mode::Normal,
2647 ),
2648 ];
2649
2650 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2651 cx.set_state(initial_state, Mode::Normal);
2652
2653 cx.simulate_keystrokes(keystrokes);
2654
2655 cx.assert_state(expected_state, *expected_mode);
2656 }
2657
2658 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2659 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2660 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2661 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2662 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2663 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2664 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2665 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2666 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2667 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2668 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2669 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2670 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2671 ];
2672
2673 for (keystrokes, initial_state, mode) in INVALID_CASES {
2674 cx.set_state(initial_state, Mode::Normal);
2675
2676 cx.simulate_keystrokes(keystrokes);
2677
2678 cx.assert_state(initial_state, *mode);
2679 }
2680 }
2681
2682 #[gpui::test]
2683 async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2684 let mut cx = VimTestContext::new(cx, true).await;
2685 cx.update(|_, cx| {
2686 cx.bind_keys([KeyBinding::new(
2687 "b",
2688 AnyBrackets,
2689 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2690 )]);
2691 });
2692
2693 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2694 (
2695 "c i b",
2696 indoc! {"
2697 {
2698 {
2699 ˇprint('hello')
2700 }
2701 }
2702 "},
2703 indoc! {"
2704 {
2705 {
2706 ˇ
2707 }
2708 }
2709 "},
2710 Mode::Insert,
2711 ),
2712 // Bracket (Parentheses)
2713 (
2714 "c i b",
2715 "Thisˇ is a (simple [quote]) example.",
2716 "This is a (ˇ) example.",
2717 Mode::Insert,
2718 ),
2719 (
2720 "c i b",
2721 "This is a [simple (qˇuote)] example.",
2722 "This is a [simple (ˇ)] example.",
2723 Mode::Insert,
2724 ),
2725 (
2726 "c a b",
2727 "This is a [simple (qˇuote)] example.",
2728 "This is a [simple ˇ] example.",
2729 Mode::Insert,
2730 ),
2731 (
2732 "c a b",
2733 "Thisˇ is a (simple [quote]) example.",
2734 "This is a ˇ example.",
2735 Mode::Insert,
2736 ),
2737 (
2738 "c i b",
2739 "This is a (qˇuote) example.",
2740 "This is a (ˇ) example.",
2741 Mode::Insert,
2742 ),
2743 (
2744 "c a b",
2745 "This is a (qˇuote) example.",
2746 "This is a ˇ example.",
2747 Mode::Insert,
2748 ),
2749 (
2750 "d i b",
2751 "This is a (qˇuote) example.",
2752 "This is a (ˇ) example.",
2753 Mode::Normal,
2754 ),
2755 (
2756 "d a b",
2757 "This is a (qˇuote) example.",
2758 "This is a ˇ example.",
2759 Mode::Normal,
2760 ),
2761 // Square brackets
2762 (
2763 "c i b",
2764 "This is a [qˇuote] example.",
2765 "This is a [ˇ] example.",
2766 Mode::Insert,
2767 ),
2768 (
2769 "c a b",
2770 "This is a [qˇuote] example.",
2771 "This is a ˇ example.",
2772 Mode::Insert,
2773 ),
2774 (
2775 "d i b",
2776 "This is a [qˇuote] example.",
2777 "This is a [ˇ] example.",
2778 Mode::Normal,
2779 ),
2780 (
2781 "d a b",
2782 "This is a [qˇuote] example.",
2783 "This is a ˇ example.",
2784 Mode::Normal,
2785 ),
2786 // Curly brackets
2787 (
2788 "c i b",
2789 "This is a {qˇuote} example.",
2790 "This is a {ˇ} example.",
2791 Mode::Insert,
2792 ),
2793 (
2794 "c a b",
2795 "This is a {qˇuote} example.",
2796 "This is a ˇ example.",
2797 Mode::Insert,
2798 ),
2799 (
2800 "d i b",
2801 "This is a {qˇuote} example.",
2802 "This is a {ˇ} example.",
2803 Mode::Normal,
2804 ),
2805 (
2806 "d a b",
2807 "This is a {qˇuote} example.",
2808 "This is a ˇ example.",
2809 Mode::Normal,
2810 ),
2811 ];
2812
2813 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2814 cx.set_state(initial_state, Mode::Normal);
2815
2816 cx.simulate_keystrokes(keystrokes);
2817
2818 cx.assert_state(expected_state, *expected_mode);
2819 }
2820
2821 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2822 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2823 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2824 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2825 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2826 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2827 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2828 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2829 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2830 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2831 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2832 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2833 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2834 ];
2835
2836 for (keystrokes, initial_state, mode) in INVALID_CASES {
2837 cx.set_state(initial_state, Mode::Normal);
2838
2839 cx.simulate_keystrokes(keystrokes);
2840
2841 cx.assert_state(initial_state, *mode);
2842 }
2843 }
2844
2845 #[gpui::test]
2846 async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
2847 let mut cx = VimTestContext::new(cx, true).await;
2848 cx.update(|_, cx| {
2849 cx.bind_keys([KeyBinding::new(
2850 "b",
2851 MiniBrackets,
2852 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2853 )]);
2854 });
2855
2856 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2857 // Special cases from mini.ai plugin
2858 // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
2859 // Same behavior as mini.ai plugin
2860 (
2861 "c i b",
2862 indoc! {"
2863 {
2864 {
2865 ˇprint('hello')
2866 }
2867 }
2868 "},
2869 indoc! {"
2870 {
2871 {
2872 print(ˇ)
2873 }
2874 }
2875 "},
2876 Mode::Insert,
2877 ),
2878 // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
2879 // Same behavior as mini.ai plugin
2880 (
2881 "c i b",
2882 indoc! {"
2883 {
2884 {
2885 ˇ
2886 print('hello')
2887 }
2888 }
2889 "},
2890 indoc! {"
2891 {
2892 {ˇ}
2893 }
2894 "},
2895 Mode::Insert,
2896 ),
2897 // If you are in the open bracket then it has higher priority
2898 (
2899 "c i b",
2900 indoc! {"
2901 «{ˇ»
2902 {
2903 print('hello')
2904 }
2905 }
2906 "},
2907 indoc! {"
2908 {ˇ}
2909 "},
2910 Mode::Insert,
2911 ),
2912 // If you are in the close bracket then it has higher priority
2913 (
2914 "c i b",
2915 indoc! {"
2916 {
2917 {
2918 print('hello')
2919 }
2920 «}ˇ»
2921 "},
2922 indoc! {"
2923 {ˇ}
2924 "},
2925 Mode::Insert,
2926 ),
2927 // Bracket (Parentheses)
2928 (
2929 "c i b",
2930 "Thisˇ is a (simple [quote]) example.",
2931 "This is a (ˇ) example.",
2932 Mode::Insert,
2933 ),
2934 (
2935 "c i b",
2936 "This is a [simple (qˇuote)] example.",
2937 "This is a [simple (ˇ)] example.",
2938 Mode::Insert,
2939 ),
2940 (
2941 "c a b",
2942 "This is a [simple (qˇuote)] example.",
2943 "This is a [simple ˇ] example.",
2944 Mode::Insert,
2945 ),
2946 (
2947 "c a b",
2948 "Thisˇ is a (simple [quote]) example.",
2949 "This is a ˇ example.",
2950 Mode::Insert,
2951 ),
2952 (
2953 "c i b",
2954 "This is a (qˇuote) example.",
2955 "This is a (ˇ) example.",
2956 Mode::Insert,
2957 ),
2958 (
2959 "c a b",
2960 "This is a (qˇuote) example.",
2961 "This is a ˇ example.",
2962 Mode::Insert,
2963 ),
2964 (
2965 "d i b",
2966 "This is a (qˇuote) example.",
2967 "This is a (ˇ) example.",
2968 Mode::Normal,
2969 ),
2970 (
2971 "d a b",
2972 "This is a (qˇuote) example.",
2973 "This is a ˇ example.",
2974 Mode::Normal,
2975 ),
2976 // Square brackets
2977 (
2978 "c i b",
2979 "This is a [qˇuote] example.",
2980 "This is a [ˇ] example.",
2981 Mode::Insert,
2982 ),
2983 (
2984 "c a b",
2985 "This is a [qˇuote] example.",
2986 "This is a ˇ example.",
2987 Mode::Insert,
2988 ),
2989 (
2990 "d i b",
2991 "This is a [qˇuote] example.",
2992 "This is a [ˇ] example.",
2993 Mode::Normal,
2994 ),
2995 (
2996 "d a b",
2997 "This is a [qˇuote] example.",
2998 "This is a ˇ example.",
2999 Mode::Normal,
3000 ),
3001 // Curly brackets
3002 (
3003 "c i b",
3004 "This is a {qˇuote} example.",
3005 "This is a {ˇ} example.",
3006 Mode::Insert,
3007 ),
3008 (
3009 "c a b",
3010 "This is a {qˇuote} example.",
3011 "This is a ˇ example.",
3012 Mode::Insert,
3013 ),
3014 (
3015 "d i b",
3016 "This is a {qˇuote} example.",
3017 "This is a {ˇ} example.",
3018 Mode::Normal,
3019 ),
3020 (
3021 "d a b",
3022 "This is a {qˇuote} example.",
3023 "This is a ˇ example.",
3024 Mode::Normal,
3025 ),
3026 ];
3027
3028 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3029 cx.set_state(initial_state, Mode::Normal);
3030
3031 cx.simulate_keystrokes(keystrokes);
3032
3033 cx.assert_state(expected_state, *expected_mode);
3034 }
3035
3036 const INVALID_CASES: &[(&str, &str, Mode)] = &[
3037 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3038 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3039 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3040 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3041 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3042 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3043 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3044 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3045 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3046 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3047 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3048 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3049 ];
3050
3051 for (keystrokes, initial_state, mode) in INVALID_CASES {
3052 cx.set_state(initial_state, Mode::Normal);
3053
3054 cx.simulate_keystrokes(keystrokes);
3055
3056 cx.assert_state(initial_state, *mode);
3057 }
3058 }
3059
3060 #[gpui::test]
3061 async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3062 let mut cx = NeovimBackedTestContext::new(cx).await;
3063 cx.set_shared_state("(trailingˇ whitespace )")
3064 .await;
3065 cx.simulate_shared_keystrokes("v i b").await;
3066 cx.shared_state().await.assert_matches();
3067 cx.simulate_shared_keystrokes("escape y i b").await;
3068 cx.shared_clipboard()
3069 .await
3070 .assert_eq("trailing whitespace ");
3071 }
3072
3073 #[gpui::test]
3074 async fn test_tags(cx: &mut gpui::TestAppContext) {
3075 let mut cx = VimTestContext::new_html(cx).await;
3076
3077 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3078 cx.simulate_keystrokes("v i t");
3079 cx.assert_state(
3080 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3081 Mode::Visual,
3082 );
3083 cx.simulate_keystrokes("a t");
3084 cx.assert_state(
3085 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3086 Mode::Visual,
3087 );
3088 cx.simulate_keystrokes("a t");
3089 cx.assert_state(
3090 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3091 Mode::Visual,
3092 );
3093
3094 // The cursor is before the tag
3095 cx.set_state(
3096 "<html><head></head><body> ˇ <b>hi!</b></body>",
3097 Mode::Normal,
3098 );
3099 cx.simulate_keystrokes("v i t");
3100 cx.assert_state(
3101 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
3102 Mode::Visual,
3103 );
3104 cx.simulate_keystrokes("a t");
3105 cx.assert_state(
3106 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
3107 Mode::Visual,
3108 );
3109
3110 // The cursor is in the open tag
3111 cx.set_state(
3112 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3113 Mode::Normal,
3114 );
3115 cx.simulate_keystrokes("v a t");
3116 cx.assert_state(
3117 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3118 Mode::Visual,
3119 );
3120 cx.simulate_keystrokes("i t");
3121 cx.assert_state(
3122 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3123 Mode::Visual,
3124 );
3125
3126 // current selection length greater than 1
3127 cx.set_state(
3128 "<html><head></head><body><«b>hi!ˇ»</b></body>",
3129 Mode::Visual,
3130 );
3131 cx.simulate_keystrokes("i t");
3132 cx.assert_state(
3133 "<html><head></head><body><b>«hi!ˇ»</b></body>",
3134 Mode::Visual,
3135 );
3136 cx.simulate_keystrokes("a t");
3137 cx.assert_state(
3138 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3139 Mode::Visual,
3140 );
3141
3142 cx.set_state(
3143 "<html><head></head><body><«b>hi!</ˇ»b></body>",
3144 Mode::Visual,
3145 );
3146 cx.simulate_keystrokes("a t");
3147 cx.assert_state(
3148 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3149 Mode::Visual,
3150 );
3151 }
3152 #[gpui::test]
3153 async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3154 let mut cx = NeovimBackedTestContext::new(cx).await;
3155
3156 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3157 .await;
3158 cx.simulate_shared_keystrokes("v a w").await;
3159 cx.shared_state()
3160 .await
3161 .assert_eq(" «const ˇ»f = (x: unknown) => {");
3162
3163 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3164 .await;
3165 cx.simulate_shared_keystrokes("y a w").await;
3166 cx.shared_clipboard().await.assert_eq("const ");
3167
3168 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3169 .await;
3170 cx.simulate_shared_keystrokes("d a w").await;
3171 cx.shared_state()
3172 .await
3173 .assert_eq(" ˇf = (x: unknown) => {");
3174 cx.shared_clipboard().await.assert_eq("const ");
3175
3176 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
3177 .await;
3178 cx.simulate_shared_keystrokes("c a w").await;
3179 cx.shared_state()
3180 .await
3181 .assert_eq(" ˇf = (x: unknown) => {");
3182 cx.shared_clipboard().await.assert_eq("const ");
3183 }
3184}