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