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