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