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