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