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