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