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