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