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