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