1use std::{iter::Peekable, str::Chars, time::Duration};
2
3use editor::Editor;
4use gpui::{actions, impl_actions, ViewContext};
5use language::Point;
6use search::{buffer_search, BufferSearchBar, SearchOptions};
7use serde_derive::Deserialize;
8use util::serde::default_true;
9use workspace::{notifications::NotifyResultExt, searchable::Direction};
10
11use crate::{
12 command::CommandRange,
13 motion::Motion,
14 state::{Mode, SearchState},
15 Vim,
16};
17
18#[derive(Clone, Deserialize, PartialEq)]
19#[serde(rename_all = "camelCase")]
20pub(crate) struct MoveToNext {
21 #[serde(default = "default_true")]
22 case_sensitive: bool,
23 #[serde(default)]
24 partial_word: bool,
25 #[serde(default = "default_true")]
26 regex: bool,
27}
28
29#[derive(Clone, Deserialize, PartialEq)]
30#[serde(rename_all = "camelCase")]
31pub(crate) struct MoveToPrev {
32 #[serde(default = "default_true")]
33 case_sensitive: bool,
34 #[serde(default)]
35 partial_word: bool,
36 #[serde(default = "default_true")]
37 regex: bool,
38}
39
40#[derive(Clone, Deserialize, PartialEq)]
41pub(crate) struct Search {
42 #[serde(default)]
43 backwards: bool,
44}
45
46#[derive(Debug, Clone, PartialEq, Deserialize)]
47pub struct FindCommand {
48 pub query: String,
49 pub backwards: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Deserialize)]
53pub struct ReplaceCommand {
54 pub(crate) range: CommandRange,
55 pub(crate) replacement: Replacement,
56}
57
58#[derive(Debug, Default, PartialEq, Deserialize, Clone)]
59pub(crate) struct Replacement {
60 search: String,
61 replacement: String,
62 should_replace_all: bool,
63 is_case_sensitive: bool,
64}
65
66actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
67impl_actions!(
68 vim,
69 [FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
70);
71
72pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
73 Vim::action(editor, cx, Vim::move_to_next);
74 Vim::action(editor, cx, Vim::move_to_prev);
75 Vim::action(editor, cx, Vim::move_to_next_match);
76 Vim::action(editor, cx, Vim::move_to_prev_match);
77 Vim::action(editor, cx, Vim::search);
78 Vim::action(editor, cx, Vim::search_deploy);
79 Vim::action(editor, cx, Vim::find_command);
80 Vim::action(editor, cx, Vim::replace_command);
81}
82
83impl Vim {
84 fn move_to_next(&mut self, action: &MoveToNext, cx: &mut ViewContext<Self>) {
85 self.move_to_internal(
86 Direction::Next,
87 action.case_sensitive,
88 !action.partial_word,
89 action.regex,
90 cx,
91 )
92 }
93
94 fn move_to_prev(&mut self, action: &MoveToPrev, cx: &mut ViewContext<Self>) {
95 self.move_to_internal(
96 Direction::Prev,
97 action.case_sensitive,
98 !action.partial_word,
99 action.regex,
100 cx,
101 )
102 }
103
104 fn move_to_next_match(&mut self, _: &MoveToNextMatch, cx: &mut ViewContext<Self>) {
105 self.move_to_match_internal(self.search.direction, cx)
106 }
107
108 fn move_to_prev_match(&mut self, _: &MoveToPrevMatch, cx: &mut ViewContext<Self>) {
109 self.move_to_match_internal(self.search.direction.opposite(), cx)
110 }
111
112 fn search(&mut self, action: &Search, cx: &mut ViewContext<Self>) {
113 let Some(pane) = self.pane(cx) else { return };
114 let direction = if action.backwards {
115 Direction::Prev
116 } else {
117 Direction::Next
118 };
119 let count = self.take_count(cx).unwrap_or(1);
120 let prior_selections = self.editor_selections(cx);
121 pane.update(cx, |pane, cx| {
122 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
123 search_bar.update(cx, |search_bar, cx| {
124 if !search_bar.show(cx) {
125 return;
126 }
127 let query = search_bar.query(cx);
128
129 search_bar.select_query(cx);
130 cx.focus_self();
131
132 search_bar.set_replacement(None, cx);
133 search_bar.set_search_options(SearchOptions::NONE | SearchOptions::REGEX, cx);
134
135 self.search = SearchState {
136 direction,
137 count,
138 initial_query: query,
139 prior_selections,
140 prior_operator: self.operator_stack.last().cloned(),
141 prior_mode: self.mode,
142 }
143 });
144 }
145 })
146 }
147
148 // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
149 fn search_deploy(&mut self, _: &buffer_search::Deploy, cx: &mut ViewContext<Self>) {
150 self.search = Default::default();
151 cx.propagate();
152 }
153
154 pub fn search_submit(&mut self, cx: &mut ViewContext<Self>) {
155 self.store_visual_marks(cx);
156 let Some(pane) = self.pane(cx) else { return };
157 let result = pane.update(cx, |pane, cx| {
158 let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
159 search_bar.update(cx, |search_bar, cx| {
160 let mut count = self.search.count;
161 let direction = self.search.direction;
162 // in the case that the query has changed, the search bar
163 // will have selected the next match already.
164 if (search_bar.query(cx) != self.search.initial_query)
165 && self.search.direction == Direction::Next
166 {
167 count = count.saturating_sub(1)
168 }
169 self.search.count = 1;
170 search_bar.select_match(direction, count, cx);
171 search_bar.focus_editor(&Default::default(), cx);
172
173 let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
174 let prior_mode = self.search.prior_mode;
175 let prior_operator = self.search.prior_operator.take();
176
177 let query = search_bar.query(cx).into();
178 Vim::globals(cx).registers.insert('/', query);
179 Some((prior_selections, prior_mode, prior_operator))
180 })
181 });
182
183 let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
184 return;
185 };
186
187 let new_selections = self.editor_selections(cx);
188
189 // If the active editor has changed during a search, don't panic.
190 if prior_selections.iter().any(|s| {
191 self.update_editor(cx, |_, editor, cx| {
192 !s.start.is_valid(&editor.snapshot(cx).buffer_snapshot)
193 })
194 .unwrap_or(true)
195 }) {
196 prior_selections.clear();
197 }
198
199 if prior_mode != self.mode {
200 self.switch_mode(prior_mode, true, cx);
201 }
202 if let Some(operator) = prior_operator {
203 self.push_operator(operator, cx);
204 };
205 self.search_motion(
206 Motion::ZedSearchResult {
207 prior_selections,
208 new_selections,
209 },
210 cx,
211 );
212 }
213
214 pub fn move_to_match_internal(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
215 let Some(pane) = self.pane(cx) else { return };
216 let count = self.take_count(cx).unwrap_or(1);
217 let prior_selections = self.editor_selections(cx);
218
219 let success = pane.update(cx, |pane, cx| {
220 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
221 return false;
222 };
223 search_bar.update(cx, |search_bar, cx| {
224 if !search_bar.has_active_match() || !search_bar.show(cx) {
225 return false;
226 }
227 search_bar.select_match(direction, count, cx);
228 true
229 })
230 });
231 if !success {
232 return;
233 }
234
235 let new_selections = self.editor_selections(cx);
236 self.search_motion(
237 Motion::ZedSearchResult {
238 prior_selections,
239 new_selections,
240 },
241 cx,
242 );
243 }
244
245 pub fn move_to_internal(
246 &mut self,
247 direction: Direction,
248 case_sensitive: bool,
249 whole_word: bool,
250 regex: bool,
251 cx: &mut ViewContext<Self>,
252 ) {
253 let Some(pane) = self.pane(cx) else { return };
254 let count = self.take_count(cx).unwrap_or(1);
255 let prior_selections = self.editor_selections(cx);
256 let vim = cx.view().clone();
257
258 let searched = pane.update(cx, |pane, cx| {
259 self.search.direction = direction;
260 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
261 return false;
262 };
263 let search = search_bar.update(cx, |search_bar, cx| {
264 let mut options = SearchOptions::NONE;
265 if case_sensitive {
266 options |= SearchOptions::CASE_SENSITIVE;
267 }
268 if regex {
269 options |= SearchOptions::REGEX;
270 }
271 if whole_word {
272 options |= SearchOptions::WHOLE_WORD;
273 }
274 if !search_bar.show(cx) {
275 return None;
276 }
277 let Some(query) = search_bar.query_suggestion(cx) else {
278 drop(search_bar.search("", None, cx));
279 return None;
280 };
281 let query = regex::escape(&query);
282 Some(search_bar.search(&query, Some(options), cx))
283 });
284
285 let Some(search) = search else { return false };
286
287 let search_bar = search_bar.downgrade();
288 cx.spawn(|_, mut cx| async move {
289 search.await?;
290 search_bar.update(&mut cx, |search_bar, cx| {
291 search_bar.select_match(direction, count, cx);
292
293 vim.update(cx, |vim, cx| {
294 let new_selections = vim.editor_selections(cx);
295 vim.search_motion(
296 Motion::ZedSearchResult {
297 prior_selections,
298 new_selections,
299 },
300 cx,
301 )
302 });
303 })?;
304 anyhow::Ok(())
305 })
306 .detach_and_log_err(cx);
307 true
308 });
309 if !searched {
310 self.clear_operator(cx)
311 }
312
313 if self.mode.is_visual() {
314 self.switch_mode(Mode::Normal, false, cx)
315 }
316 }
317
318 fn find_command(&mut self, action: &FindCommand, cx: &mut ViewContext<Self>) {
319 let Some(pane) = self.pane(cx) else { return };
320 pane.update(cx, |pane, cx| {
321 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
322 let search = search_bar.update(cx, |search_bar, cx| {
323 if !search_bar.show(cx) {
324 return None;
325 }
326 let mut query = action.query.clone();
327 if query.is_empty() {
328 query = search_bar.query(cx);
329 };
330
331 let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
332 if search_bar.should_use_smartcase_search(cx) {
333 options.set(
334 SearchOptions::CASE_SENSITIVE,
335 search_bar.is_contains_uppercase(&query),
336 );
337 }
338
339 Some(search_bar.search(&query, Some(options), cx))
340 });
341 let Some(search) = search else { return };
342 let search_bar = search_bar.downgrade();
343 let direction = if action.backwards {
344 Direction::Prev
345 } else {
346 Direction::Next
347 };
348 cx.spawn(|_, mut cx| async move {
349 search.await?;
350 search_bar.update(&mut cx, |search_bar, cx| {
351 search_bar.select_match(direction, 1, cx)
352 })?;
353 anyhow::Ok(())
354 })
355 .detach_and_log_err(cx);
356 }
357 })
358 }
359
360 fn replace_command(&mut self, action: &ReplaceCommand, cx: &mut ViewContext<Self>) {
361 let replacement = action.replacement.clone();
362 let Some(((pane, workspace), editor)) =
363 self.pane(cx).zip(self.workspace(cx)).zip(self.editor())
364 else {
365 return;
366 };
367 if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
368 let range = action.range.buffer_range(vim, editor, cx)?;
369 let snapshot = &editor.snapshot(cx).buffer_snapshot;
370 let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
371 let range = snapshot.anchor_before(Point::new(range.start.0, 0))
372 ..snapshot.anchor_after(end_point);
373 editor.set_search_within_ranges(&[range], cx);
374 anyhow::Ok(())
375 }) {
376 workspace.update(cx, |workspace, cx| {
377 result.notify_err(workspace, cx);
378 })
379 }
380 let vim = cx.view().clone();
381 pane.update(cx, |pane, cx| {
382 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
383 return;
384 };
385 let search = search_bar.update(cx, |search_bar, cx| {
386 if !search_bar.show(cx) {
387 return None;
388 }
389
390 let mut options = SearchOptions::REGEX;
391 if replacement.is_case_sensitive {
392 options.set(SearchOptions::CASE_SENSITIVE, true)
393 }
394 let search = if replacement.search.is_empty() {
395 search_bar.query(cx)
396 } else {
397 replacement.search
398 };
399 if search_bar.should_use_smartcase_search(cx) {
400 options.set(
401 SearchOptions::CASE_SENSITIVE,
402 search_bar.is_contains_uppercase(&search),
403 );
404 }
405 search_bar.set_replacement(Some(&replacement.replacement), cx);
406 Some(search_bar.search(&search, Some(options), cx))
407 });
408 let Some(search) = search else { return };
409 let search_bar = search_bar.downgrade();
410 cx.spawn(|_, mut cx| async move {
411 search.await?;
412 search_bar.update(&mut cx, |search_bar, cx| {
413 if replacement.should_replace_all {
414 search_bar.select_last_match(cx);
415 search_bar.replace_all(&Default::default(), cx);
416 cx.spawn(|_, mut cx| async move {
417 cx.background_executor()
418 .timer(Duration::from_millis(200))
419 .await;
420 editor
421 .update(&mut cx, |editor, cx| editor.clear_search_within_ranges(cx))
422 .ok();
423 })
424 .detach();
425 vim.update(cx, |vim, cx| {
426 vim.move_cursor(
427 Motion::StartOfLine {
428 display_lines: false,
429 },
430 None,
431 cx,
432 )
433 });
434 }
435 })?;
436 anyhow::Ok(())
437 })
438 .detach_and_log_err(cx);
439 })
440 }
441}
442
443impl Replacement {
444 // convert a vim query into something more usable by zed.
445 // we don't attempt to fully convert between the two regex syntaxes,
446 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
447 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
448 pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
449 let delimiter = chars
450 .next()
451 .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
452
453 let mut search = String::new();
454 let mut replacement = String::new();
455 let mut flags = String::new();
456
457 let mut buffer = &mut search;
458
459 let mut escaped = false;
460 // 0 - parsing search
461 // 1 - parsing replacement
462 // 2 - parsing flags
463 let mut phase = 0;
464
465 for c in chars {
466 if escaped {
467 escaped = false;
468 if phase == 1 && c.is_ascii_digit() {
469 buffer.push('$')
470 // unescape escaped parens
471 } else if phase == 0 && c == '(' || c == ')' {
472 } else if c != delimiter {
473 buffer.push('\\')
474 }
475 buffer.push(c)
476 } else if c == '\\' {
477 escaped = true;
478 } else if c == delimiter {
479 if phase == 0 {
480 buffer = &mut replacement;
481 phase = 1;
482 } else if phase == 1 {
483 buffer = &mut flags;
484 phase = 2;
485 } else {
486 break;
487 }
488 } else {
489 // escape unescaped parens
490 if phase == 0 && c == '(' || c == ')' {
491 buffer.push('\\')
492 }
493 buffer.push(c)
494 }
495 }
496
497 let mut replacement = Replacement {
498 search,
499 replacement,
500 should_replace_all: true,
501 is_case_sensitive: true,
502 };
503
504 for c in flags.chars() {
505 match c {
506 'g' | 'I' => {}
507 'c' | 'n' => replacement.should_replace_all = false,
508 'i' => replacement.is_case_sensitive = false,
509 _ => {}
510 }
511 }
512
513 Some(replacement)
514 }
515}
516
517#[cfg(test)]
518mod test {
519 use std::time::Duration;
520
521 use crate::{
522 state::Mode,
523 test::{NeovimBackedTestContext, VimTestContext},
524 };
525 use editor::EditorSettings;
526 use editor::{display_map::DisplayRow, DisplayPoint};
527 use indoc::indoc;
528 use search::BufferSearchBar;
529 use settings::SettingsStore;
530
531 #[gpui::test]
532 async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
533 let mut cx = VimTestContext::new(cx, true).await;
534 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
535
536 cx.simulate_keystrokes("*");
537 cx.run_until_parked();
538 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
539
540 cx.simulate_keystrokes("*");
541 cx.run_until_parked();
542 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
543
544 cx.simulate_keystrokes("#");
545 cx.run_until_parked();
546 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
547
548 cx.simulate_keystrokes("#");
549 cx.run_until_parked();
550 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
551
552 cx.simulate_keystrokes("2 *");
553 cx.run_until_parked();
554 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
555
556 cx.simulate_keystrokes("g *");
557 cx.run_until_parked();
558 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
559
560 cx.simulate_keystrokes("n");
561 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
562
563 cx.simulate_keystrokes("g #");
564 cx.run_until_parked();
565 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
566 }
567
568 #[gpui::test]
569 async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
570 let mut cx = VimTestContext::new(cx, true).await;
571
572 cx.update_global(|store: &mut SettingsStore, cx| {
573 store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
574 });
575
576 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
577
578 cx.simulate_keystrokes("*");
579 cx.run_until_parked();
580 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
581
582 cx.simulate_keystrokes("*");
583 cx.run_until_parked();
584 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
585
586 cx.simulate_keystrokes("#");
587 cx.run_until_parked();
588 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
589
590 cx.simulate_keystrokes("3 *");
591 cx.run_until_parked();
592 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
593
594 cx.simulate_keystrokes("g *");
595 cx.run_until_parked();
596 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
597
598 cx.simulate_keystrokes("n");
599 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
600
601 cx.simulate_keystrokes("g #");
602 cx.run_until_parked();
603 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
604 }
605
606 #[gpui::test]
607 async fn test_search(cx: &mut gpui::TestAppContext) {
608 let mut cx = VimTestContext::new(cx, true).await;
609
610 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
611 cx.simulate_keystrokes("/ c c");
612
613 let search_bar = cx.workspace(|workspace, cx| {
614 workspace
615 .active_pane()
616 .read(cx)
617 .toolbar()
618 .read(cx)
619 .item_of_type::<BufferSearchBar>()
620 .expect("Buffer search bar should be deployed")
621 });
622
623 cx.update_view(search_bar, |bar, cx| {
624 assert_eq!(bar.query(cx), "cc");
625 });
626
627 cx.run_until_parked();
628
629 cx.update_editor(|editor, cx| {
630 let highlights = editor.all_text_background_highlights(cx);
631 assert_eq!(3, highlights.len());
632 assert_eq!(
633 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
634 highlights[0].0
635 )
636 });
637
638 cx.simulate_keystrokes("enter");
639 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
640
641 // n to go to next/N to go to previous
642 cx.simulate_keystrokes("n");
643 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
644 cx.simulate_keystrokes("shift-n");
645 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
646
647 // ?<enter> to go to previous
648 cx.simulate_keystrokes("? enter");
649 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
650 cx.simulate_keystrokes("? enter");
651 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
652
653 // /<enter> to go to next
654 cx.simulate_keystrokes("/ enter");
655 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
656
657 // ?{search}<enter> to search backwards
658 cx.simulate_keystrokes("? b enter");
659 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
660
661 // works with counts
662 cx.simulate_keystrokes("4 / c");
663 cx.simulate_keystrokes("enter");
664 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
665
666 // check that searching resumes from cursor, not previous match
667 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
668 cx.simulate_keystrokes("/ d");
669 cx.simulate_keystrokes("enter");
670 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
671 cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
672 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
673 cx.simulate_keystrokes("/ b");
674 cx.simulate_keystrokes("enter");
675 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
676
677 // check that searching switches to normal mode if in visual mode
678 cx.set_state("ˇone two one", Mode::Normal);
679 cx.simulate_keystrokes("v l l");
680 cx.assert_editor_state("«oneˇ» two one");
681 cx.simulate_keystrokes("*");
682 cx.assert_state("one two ˇone", Mode::Normal);
683
684 // check that searching with unable search wrap
685 cx.update_global(|store: &mut SettingsStore, cx| {
686 store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
687 });
688 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
689 cx.simulate_keystrokes("/ c c enter");
690
691 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
692
693 // n to go to next/N to go to previous
694 cx.simulate_keystrokes("n");
695 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
696 cx.simulate_keystrokes("shift-n");
697 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
698
699 // ?<enter> to go to previous
700 cx.simulate_keystrokes("? enter");
701 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
702 cx.simulate_keystrokes("? enter");
703 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
704 }
705
706 #[gpui::test]
707 async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
708 let mut cx = VimTestContext::new(cx, false).await;
709 cx.cx.set_state("ˇone one one one");
710 cx.simulate_keystrokes("cmd-f");
711 cx.run_until_parked();
712
713 cx.assert_editor_state("«oneˇ» one one one");
714 cx.simulate_keystrokes("enter");
715 cx.assert_editor_state("one «oneˇ» one one");
716 cx.simulate_keystrokes("shift-enter");
717 cx.assert_editor_state("«oneˇ» one one one");
718 }
719
720 #[gpui::test]
721 async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
722 let mut cx = NeovimBackedTestContext::new(cx).await;
723
724 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
725 cx.simulate_shared_keystrokes("v 3 l *").await;
726 cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
727 }
728
729 #[gpui::test]
730 async fn test_d_search(cx: &mut gpui::TestAppContext) {
731 let mut cx = NeovimBackedTestContext::new(cx).await;
732
733 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
734 cx.simulate_shared_keystrokes("d / c d").await;
735 cx.simulate_shared_keystrokes("enter").await;
736 cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
737 }
738
739 #[gpui::test]
740 async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
741 let mut cx = NeovimBackedTestContext::new(cx).await;
742
743 cx.set_shared_state("ˇa b a b a b a").await;
744 cx.simulate_shared_keystrokes("*").await;
745 cx.simulate_shared_keystrokes("n").await;
746 cx.shared_state().await.assert_eq("a b a b ˇa b a");
747 cx.simulate_shared_keystrokes("#").await;
748 cx.shared_state().await.assert_eq("a b ˇa b a b a");
749 cx.simulate_shared_keystrokes("n").await;
750 cx.shared_state().await.assert_eq("ˇa b a b a b a");
751 }
752
753 #[gpui::test]
754 async fn test_v_search(cx: &mut gpui::TestAppContext) {
755 let mut cx = NeovimBackedTestContext::new(cx).await;
756
757 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
758 cx.simulate_shared_keystrokes("v / c d").await;
759 cx.simulate_shared_keystrokes("enter").await;
760 cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
761
762 cx.set_shared_state("a a aˇ a a a").await;
763 cx.simulate_shared_keystrokes("v / a").await;
764 cx.simulate_shared_keystrokes("enter").await;
765 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
766 cx.simulate_shared_keystrokes("/ enter").await;
767 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
768 cx.simulate_shared_keystrokes("? enter").await;
769 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
770 cx.simulate_shared_keystrokes("? enter").await;
771 cx.shared_state().await.assert_eq("a a «ˇa »a a a");
772 cx.simulate_shared_keystrokes("/ enter").await;
773 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
774 cx.simulate_shared_keystrokes("/ enter").await;
775 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
776 }
777
778 #[gpui::test]
779 async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
780 let mut cx = NeovimBackedTestContext::new(cx).await;
781
782 cx.set_shared_state(indoc! {
783 "ˇone two
784 three four
785 five six
786 "
787 })
788 .await;
789 cx.simulate_shared_keystrokes("ctrl-v j / f").await;
790 cx.simulate_shared_keystrokes("enter").await;
791 cx.shared_state().await.assert_eq(indoc! {
792 "«one twoˇ»
793 «three fˇ»our
794 five six
795 "
796 });
797 }
798
799 // cargo test -p vim --features neovim test_replace_with_range_at_start
800 #[gpui::test]
801 async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
802 let mut cx = NeovimBackedTestContext::new(cx).await;
803
804 cx.set_shared_state(indoc! {
805 "ˇa
806 a
807 a
808 a
809 a
810 a
811 a
812 "
813 })
814 .await;
815 cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
816 cx.simulate_shared_keystrokes("enter").await;
817 cx.shared_state().await.assert_eq(indoc! {
818 "a
819 ba
820 ba
821 ba
822 ˇba
823 a
824 a
825 "
826 });
827 cx.executor().advance_clock(Duration::from_millis(250));
828 cx.run_until_parked();
829
830 cx.simulate_shared_keystrokes("/ a enter").await;
831 cx.shared_state().await.assert_eq(indoc! {
832 "a
833 ba
834 ba
835 ba
836 bˇa
837 a
838 a
839 "
840 });
841 }
842
843 // cargo test -p vim --features neovim test_replace_with_range
844 #[gpui::test]
845 async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
846 let mut cx = NeovimBackedTestContext::new(cx).await;
847
848 cx.set_shared_state(indoc! {
849 "ˇa
850 a
851 a
852 a
853 a
854 a
855 a
856 "
857 })
858 .await;
859 cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
860 cx.simulate_shared_keystrokes("enter").await;
861 cx.shared_state().await.assert_eq(indoc! {
862 "a
863 b
864 b
865 b
866 ˇb
867 a
868 a
869 "
870 });
871 cx.executor().advance_clock(Duration::from_millis(250));
872 cx.run_until_parked();
873
874 cx.simulate_shared_keystrokes("/ a enter").await;
875 cx.shared_state().await.assert_eq(indoc! {
876 "a
877 b
878 b
879 b
880 b
881 ˇa
882 a
883 "
884 });
885 }
886}