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 if end > opening.end {
1550 closing.start = end;
1551 }
1552 }
1553 }
1554
1555 let result = if around {
1556 opening.start..closing.end
1557 } else {
1558 opening.end..closing.start
1559 };
1560
1561 Some(
1562 map.clip_point(result.start.to_display_point(map), Bias::Left)
1563 ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1564 )
1565}
1566
1567#[cfg(test)]
1568mod test {
1569 use gpui::KeyBinding;
1570 use indoc::indoc;
1571
1572 use crate::{
1573 object::AnyBrackets,
1574 state::Mode,
1575 test::{NeovimBackedTestContext, VimTestContext},
1576 };
1577
1578 const WORD_LOCATIONS: &str = indoc! {"
1579 The quick ˇbrowˇnˇ•••
1580 fox ˇjuˇmpsˇ over
1581 the lazy dogˇ••
1582 ˇ
1583 ˇ
1584 ˇ
1585 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1586 ˇ••
1587 ˇ••
1588 ˇ fox-jumpˇs over
1589 the lazy dogˇ•
1590 ˇ
1591 "
1592 };
1593
1594 #[gpui::test]
1595 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1596 let mut cx = NeovimBackedTestContext::new(cx).await;
1597
1598 cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1599 .await
1600 .assert_matches();
1601 cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1602 .await
1603 .assert_matches();
1604 cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1605 .await
1606 .assert_matches();
1607 cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1608 .await
1609 .assert_matches();
1610 }
1611
1612 #[gpui::test]
1613 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1614 let mut cx = NeovimBackedTestContext::new(cx).await;
1615
1616 cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1617 .await
1618 .assert_matches();
1619 cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1620 .await
1621 .assert_matches();
1622 cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1623 .await
1624 .assert_matches();
1625 cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1626 .await
1627 .assert_matches();
1628 }
1629
1630 #[gpui::test]
1631 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1632 let mut cx = NeovimBackedTestContext::new(cx).await;
1633
1634 /*
1635 cx.set_shared_state("The quick ˇbrown\nfox").await;
1636 cx.simulate_shared_keystrokes(["v"]).await;
1637 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1638 cx.simulate_shared_keystrokes(["i", "w"]).await;
1639 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1640 */
1641 cx.set_shared_state("The quick brown\nˇ\nfox").await;
1642 cx.simulate_shared_keystrokes("v").await;
1643 cx.shared_state()
1644 .await
1645 .assert_eq("The quick brown\n«\nˇ»fox");
1646 cx.simulate_shared_keystrokes("i w").await;
1647 cx.shared_state()
1648 .await
1649 .assert_eq("The quick brown\n«\nˇ»fox");
1650
1651 cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1652 .await
1653 .assert_matches();
1654 cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1655 .await
1656 .assert_matches();
1657 }
1658
1659 const PARAGRAPH_EXAMPLES: &[&str] = &[
1660 // Single line
1661 "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1662 // Multiple lines without empty lines
1663 indoc! {"
1664 ˇThe quick brownˇ
1665 ˇfox jumps overˇ
1666 the lazy dog.ˇ
1667 "},
1668 // Heading blank paragraph and trailing normal paragraph
1669 indoc! {"
1670 ˇ
1671 ˇ
1672 ˇThe quick brown fox jumps
1673 ˇover the lazy dog.
1674 ˇ
1675 ˇ
1676 ˇThe quick brown fox jumpsˇ
1677 ˇover the lazy dog.ˇ
1678 "},
1679 // Inserted blank paragraph and trailing blank paragraph
1680 indoc! {"
1681 ˇThe quick brown fox jumps
1682 ˇover the lazy dog.
1683 ˇ
1684 ˇ
1685 ˇ
1686 ˇThe quick brown fox jumpsˇ
1687 ˇover the lazy dog.ˇ
1688 ˇ
1689 ˇ
1690 ˇ
1691 "},
1692 // "Blank" paragraph with whitespace characters
1693 indoc! {"
1694 ˇThe quick brown fox jumps
1695 over the lazy dog.
1696
1697 ˇ \t
1698
1699 ˇThe quick brown fox jumps
1700 over the lazy dog.ˇ
1701 ˇ
1702 ˇ \t
1703 \t \t
1704 "},
1705 // Single line "paragraphs", where selection size might be zero.
1706 indoc! {"
1707 ˇThe quick brown fox jumps over the lazy dog.
1708 ˇ
1709 ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1710 ˇ
1711 "},
1712 ];
1713
1714 #[gpui::test]
1715 async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1716 let mut cx = NeovimBackedTestContext::new(cx).await;
1717
1718 for paragraph_example in PARAGRAPH_EXAMPLES {
1719 cx.simulate_at_each_offset("c i p", paragraph_example)
1720 .await
1721 .assert_matches();
1722 cx.simulate_at_each_offset("c a p", paragraph_example)
1723 .await
1724 .assert_matches();
1725 }
1726 }
1727
1728 #[gpui::test]
1729 async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1730 let mut cx = NeovimBackedTestContext::new(cx).await;
1731
1732 for paragraph_example in PARAGRAPH_EXAMPLES {
1733 cx.simulate_at_each_offset("d i p", paragraph_example)
1734 .await
1735 .assert_matches();
1736 cx.simulate_at_each_offset("d a p", paragraph_example)
1737 .await
1738 .assert_matches();
1739 }
1740 }
1741
1742 #[gpui::test]
1743 async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1744 let mut cx = NeovimBackedTestContext::new(cx).await;
1745
1746 const EXAMPLES: &[&str] = &[
1747 indoc! {"
1748 ˇThe quick brown
1749 fox jumps over
1750 the lazy dog.
1751 "},
1752 indoc! {"
1753 ˇ
1754
1755 ˇThe quick brown fox jumps
1756 over the lazy dog.
1757 ˇ
1758
1759 ˇThe quick brown fox jumps
1760 over the lazy dog.
1761 "},
1762 indoc! {"
1763 ˇThe quick brown fox jumps over the lazy dog.
1764 ˇ
1765 ˇThe quick brown fox jumps over the lazy dog.
1766
1767 "},
1768 ];
1769
1770 for paragraph_example in EXAMPLES {
1771 cx.simulate_at_each_offset("v i p", paragraph_example)
1772 .await
1773 .assert_matches();
1774 cx.simulate_at_each_offset("v a p", paragraph_example)
1775 .await
1776 .assert_matches();
1777 }
1778 }
1779
1780 // Test string with "`" for opening surrounders and "'" for closing surrounders
1781 const SURROUNDING_MARKER_STRING: &str = indoc! {"
1782 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1783 'ˇfox juˇmps ov`ˇer
1784 the ˇlazy d'o`ˇg"};
1785
1786 const SURROUNDING_OBJECTS: &[(char, char)] = &[
1787 ('"', '"'), // Double Quote
1788 ('(', ')'), // Parentheses
1789 ];
1790
1791 #[gpui::test]
1792 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1793 let mut cx = NeovimBackedTestContext::new(cx).await;
1794
1795 for (start, end) in SURROUNDING_OBJECTS {
1796 let marked_string = SURROUNDING_MARKER_STRING
1797 .replace('`', &start.to_string())
1798 .replace('\'', &end.to_string());
1799
1800 cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1801 .await
1802 .assert_matches();
1803 cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1804 .await
1805 .assert_matches();
1806 cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1807 .await
1808 .assert_matches();
1809 cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1810 .await
1811 .assert_matches();
1812 }
1813 }
1814 #[gpui::test]
1815 async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1816 let mut cx = NeovimBackedTestContext::new(cx).await;
1817 cx.set_shared_wrap(12).await;
1818
1819 cx.set_shared_state(indoc! {
1820 "\"ˇhello world\"!"
1821 })
1822 .await;
1823 cx.simulate_shared_keystrokes("v i \"").await;
1824 cx.shared_state().await.assert_eq(indoc! {
1825 "\"«hello worldˇ»\"!"
1826 });
1827
1828 cx.set_shared_state(indoc! {
1829 "\"hˇello world\"!"
1830 })
1831 .await;
1832 cx.simulate_shared_keystrokes("v i \"").await;
1833 cx.shared_state().await.assert_eq(indoc! {
1834 "\"«hello worldˇ»\"!"
1835 });
1836
1837 cx.set_shared_state(indoc! {
1838 "helˇlo \"world\"!"
1839 })
1840 .await;
1841 cx.simulate_shared_keystrokes("v i \"").await;
1842 cx.shared_state().await.assert_eq(indoc! {
1843 "hello \"«worldˇ»\"!"
1844 });
1845
1846 cx.set_shared_state(indoc! {
1847 "hello \"wˇorld\"!"
1848 })
1849 .await;
1850 cx.simulate_shared_keystrokes("v i \"").await;
1851 cx.shared_state().await.assert_eq(indoc! {
1852 "hello \"«worldˇ»\"!"
1853 });
1854
1855 cx.set_shared_state(indoc! {
1856 "hello \"wˇorld\"!"
1857 })
1858 .await;
1859 cx.simulate_shared_keystrokes("v a \"").await;
1860 cx.shared_state().await.assert_eq(indoc! {
1861 "hello« \"world\"ˇ»!"
1862 });
1863
1864 cx.set_shared_state(indoc! {
1865 "hello \"wˇorld\" !"
1866 })
1867 .await;
1868 cx.simulate_shared_keystrokes("v a \"").await;
1869 cx.shared_state().await.assert_eq(indoc! {
1870 "hello «\"world\" ˇ»!"
1871 });
1872
1873 cx.set_shared_state(indoc! {
1874 "hello \"wˇorld\"•
1875 goodbye"
1876 })
1877 .await;
1878 cx.simulate_shared_keystrokes("v a \"").await;
1879 cx.shared_state().await.assert_eq(indoc! {
1880 "hello «\"world\" ˇ»
1881 goodbye"
1882 });
1883 }
1884
1885 #[gpui::test]
1886 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1887 let mut cx = VimTestContext::new(cx, true).await;
1888
1889 cx.set_state(
1890 indoc! {
1891 "func empty(a string) bool {
1892 if a == \"\" {
1893 return true
1894 }
1895 ˇreturn false
1896 }"
1897 },
1898 Mode::Normal,
1899 );
1900 cx.simulate_keystrokes("v i {");
1901 cx.assert_state(
1902 indoc! {
1903 "func empty(a string) bool {
1904 «ˇif a == \"\" {
1905 return true
1906 }
1907 return false»
1908 }"
1909 },
1910 Mode::Visual,
1911 );
1912
1913 cx.set_state(
1914 indoc! {
1915 "func empty(a string) bool {
1916 if a == \"\" {
1917 ˇreturn true
1918 }
1919 return false
1920 }"
1921 },
1922 Mode::Normal,
1923 );
1924 cx.simulate_keystrokes("v i {");
1925 cx.assert_state(
1926 indoc! {
1927 "func empty(a string) bool {
1928 if a == \"\" {
1929 «ˇreturn true»
1930 }
1931 return false
1932 }"
1933 },
1934 Mode::Visual,
1935 );
1936
1937 cx.set_state(
1938 indoc! {
1939 "func empty(a string) bool {
1940 if a == \"\" ˇ{
1941 return true
1942 }
1943 return false
1944 }"
1945 },
1946 Mode::Normal,
1947 );
1948 cx.simulate_keystrokes("v i {");
1949 cx.assert_state(
1950 indoc! {
1951 "func empty(a string) bool {
1952 if a == \"\" {
1953 «ˇreturn true»
1954 }
1955 return false
1956 }"
1957 },
1958 Mode::Visual,
1959 );
1960
1961 cx.set_state(
1962 indoc! {
1963 "func empty(a string) bool {
1964 if a == \"\" {
1965 return true
1966 }
1967 return false
1968 ˇ}"
1969 },
1970 Mode::Normal,
1971 );
1972 cx.simulate_keystrokes("v i {");
1973 cx.assert_state(
1974 indoc! {
1975 "func empty(a string) bool {
1976 «ˇif a == \"\" {
1977 return true
1978 }
1979 return false»
1980 }"
1981 },
1982 Mode::Visual,
1983 );
1984 }
1985
1986 #[gpui::test]
1987 async fn test_singleline_surrounding_character_objects_with_escape(
1988 cx: &mut gpui::TestAppContext,
1989 ) {
1990 let mut cx = NeovimBackedTestContext::new(cx).await;
1991 cx.set_shared_state(indoc! {
1992 "h\"e\\\"lˇlo \\\"world\"!"
1993 })
1994 .await;
1995 cx.simulate_shared_keystrokes("v i \"").await;
1996 cx.shared_state().await.assert_eq(indoc! {
1997 "h\"«e\\\"llo \\\"worldˇ»\"!"
1998 });
1999
2000 cx.set_shared_state(indoc! {
2001 "hello \"teˇst \\\"inside\\\" world\""
2002 })
2003 .await;
2004 cx.simulate_shared_keystrokes("v i \"").await;
2005 cx.shared_state().await.assert_eq(indoc! {
2006 "hello \"«test \\\"inside\\\" worldˇ»\""
2007 });
2008 }
2009
2010 #[gpui::test]
2011 async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2012 let mut cx = VimTestContext::new(cx, true).await;
2013 cx.set_state(
2014 indoc! {"
2015 fn boop() {
2016 baz(ˇ|a, b| { bar(|j, k| { })})
2017 }"
2018 },
2019 Mode::Normal,
2020 );
2021 cx.simulate_keystrokes("c i |");
2022 cx.assert_state(
2023 indoc! {"
2024 fn boop() {
2025 baz(|ˇ| { bar(|j, k| { })})
2026 }"
2027 },
2028 Mode::Insert,
2029 );
2030 cx.simulate_keystrokes("escape 1 8 |");
2031 cx.assert_state(
2032 indoc! {"
2033 fn boop() {
2034 baz(|| { bar(ˇ|j, k| { })})
2035 }"
2036 },
2037 Mode::Normal,
2038 );
2039
2040 cx.simulate_keystrokes("v a |");
2041 cx.assert_state(
2042 indoc! {"
2043 fn boop() {
2044 baz(|| { bar(«|j, k| ˇ»{ })})
2045 }"
2046 },
2047 Mode::Visual,
2048 );
2049 }
2050
2051 #[gpui::test]
2052 async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2053 let mut cx = VimTestContext::new(cx, true).await;
2054
2055 // Generic arguments
2056 cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2057 cx.simulate_keystrokes("v i a");
2058 cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2059
2060 // Function arguments
2061 cx.set_state(
2062 "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2063 Mode::Normal,
2064 );
2065 cx.simulate_keystrokes("d a a");
2066 cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2067
2068 cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2069 cx.simulate_keystrokes("v a a");
2070 cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2071
2072 // Tuple, vec, and array arguments
2073 cx.set_state(
2074 "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2075 Mode::Normal,
2076 );
2077 cx.simulate_keystrokes("c i a");
2078 cx.assert_state(
2079 "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2080 Mode::Insert,
2081 );
2082
2083 cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2084 cx.simulate_keystrokes("c a a");
2085 cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2086
2087 cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2088 cx.simulate_keystrokes("c i a");
2089 cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2090
2091 cx.set_state(
2092 "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2093 Mode::Normal,
2094 );
2095 cx.simulate_keystrokes("c a a");
2096 cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2097
2098 // Cursor immediately before / after brackets
2099 cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2100 cx.simulate_keystrokes("v i a");
2101 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2102
2103 cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2104 cx.simulate_keystrokes("v i a");
2105 cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2106 }
2107
2108 #[gpui::test]
2109 async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2110 let mut cx = VimTestContext::new(cx, true).await;
2111
2112 // Base use case
2113 cx.set_state(
2114 indoc! {"
2115 fn boop() {
2116 // Comment
2117 baz();ˇ
2118
2119 loop {
2120 bar(1);
2121 bar(2);
2122 }
2123
2124 result
2125 }
2126 "},
2127 Mode::Normal,
2128 );
2129 cx.simulate_keystrokes("v i i");
2130 cx.assert_state(
2131 indoc! {"
2132 fn boop() {
2133 « // Comment
2134 baz();
2135
2136 loop {
2137 bar(1);
2138 bar(2);
2139 }
2140
2141 resultˇ»
2142 }
2143 "},
2144 Mode::Visual,
2145 );
2146
2147 // Around indent (include line above)
2148 cx.set_state(
2149 indoc! {"
2150 const ABOVE: str = true;
2151 fn boop() {
2152
2153 hello();
2154 worˇld()
2155 }
2156 "},
2157 Mode::Normal,
2158 );
2159 cx.simulate_keystrokes("v a i");
2160 cx.assert_state(
2161 indoc! {"
2162 const ABOVE: str = true;
2163 «fn boop() {
2164
2165 hello();
2166 world()ˇ»
2167 }
2168 "},
2169 Mode::Visual,
2170 );
2171
2172 // Around indent (include line above & below)
2173 cx.set_state(
2174 indoc! {"
2175 const ABOVE: str = true;
2176 fn boop() {
2177 hellˇo();
2178 world()
2179
2180 }
2181 const BELOW: str = true;
2182 "},
2183 Mode::Normal,
2184 );
2185 cx.simulate_keystrokes("c a shift-i");
2186 cx.assert_state(
2187 indoc! {"
2188 const ABOVE: str = true;
2189 ˇ
2190 const BELOW: str = true;
2191 "},
2192 Mode::Insert,
2193 );
2194 }
2195
2196 #[gpui::test]
2197 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2198 let mut cx = NeovimBackedTestContext::new(cx).await;
2199
2200 for (start, end) in SURROUNDING_OBJECTS {
2201 let marked_string = SURROUNDING_MARKER_STRING
2202 .replace('`', &start.to_string())
2203 .replace('\'', &end.to_string());
2204
2205 cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2206 .await
2207 .assert_matches();
2208 cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2209 .await
2210 .assert_matches();
2211 cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2212 .await
2213 .assert_matches();
2214 cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2215 .await
2216 .assert_matches();
2217 }
2218 }
2219
2220 #[gpui::test]
2221 async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2222 let mut cx = VimTestContext::new_typescript(cx).await;
2223
2224 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2225 // Special cases from mini.ai plugin
2226 // the false string in the middle should not be considered
2227 (
2228 "c i q",
2229 "'first' false ˇstring 'second'",
2230 "'first' false string 'ˇ'",
2231 Mode::Insert,
2232 ),
2233 // Multiline support :)! Same behavior as mini.ai plugin
2234 (
2235 "c i q",
2236 indoc! {"
2237 `
2238 first
2239 middle ˇstring
2240 second
2241 `
2242 "},
2243 indoc! {"
2244 `ˇ`
2245 "},
2246 Mode::Insert,
2247 ),
2248 // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2249 // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in AnyQuotes :)
2250 // Bug reference: https://github.com/zed-industries/zed/issues/23889
2251 ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2252 // Single quotes
2253 (
2254 "c i q",
2255 "Thisˇ is a 'quote' example.",
2256 "This is a 'ˇ' example.",
2257 Mode::Insert,
2258 ),
2259 (
2260 "c a q",
2261 "Thisˇ is a 'quote' example.",
2262 "This is a ˇ example.", // same mini.ai plugin behavior
2263 Mode::Insert,
2264 ),
2265 (
2266 "c i q",
2267 "This is a \"simple 'qˇuote'\" example.",
2268 "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2269 Mode::Insert,
2270 ),
2271 (
2272 "c a q",
2273 "This is a \"simple 'qˇuote'\" example.",
2274 "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2275 Mode::Insert,
2276 ),
2277 (
2278 "c i q",
2279 "This is a 'qˇuote' example.",
2280 "This is a 'ˇ' example.",
2281 Mode::Insert,
2282 ),
2283 (
2284 "c a q",
2285 "This is a 'qˇuote' example.",
2286 "This is a ˇ example.", // same mini.ai plugin behavior
2287 Mode::Insert,
2288 ),
2289 (
2290 "d i q",
2291 "This is a 'qˇuote' example.",
2292 "This is a 'ˇ' example.",
2293 Mode::Normal,
2294 ),
2295 (
2296 "d a q",
2297 "This is a 'qˇuote' example.",
2298 "This is a ˇ example.", // same mini.ai plugin behavior
2299 Mode::Normal,
2300 ),
2301 // Double quotes
2302 (
2303 "c i q",
2304 "This is a \"qˇuote\" example.",
2305 "This is a \"ˇ\" example.",
2306 Mode::Insert,
2307 ),
2308 (
2309 "c a q",
2310 "This is a \"qˇuote\" example.",
2311 "This is a ˇ example.", // same mini.ai plugin behavior
2312 Mode::Insert,
2313 ),
2314 (
2315 "d i q",
2316 "This is a \"qˇuote\" example.",
2317 "This is a \"ˇ\" example.",
2318 Mode::Normal,
2319 ),
2320 (
2321 "d a q",
2322 "This is a \"qˇuote\" example.",
2323 "This is a ˇ example.", // same mini.ai plugin behavior
2324 Mode::Normal,
2325 ),
2326 // Back quotes
2327 (
2328 "c i q",
2329 "This is a `qˇuote` example.",
2330 "This is a `ˇ` example.",
2331 Mode::Insert,
2332 ),
2333 (
2334 "c a q",
2335 "This is a `qˇuote` example.",
2336 "This is a ˇ example.", // same mini.ai plugin behavior
2337 Mode::Insert,
2338 ),
2339 (
2340 "d i q",
2341 "This is a `qˇuote` example.",
2342 "This is a `ˇ` example.",
2343 Mode::Normal,
2344 ),
2345 (
2346 "d a q",
2347 "This is a `qˇuote` example.",
2348 "This is a ˇ example.", // same mini.ai plugin behavior
2349 Mode::Normal,
2350 ),
2351 ];
2352
2353 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2354 cx.set_state(initial_state, Mode::Normal);
2355
2356 cx.simulate_keystrokes(keystrokes);
2357
2358 cx.assert_state(expected_state, *expected_mode);
2359 }
2360
2361 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2362 ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2363 ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2364 ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2365 ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2366 ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2367 ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2368 ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2369 ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2370 ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2371 ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2372 ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2373 ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2374 ];
2375
2376 for (keystrokes, initial_state, mode) in INVALID_CASES {
2377 cx.set_state(initial_state, Mode::Normal);
2378
2379 cx.simulate_keystrokes(keystrokes);
2380
2381 cx.assert_state(initial_state, *mode);
2382 }
2383 }
2384
2385 #[gpui::test]
2386 async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2387 let mut cx = VimTestContext::new(cx, true).await;
2388 cx.update(|_, cx| {
2389 cx.bind_keys([KeyBinding::new(
2390 "b",
2391 AnyBrackets,
2392 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2393 )]);
2394 });
2395
2396 const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2397 // Special cases from mini.ai plugin
2398 // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
2399 // Same behavior as mini.ai plugin
2400 (
2401 "c i b",
2402 indoc! {"
2403 {
2404 {
2405 ˇprint('hello')
2406 }
2407 }
2408 "},
2409 indoc! {"
2410 {
2411 {
2412 print(ˇ)
2413 }
2414 }
2415 "},
2416 Mode::Insert,
2417 ),
2418 // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
2419 // Same behavior as mini.ai plugin
2420 (
2421 "c i b",
2422 indoc! {"
2423 {
2424 {
2425 ˇ
2426 print('hello')
2427 }
2428 }
2429 "},
2430 indoc! {"
2431 {
2432 {ˇ}
2433 }
2434 "},
2435 Mode::Insert,
2436 ),
2437 // If you are in the open bracket then it has higher priority
2438 (
2439 "c i b",
2440 indoc! {"
2441 «{ˇ»
2442 {
2443 print('hello')
2444 }
2445 }
2446 "},
2447 indoc! {"
2448 {ˇ}
2449 "},
2450 Mode::Insert,
2451 ),
2452 // If you are in the close bracket then it has higher priority
2453 (
2454 "c i b",
2455 indoc! {"
2456 {
2457 {
2458 print('hello')
2459 }
2460 «}ˇ»
2461 "},
2462 indoc! {"
2463 {ˇ}
2464 "},
2465 Mode::Insert,
2466 ),
2467 // Bracket (Parentheses)
2468 (
2469 "c i b",
2470 "Thisˇ is a (simple [quote]) example.",
2471 "This is a (ˇ) example.",
2472 Mode::Insert,
2473 ),
2474 (
2475 "c i b",
2476 "This is a [simple (qˇuote)] example.",
2477 "This is a [simple (ˇ)] example.",
2478 Mode::Insert,
2479 ),
2480 (
2481 "c a b",
2482 "This is a [simple (qˇuote)] example.",
2483 "This is a [simple ˇ] example.",
2484 Mode::Insert,
2485 ),
2486 (
2487 "c a b",
2488 "Thisˇ is a (simple [quote]) example.",
2489 "This is a ˇ example.",
2490 Mode::Insert,
2491 ),
2492 (
2493 "c i b",
2494 "This is a (qˇuote) example.",
2495 "This is a (ˇ) example.",
2496 Mode::Insert,
2497 ),
2498 (
2499 "c a b",
2500 "This is a (qˇuote) example.",
2501 "This is a ˇ example.",
2502 Mode::Insert,
2503 ),
2504 (
2505 "d i b",
2506 "This is a (qˇuote) example.",
2507 "This is a (ˇ) example.",
2508 Mode::Normal,
2509 ),
2510 (
2511 "d a b",
2512 "This is a (qˇuote) example.",
2513 "This is a ˇ example.",
2514 Mode::Normal,
2515 ),
2516 // Square brackets
2517 (
2518 "c i b",
2519 "This is a [qˇuote] example.",
2520 "This is a [ˇ] example.",
2521 Mode::Insert,
2522 ),
2523 (
2524 "c a b",
2525 "This is a [qˇuote] example.",
2526 "This is a ˇ example.",
2527 Mode::Insert,
2528 ),
2529 (
2530 "d i b",
2531 "This is a [qˇuote] example.",
2532 "This is a [ˇ] example.",
2533 Mode::Normal,
2534 ),
2535 (
2536 "d a b",
2537 "This is a [qˇuote] example.",
2538 "This is a ˇ example.",
2539 Mode::Normal,
2540 ),
2541 // Curly brackets
2542 (
2543 "c i b",
2544 "This is a {qˇuote} example.",
2545 "This is a {ˇ} example.",
2546 Mode::Insert,
2547 ),
2548 (
2549 "c a b",
2550 "This is a {qˇuote} example.",
2551 "This is a ˇ example.",
2552 Mode::Insert,
2553 ),
2554 (
2555 "d i b",
2556 "This is a {qˇuote} example.",
2557 "This is a {ˇ} example.",
2558 Mode::Normal,
2559 ),
2560 (
2561 "d a b",
2562 "This is a {qˇuote} example.",
2563 "This is a ˇ example.",
2564 Mode::Normal,
2565 ),
2566 ];
2567
2568 for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2569 cx.set_state(initial_state, Mode::Normal);
2570
2571 cx.simulate_keystrokes(keystrokes);
2572
2573 cx.assert_state(expected_state, *expected_mode);
2574 }
2575
2576 const INVALID_CASES: &[(&str, &str, Mode)] = &[
2577 ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2578 ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2579 ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2580 ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2581 ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2582 ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2583 ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2584 ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2585 ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2586 ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2587 ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2588 ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2589 ];
2590
2591 for (keystrokes, initial_state, mode) in INVALID_CASES {
2592 cx.set_state(initial_state, Mode::Normal);
2593
2594 cx.simulate_keystrokes(keystrokes);
2595
2596 cx.assert_state(initial_state, *mode);
2597 }
2598 }
2599
2600 #[gpui::test]
2601 async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
2602 let mut cx = NeovimBackedTestContext::new(cx).await;
2603
2604 cx.set_shared_state("(trailingˇ whitespace )")
2605 .await;
2606 cx.simulate_shared_keystrokes("v i b").await;
2607 cx.shared_state().await.assert_matches();
2608 cx.simulate_shared_keystrokes("escape y i b").await;
2609 cx.shared_clipboard()
2610 .await
2611 .assert_eq("trailing whitespace ");
2612 }
2613
2614 #[gpui::test]
2615 async fn test_tags(cx: &mut gpui::TestAppContext) {
2616 let mut cx = VimTestContext::new_html(cx).await;
2617
2618 cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
2619 cx.simulate_keystrokes("v i t");
2620 cx.assert_state(
2621 "<html><head></head><body><b>«hi!ˇ»</b></body>",
2622 Mode::Visual,
2623 );
2624 cx.simulate_keystrokes("a t");
2625 cx.assert_state(
2626 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2627 Mode::Visual,
2628 );
2629 cx.simulate_keystrokes("a t");
2630 cx.assert_state(
2631 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2632 Mode::Visual,
2633 );
2634
2635 // The cursor is before the tag
2636 cx.set_state(
2637 "<html><head></head><body> ˇ <b>hi!</b></body>",
2638 Mode::Normal,
2639 );
2640 cx.simulate_keystrokes("v i t");
2641 cx.assert_state(
2642 "<html><head></head><body> <b>«hi!ˇ»</b></body>",
2643 Mode::Visual,
2644 );
2645 cx.simulate_keystrokes("a t");
2646 cx.assert_state(
2647 "<html><head></head><body> «<b>hi!</b>ˇ»</body>",
2648 Mode::Visual,
2649 );
2650
2651 // The cursor is in the open tag
2652 cx.set_state(
2653 "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
2654 Mode::Normal,
2655 );
2656 cx.simulate_keystrokes("v a t");
2657 cx.assert_state(
2658 "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
2659 Mode::Visual,
2660 );
2661 cx.simulate_keystrokes("i t");
2662 cx.assert_state(
2663 "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
2664 Mode::Visual,
2665 );
2666
2667 // current selection length greater than 1
2668 cx.set_state(
2669 "<html><head></head><body><«b>hi!ˇ»</b></body>",
2670 Mode::Visual,
2671 );
2672 cx.simulate_keystrokes("i t");
2673 cx.assert_state(
2674 "<html><head></head><body><b>«hi!ˇ»</b></body>",
2675 Mode::Visual,
2676 );
2677 cx.simulate_keystrokes("a t");
2678 cx.assert_state(
2679 "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2680 Mode::Visual,
2681 );
2682
2683 cx.set_state(
2684 "<html><head></head><body><«b>hi!</ˇ»b></body>",
2685 Mode::Visual,
2686 );
2687 cx.simulate_keystrokes("a t");
2688 cx.assert_state(
2689 "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2690 Mode::Visual,
2691 );
2692 }
2693 #[gpui::test]
2694 async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
2695 let mut cx = NeovimBackedTestContext::new(cx).await;
2696
2697 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
2698 .await;
2699 cx.simulate_shared_keystrokes("v a w").await;
2700 cx.shared_state()
2701 .await
2702 .assert_eq(" «const ˇ»f = (x: unknown) => {");
2703
2704 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
2705 .await;
2706 cx.simulate_shared_keystrokes("y a w").await;
2707 cx.shared_clipboard().await.assert_eq("const ");
2708
2709 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
2710 .await;
2711 cx.simulate_shared_keystrokes("d a w").await;
2712 cx.shared_state()
2713 .await
2714 .assert_eq(" ˇf = (x: unknown) => {");
2715 cx.shared_clipboard().await.assert_eq("const ");
2716
2717 cx.set_shared_state(" ˇconst f = (x: unknown) => {")
2718 .await;
2719 cx.simulate_shared_keystrokes("c a w").await;
2720 cx.shared_state()
2721 .await
2722 .assert_eq(" ˇf = (x: unknown) => {");
2723 cx.shared_clipboard().await.assert_eq("const ");
2724 }
2725}