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