1use std::ops::Range;
2
3use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
4use gpui::{actions, impl_actions, AppContext, WindowContext};
5use language::Selection;
6use serde::Deserialize;
7use workspace::Workspace;
8
9use crate::{motion::right, normal::normal_object, state::Mode, visual::visual_object, Vim};
10
11#[derive(Copy, Clone, Debug, PartialEq)]
12pub enum Object {
13 Word { ignore_punctuation: bool },
14 Sentence,
15 Quotes,
16 BackQuotes,
17 DoubleQuotes,
18 Parentheses,
19 SquareBrackets,
20 CurlyBrackets,
21 AngleBrackets,
22}
23
24#[derive(Clone, Deserialize, PartialEq)]
25#[serde(rename_all = "camelCase")]
26struct Word {
27 #[serde(default)]
28 ignore_punctuation: bool,
29}
30
31actions!(
32 vim,
33 [
34 Sentence,
35 Quotes,
36 BackQuotes,
37 DoubleQuotes,
38 Parentheses,
39 SquareBrackets,
40 CurlyBrackets,
41 AngleBrackets
42 ]
43);
44impl_actions!(vim, [Word]);
45
46pub fn init(cx: &mut AppContext) {
47 cx.add_action(
48 |_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
49 object(Object::Word { ignore_punctuation }, cx)
50 },
51 );
52 cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
53 cx.add_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
54 cx.add_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
55 cx.add_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| object(Object::DoubleQuotes, cx));
56 cx.add_action(|_: &mut Workspace, _: &Parentheses, cx: _| object(Object::Parentheses, cx));
57 cx.add_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
58 object(Object::SquareBrackets, cx)
59 });
60 cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
61 cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
62}
63
64fn object(object: Object, cx: &mut WindowContext) {
65 match Vim::read(cx).state().mode {
66 Mode::Normal => normal_object(object, cx),
67 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
68 Mode::Insert => {
69 // Shouldn't execute a text object in insert mode. Ignoring
70 }
71 }
72}
73
74impl Object {
75 pub fn is_multiline(self) -> bool {
76 match self {
77 Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => {
78 false
79 }
80 Object::Sentence
81 | Object::Parentheses
82 | Object::AngleBrackets
83 | Object::CurlyBrackets
84 | Object::SquareBrackets => true,
85 }
86 }
87 pub fn range(
88 self,
89 map: &DisplaySnapshot,
90 relative_to: DisplayPoint,
91 around: bool,
92 ) -> Option<Range<DisplayPoint>> {
93 match self {
94 Object::Word { ignore_punctuation } => {
95 if around {
96 around_word(map, relative_to, ignore_punctuation)
97 } else {
98 in_word(map, relative_to, ignore_punctuation)
99 }
100 }
101 Object::Sentence => sentence(map, relative_to, around),
102 Object::Quotes => {
103 surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
104 }
105 Object::BackQuotes => {
106 surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
107 }
108 Object::DoubleQuotes => {
109 surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
110 }
111 Object::Parentheses => {
112 surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
113 }
114 Object::SquareBrackets => {
115 surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
116 }
117 Object::CurlyBrackets => {
118 surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
119 }
120 Object::AngleBrackets => {
121 surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
122 }
123 }
124 }
125
126 pub fn expand_selection(
127 self,
128 map: &DisplaySnapshot,
129 selection: &mut Selection<DisplayPoint>,
130 around: bool,
131 ) -> bool {
132 if let Some(range) = self.range(map, selection.head(), around) {
133 selection.start = range.start;
134 selection.end = range.end;
135 true
136 } else {
137 false
138 }
139 }
140}
141
142/// Return a range that surrounds the word relative_to is in
143/// If relative_to is at the start of a word, return the word.
144/// If relative_to is between words, return the space between
145fn in_word(
146 map: &DisplaySnapshot,
147 relative_to: DisplayPoint,
148 ignore_punctuation: bool,
149) -> Option<Range<DisplayPoint>> {
150 // Use motion::right so that we consider the character under the cursor when looking for the start
151 let start = movement::find_preceding_boundary_in_line(
152 map,
153 right(map, relative_to, 1),
154 |left, right| {
155 char_kind(left).coerce_punctuation(ignore_punctuation)
156 != char_kind(right).coerce_punctuation(ignore_punctuation)
157 },
158 );
159 let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
160 char_kind(left).coerce_punctuation(ignore_punctuation)
161 != char_kind(right).coerce_punctuation(ignore_punctuation)
162 });
163
164 Some(start..end)
165}
166
167/// Return a range that surrounds the word and following whitespace
168/// relative_to is in.
169/// If relative_to is at the start of a word, return the word and following whitespace.
170/// If relative_to is between words, return the whitespace back and the following word
171
172/// if in word
173/// delete that word
174/// if there is whitespace following the word, delete that as well
175/// otherwise, delete any preceding whitespace
176/// otherwise
177/// delete whitespace around cursor
178/// delete word following the cursor
179fn around_word(
180 map: &DisplaySnapshot,
181 relative_to: DisplayPoint,
182 ignore_punctuation: bool,
183) -> Option<Range<DisplayPoint>> {
184 let in_word = map
185 .chars_at(relative_to)
186 .next()
187 .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
188 .unwrap_or(false);
189
190 if in_word {
191 around_containing_word(map, relative_to, ignore_punctuation)
192 } else {
193 around_next_word(map, relative_to, ignore_punctuation)
194 }
195}
196
197fn around_containing_word(
198 map: &DisplaySnapshot,
199 relative_to: DisplayPoint,
200 ignore_punctuation: bool,
201) -> Option<Range<DisplayPoint>> {
202 in_word(map, relative_to, ignore_punctuation)
203 .map(|range| expand_to_include_whitespace(map, range, true))
204}
205
206fn around_next_word(
207 map: &DisplaySnapshot,
208 relative_to: DisplayPoint,
209 ignore_punctuation: bool,
210) -> Option<Range<DisplayPoint>> {
211 // Get the start of the word
212 let start = movement::find_preceding_boundary_in_line(
213 map,
214 right(map, relative_to, 1),
215 |left, right| {
216 char_kind(left).coerce_punctuation(ignore_punctuation)
217 != char_kind(right).coerce_punctuation(ignore_punctuation)
218 },
219 );
220
221 let mut word_found = false;
222 let end = movement::find_boundary(map, relative_to, |left, right| {
223 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
224 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
225
226 let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
227
228 if right_kind != CharKind::Whitespace {
229 word_found = true;
230 }
231
232 found
233 });
234
235 Some(start..end)
236}
237
238fn sentence(
239 map: &DisplaySnapshot,
240 relative_to: DisplayPoint,
241 around: bool,
242) -> Option<Range<DisplayPoint>> {
243 let mut start = None;
244 let mut previous_end = relative_to;
245
246 let mut chars = map.chars_at(relative_to).peekable();
247
248 // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
249 for (char, point) in chars
250 .peek()
251 .cloned()
252 .into_iter()
253 .chain(map.reverse_chars_at(relative_to))
254 {
255 if is_sentence_end(map, point) {
256 break;
257 }
258
259 if is_possible_sentence_start(char) {
260 start = Some(point);
261 }
262
263 previous_end = point;
264 }
265
266 // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
267 let mut end = relative_to;
268 for (char, point) in chars {
269 if start.is_none() && is_possible_sentence_start(char) {
270 if around {
271 start = Some(point);
272 continue;
273 } else {
274 end = point;
275 break;
276 }
277 }
278
279 end = point;
280 *end.column_mut() += char.len_utf8() as u32;
281 end = map.clip_point(end, Bias::Left);
282
283 if is_sentence_end(map, end) {
284 break;
285 }
286 }
287
288 let mut range = start.unwrap_or(previous_end)..end;
289 if around {
290 range = expand_to_include_whitespace(map, range, false);
291 }
292
293 Some(range)
294}
295
296fn is_possible_sentence_start(character: char) -> bool {
297 !character.is_whitespace() && character != '.'
298}
299
300const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
301const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
302const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
303fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
304 let mut next_chars = map.chars_at(point).peekable();
305 if let Some((char, _)) = next_chars.next() {
306 // We are at a double newline. This position is a sentence end.
307 if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
308 return true;
309 }
310
311 // The next text is not a valid whitespace. This is not a sentence end
312 if !SENTENCE_END_WHITESPACE.contains(&char) {
313 return false;
314 }
315 }
316
317 for (char, _) in map.reverse_chars_at(point) {
318 if SENTENCE_END_PUNCTUATION.contains(&char) {
319 return true;
320 }
321
322 if !SENTENCE_END_FILLERS.contains(&char) {
323 return false;
324 }
325 }
326
327 return false;
328}
329
330/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
331/// whitespace to the end first and falls back to the start if there was none.
332fn expand_to_include_whitespace(
333 map: &DisplaySnapshot,
334 mut range: Range<DisplayPoint>,
335 stop_at_newline: bool,
336) -> Range<DisplayPoint> {
337 let mut whitespace_included = false;
338
339 let mut chars = map.chars_at(range.end).peekable();
340 while let Some((char, point)) = chars.next() {
341 if char == '\n' && stop_at_newline {
342 break;
343 }
344
345 if char.is_whitespace() {
346 // Set end to the next display_point or the character position after the current display_point
347 range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
348 let mut end = point;
349 *end.column_mut() += char.len_utf8() as u32;
350 map.clip_point(end, Bias::Left)
351 });
352
353 if char != '\n' {
354 whitespace_included = true;
355 }
356 } else {
357 // Found non whitespace. Quit out.
358 break;
359 }
360 }
361
362 if !whitespace_included {
363 for (char, point) in map.reverse_chars_at(range.start) {
364 if char == '\n' && stop_at_newline {
365 break;
366 }
367
368 if !char.is_whitespace() {
369 break;
370 }
371
372 range.start = point;
373 }
374 }
375
376 range
377}
378
379fn surrounding_markers(
380 map: &DisplaySnapshot,
381 relative_to: DisplayPoint,
382 around: bool,
383 search_across_lines: bool,
384 start_marker: char,
385 end_marker: char,
386) -> Option<Range<DisplayPoint>> {
387 let mut matched_ends = 0;
388 let mut start = None;
389 for (char, mut point) in map.reverse_chars_at(relative_to) {
390 if char == start_marker {
391 if matched_ends > 0 {
392 matched_ends -= 1;
393 } else {
394 if around {
395 start = Some(point)
396 } else {
397 *point.column_mut() += char.len_utf8() as u32;
398 start = Some(point)
399 }
400 break;
401 }
402 } else if char == end_marker {
403 matched_ends += 1;
404 } else if char == '\n' && !search_across_lines {
405 break;
406 }
407 }
408
409 let mut matched_starts = 0;
410 let mut end = None;
411 for (char, mut point) in map.chars_at(relative_to) {
412 if char == end_marker {
413 if start.is_none() {
414 break;
415 }
416
417 if matched_starts > 0 {
418 matched_starts -= 1;
419 } else {
420 if around {
421 *point.column_mut() += char.len_utf8() as u32;
422 end = Some(point);
423 } else {
424 end = Some(point);
425 }
426
427 break;
428 }
429 }
430
431 if char == start_marker {
432 if start.is_none() {
433 if around {
434 start = Some(point);
435 } else {
436 *point.column_mut() += char.len_utf8() as u32;
437 start = Some(point);
438 }
439 } else {
440 matched_starts += 1;
441 }
442 }
443
444 if char == '\n' && !search_across_lines {
445 break;
446 }
447 }
448
449 let (Some(mut start), Some(mut end)) = (start, end) else {
450 return None;
451 };
452
453 if !around {
454 // if a block starts with a newline, move the start to after the newline.
455 let mut was_newline = false;
456 for (char, point) in map.chars_at(start) {
457 if was_newline {
458 start = point;
459 } else if char == '\n' {
460 was_newline = true;
461 continue;
462 }
463 break;
464 }
465 // if a block ends with a newline, then whitespace, then the delimeter,
466 // move the end to after the newline.
467 let mut new_end = end;
468 for (char, point) in map.reverse_chars_at(end) {
469 if char == '\n' {
470 end = new_end;
471 break;
472 }
473 if !char.is_whitespace() {
474 break;
475 }
476 new_end = point
477 }
478 }
479
480 Some(start..end)
481}
482
483#[cfg(test)]
484mod test {
485 use indoc::indoc;
486
487 use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
488
489 const WORD_LOCATIONS: &'static str = indoc! {"
490 The quick ˇbrowˇnˇ•••
491 fox ˇjuˇmpsˇ over
492 the lazy dogˇ••
493 ˇ
494 ˇ
495 ˇ
496 Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
497 ˇ••
498 ˇ••
499 ˇ fox-jumpˇs over
500 the lazy dogˇ•
501 ˇ
502 "
503 };
504
505 #[gpui::test]
506 async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
507 let mut cx = NeovimBackedTestContext::new(cx).await;
508
509 cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
510 .await;
511 cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
512 .await;
513 cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
514 .await;
515 cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
516 .await;
517 }
518
519 #[gpui::test]
520 async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
521 let mut cx = NeovimBackedTestContext::new(cx).await;
522
523 cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
524 .await;
525 cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
526 .await;
527 cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
528 .await;
529 cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
530 .await;
531 }
532
533 #[gpui::test]
534 async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
535 let mut cx = NeovimBackedTestContext::new(cx).await;
536
537 cx.set_shared_state("The quick ˇbrown\nfox").await;
538 cx.simulate_shared_keystrokes(["v"]).await;
539 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
540 cx.simulate_shared_keystrokes(["i", "w"]).await;
541 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
542
543 cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
544 .await;
545 cx.assert_binding_matches_all_exempted(
546 ["v", "h", "i", "w"],
547 WORD_LOCATIONS,
548 ExemptionFeatures::NonEmptyVisualTextObjects,
549 )
550 .await;
551 cx.assert_binding_matches_all_exempted(
552 ["v", "l", "i", "w"],
553 WORD_LOCATIONS,
554 ExemptionFeatures::NonEmptyVisualTextObjects,
555 )
556 .await;
557 cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
558 .await;
559
560 cx.assert_binding_matches_all_exempted(
561 ["v", "i", "h", "shift-w"],
562 WORD_LOCATIONS,
563 ExemptionFeatures::NonEmptyVisualTextObjects,
564 )
565 .await;
566 cx.assert_binding_matches_all_exempted(
567 ["v", "i", "l", "shift-w"],
568 WORD_LOCATIONS,
569 ExemptionFeatures::NonEmptyVisualTextObjects,
570 )
571 .await;
572
573 cx.assert_binding_matches_all_exempted(
574 ["v", "a", "w"],
575 WORD_LOCATIONS,
576 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
577 )
578 .await;
579 cx.assert_binding_matches_all_exempted(
580 ["v", "a", "shift-w"],
581 WORD_LOCATIONS,
582 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
583 )
584 .await;
585 }
586
587 const SENTENCE_EXAMPLES: &[&'static str] = &[
588 "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
589 indoc! {"
590 ˇThe quick ˇbrownˇ
591 fox jumps over
592 the lazy doˇgˇ.ˇ ˇThe quick ˇ
593 brown fox jumps over
594 "},
595 indoc! {"
596 The quick brown fox jumps.
597 Over the lazy dog
598 ˇ
599 ˇ
600 ˇ fox-jumpˇs over
601 the lazy dog.ˇ
602 ˇ
603 "},
604 r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
605 ];
606
607 #[gpui::test]
608 async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
609 let mut cx = NeovimBackedTestContext::new(cx)
610 .await
611 .binding(["c", "i", "s"]);
612 cx.add_initial_state_exemptions(
613 "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n fox-jumps over\nthe lazy dog.\n\n",
614 ExemptionFeatures::SentenceOnEmptyLines);
615 cx.add_initial_state_exemptions(
616 "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
617 ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
618 cx.add_initial_state_exemptions(
619 "The quick brown fox jumps.\nOver the lazy dog\n\n\n fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
620 ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
621 for sentence_example in SENTENCE_EXAMPLES {
622 cx.assert_all(sentence_example).await;
623 }
624
625 let mut cx = cx.binding(["c", "a", "s"]);
626 cx.add_initial_state_exemptions(
627 "The quick brown?ˇ Fox Jumps! Over the lazy.",
628 ExemptionFeatures::IncorrectLandingPosition,
629 );
630 cx.add_initial_state_exemptions(
631 "The quick brown.)]\'\" Brown fox jumps.ˇ ",
632 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
633 );
634
635 for sentence_example in SENTENCE_EXAMPLES {
636 cx.assert_all(sentence_example).await;
637 }
638 }
639
640 #[gpui::test]
641 async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
642 let mut cx = NeovimBackedTestContext::new(cx)
643 .await
644 .binding(["d", "i", "s"]);
645 cx.add_initial_state_exemptions(
646 "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n fox-jumps over\nthe lazy dog.\n\n",
647 ExemptionFeatures::SentenceOnEmptyLines);
648 cx.add_initial_state_exemptions(
649 "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
650 ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
651 cx.add_initial_state_exemptions(
652 "The quick brown fox jumps.\nOver the lazy dog\n\n\n fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
653 ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
654
655 for sentence_example in SENTENCE_EXAMPLES {
656 cx.assert_all(sentence_example).await;
657 }
658
659 let mut cx = cx.binding(["d", "a", "s"]);
660 cx.add_initial_state_exemptions(
661 "The quick brown?ˇ Fox Jumps! Over the lazy.",
662 ExemptionFeatures::IncorrectLandingPosition,
663 );
664 cx.add_initial_state_exemptions(
665 "The quick brown.)]\'\" Brown fox jumps.ˇ ",
666 ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
667 );
668
669 for sentence_example in SENTENCE_EXAMPLES {
670 cx.assert_all(sentence_example).await;
671 }
672 }
673
674 #[gpui::test]
675 async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
676 let mut cx = NeovimBackedTestContext::new(cx)
677 .await
678 .binding(["v", "i", "s"]);
679 for sentence_example in SENTENCE_EXAMPLES {
680 cx.assert_all_exempted(sentence_example, ExemptionFeatures::SentenceOnEmptyLines)
681 .await;
682 }
683
684 let mut cx = cx.binding(["v", "a", "s"]);
685 for sentence_example in SENTENCE_EXAMPLES {
686 cx.assert_all_exempted(
687 sentence_example,
688 ExemptionFeatures::AroundSentenceStartingBetweenIncludesWrongWhitespace,
689 )
690 .await;
691 }
692 }
693
694 // Test string with "`" for opening surrounders and "'" for closing surrounders
695 const SURROUNDING_MARKER_STRING: &str = indoc! {"
696 ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
697 'ˇfox juˇmps ovˇ`ˇer
698 the ˇlazy dˇ'ˇoˇ`ˇg"};
699
700 const SURROUNDING_OBJECTS: &[(char, char)] = &[
701 ('\'', '\''), // Quote
702 ('`', '`'), // Back Quote
703 ('"', '"'), // Double Quote
704 ('(', ')'), // Parentheses
705 ('[', ']'), // SquareBrackets
706 ('{', '}'), // CurlyBrackets
707 ('<', '>'), // AngleBrackets
708 ];
709
710 #[gpui::test]
711 async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
712 let mut cx = NeovimBackedTestContext::new(cx).await;
713
714 for (start, end) in SURROUNDING_OBJECTS {
715 if ((start == &'\'' || start == &'`' || start == &'"')
716 && !ExemptionFeatures::QuotesSeekForward.supported())
717 || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
718 {
719 continue;
720 }
721
722 let marked_string = SURROUNDING_MARKER_STRING
723 .replace('`', &start.to_string())
724 .replace('\'', &end.to_string());
725
726 cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
727 .await;
728 cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
729 .await;
730 cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
731 .await;
732 cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
733 .await;
734 }
735 }
736
737 #[gpui::test]
738 async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
739 let mut cx = NeovimBackedTestContext::new(cx).await;
740
741 cx.set_shared_state(indoc! {
742 "func empty(a string) bool {
743 if a == \"\" {
744 return true
745 }
746 ˇreturn false
747 }"
748 })
749 .await;
750 cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
751 cx.assert_shared_state(indoc! {"
752 func empty(a string) bool {
753 « if a == \"\" {
754 return true
755 }
756 return false
757 ˇ»}"})
758 .await;
759 cx.set_shared_state(indoc! {
760 "func empty(a string) bool {
761 if a == \"\" {
762 ˇreturn true
763 }
764 return false
765 }"
766 })
767 .await;
768 cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
769 cx.assert_shared_state(indoc! {"
770 func empty(a string) bool {
771 if a == \"\" {
772 « return true
773 ˇ» }
774 return false
775 }"})
776 .await;
777 }
778
779 #[gpui::test]
780 async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
781 let mut cx = NeovimBackedTestContext::new(cx).await;
782
783 for (start, end) in SURROUNDING_OBJECTS {
784 if ((start == &'\'' || start == &'`' || start == &'"')
785 && !ExemptionFeatures::QuotesSeekForward.supported())
786 || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
787 {
788 continue;
789 }
790 let marked_string = SURROUNDING_MARKER_STRING
791 .replace('`', &start.to_string())
792 .replace('\'', &end.to_string());
793
794 cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
795 .await;
796 cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
797 .await;
798 cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
799 .await;
800 cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
801 .await;
802 }
803 }
804}