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