1use editor::Editor;
2use gpui::{Context, Window, actions, impl_actions, impl_internal_actions};
3use language::Point;
4use schemars::JsonSchema;
5use search::{BufferSearchBar, SearchOptions, buffer_search};
6use serde_derive::Deserialize;
7use std::{iter::Peekable, str::Chars};
8use util::serde::default_true;
9use workspace::{notifications::NotifyResultExt, searchable::Direction};
10
11use crate::{
12 Vim,
13 command::CommandRange,
14 motion::Motion,
15 state::{Mode, SearchState},
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 cursor_word = self.editor_cursor_word(window, cx);
308 let vim = cx.entity().clone();
309
310 let searched = pane.update(cx, |pane, cx| {
311 self.search.direction = direction;
312 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
313 return false;
314 };
315 let search = search_bar.update(cx, |search_bar, cx| {
316 let mut options = SearchOptions::NONE;
317 if case_sensitive {
318 options |= SearchOptions::CASE_SENSITIVE;
319 }
320 if regex {
321 options |= SearchOptions::REGEX;
322 }
323 if whole_word {
324 options |= SearchOptions::WHOLE_WORD;
325 }
326 if !search_bar.show(window, cx) {
327 return None;
328 }
329 let Some(query) = search_bar
330 .query_suggestion(window, cx)
331 .or_else(|| cursor_word)
332 else {
333 drop(search_bar.search("", None, window, cx));
334 return None;
335 };
336
337 let query = regex::escape(&query);
338 Some(search_bar.search(&query, Some(options), window, cx))
339 });
340
341 let Some(search) = search else { return false };
342
343 let search_bar = search_bar.downgrade();
344 cx.spawn_in(window, async move |_, cx| {
345 search.await?;
346 search_bar.update_in(cx, |search_bar, window, cx| {
347 search_bar.select_match(direction, count, window, cx);
348
349 vim.update(cx, |vim, cx| {
350 let new_selections = vim.editor_selections(window, cx);
351 vim.search_motion(
352 Motion::ZedSearchResult {
353 prior_selections,
354 new_selections,
355 },
356 window,
357 cx,
358 )
359 });
360 })?;
361 anyhow::Ok(())
362 })
363 .detach_and_log_err(cx);
364 true
365 });
366 if !searched {
367 self.clear_operator(window, cx)
368 }
369
370 if self.mode.is_visual() {
371 self.switch_mode(Mode::Normal, false, window, cx)
372 }
373 }
374
375 fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
376 let Some(pane) = self.pane(window, cx) else {
377 return;
378 };
379 pane.update(cx, |pane, cx| {
380 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
381 let search = search_bar.update(cx, |search_bar, cx| {
382 if !search_bar.show(window, cx) {
383 return None;
384 }
385 let mut query = action.query.clone();
386 if query.is_empty() {
387 query = search_bar.query(cx);
388 };
389
390 let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
391 if search_bar.should_use_smartcase_search(cx) {
392 options.set(
393 SearchOptions::CASE_SENSITIVE,
394 search_bar.is_contains_uppercase(&query),
395 );
396 }
397
398 Some(search_bar.search(&query, Some(options), window, cx))
399 });
400 let Some(search) = search else { return };
401 let search_bar = search_bar.downgrade();
402 let direction = if action.backwards {
403 Direction::Prev
404 } else {
405 Direction::Next
406 };
407 cx.spawn_in(window, async move |_, cx| {
408 search.await?;
409 search_bar.update_in(cx, |search_bar, window, cx| {
410 search_bar.select_match(direction, 1, window, cx)
411 })?;
412 anyhow::Ok(())
413 })
414 .detach_and_log_err(cx);
415 }
416 })
417 }
418
419 fn replace_command(
420 &mut self,
421 action: &ReplaceCommand,
422 window: &mut Window,
423 cx: &mut Context<Self>,
424 ) {
425 let replacement = action.replacement.clone();
426 let Some(((pane, workspace), editor)) = self
427 .pane(window, cx)
428 .zip(self.workspace(window))
429 .zip(self.editor())
430 else {
431 return;
432 };
433 if let Some(result) = self.update_editor(window, cx, |vim, editor, window, cx| {
434 let range = action.range.buffer_range(vim, editor, window, cx)?;
435 let snapshot = &editor.snapshot(window, cx).buffer_snapshot;
436 let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
437 let range = snapshot.anchor_before(Point::new(range.start.0, 0))
438 ..snapshot.anchor_after(end_point);
439 editor.set_search_within_ranges(&[range], cx);
440 anyhow::Ok(())
441 }) {
442 workspace.update(cx, |workspace, cx| {
443 result.notify_err(workspace, cx);
444 })
445 }
446 let vim = cx.entity().clone();
447 pane.update(cx, |pane, cx| {
448 let mut options = SearchOptions::REGEX;
449
450 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
451 return;
452 };
453 let search = search_bar.update(cx, |search_bar, cx| {
454 if !search_bar.show(window, cx) {
455 return None;
456 }
457
458 if replacement.is_case_sensitive {
459 options.set(SearchOptions::CASE_SENSITIVE, true)
460 }
461 let search = if replacement.search.is_empty() {
462 search_bar.query(cx)
463 } else {
464 replacement.search
465 };
466 if search_bar.should_use_smartcase_search(cx) {
467 options.set(
468 SearchOptions::CASE_SENSITIVE,
469 search_bar.is_contains_uppercase(&search),
470 );
471 }
472
473 if !replacement.should_replace_all {
474 options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
475 }
476
477 search_bar.set_replacement(Some(&replacement.replacement), cx);
478 Some(search_bar.search(&search, Some(options), window, cx))
479 });
480 let Some(search) = search else { return };
481 let search_bar = search_bar.downgrade();
482 cx.spawn_in(window, async move |_, cx| {
483 search.await?;
484 search_bar.update_in(cx, |search_bar, window, cx| {
485 search_bar.select_last_match(window, cx);
486 search_bar.replace_all(&Default::default(), window, cx);
487 editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
488 let _ = search_bar.search(&search_bar.query(cx), None, window, cx);
489 vim.update(cx, |vim, cx| {
490 vim.move_cursor(
491 Motion::StartOfLine {
492 display_lines: false,
493 },
494 None,
495 window,
496 cx,
497 )
498 });
499
500 // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
501 // this is not properly supported outside of vim mode, and
502 // not disabling it makes the "Replace All Matches" button
503 // actually replace only the first match on each line.
504 options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
505 search_bar.set_search_options(options, cx);
506 })?;
507 anyhow::Ok(())
508 })
509 .detach_and_log_err(cx);
510 })
511 }
512}
513
514impl Replacement {
515 // convert a vim query into something more usable by zed.
516 // we don't attempt to fully convert between the two regex syntaxes,
517 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
518 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
519 pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
520 let delimiter = chars
521 .next()
522 .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
523
524 let mut search = String::new();
525 let mut replacement = String::new();
526 let mut flags = String::new();
527
528 let mut buffer = &mut search;
529
530 let mut escaped = false;
531 // 0 - parsing search
532 // 1 - parsing replacement
533 // 2 - parsing flags
534 let mut phase = 0;
535
536 for c in chars {
537 if escaped {
538 escaped = false;
539 if phase == 1 && c.is_ascii_digit() {
540 buffer.push('$')
541 // unescape escaped parens
542 } else if phase == 0 && c == '(' || c == ')' {
543 } else if c != delimiter {
544 buffer.push('\\')
545 }
546 buffer.push(c)
547 } else if c == '\\' {
548 escaped = true;
549 } else if c == delimiter {
550 if phase == 0 {
551 buffer = &mut replacement;
552 phase = 1;
553 } else if phase == 1 {
554 buffer = &mut flags;
555 phase = 2;
556 } else {
557 break;
558 }
559 } else {
560 // escape unescaped parens
561 if phase == 0 && c == '(' || c == ')' {
562 buffer.push('\\')
563 }
564 buffer.push(c)
565 }
566 }
567
568 let mut replacement = Replacement {
569 search,
570 replacement,
571 should_replace_all: false,
572 is_case_sensitive: true,
573 };
574
575 for c in flags.chars() {
576 match c {
577 'g' => replacement.should_replace_all = true,
578 'c' | 'n' => replacement.should_replace_all = false,
579 'i' => replacement.is_case_sensitive = false,
580 'I' => replacement.is_case_sensitive = true,
581 _ => {}
582 }
583 }
584
585 Some(replacement)
586 }
587}
588
589#[cfg(test)]
590mod test {
591 use std::time::Duration;
592
593 use crate::{
594 state::Mode,
595 test::{NeovimBackedTestContext, VimTestContext},
596 };
597 use editor::EditorSettings;
598 use editor::{DisplayPoint, display_map::DisplayRow};
599
600 use indoc::indoc;
601 use search::BufferSearchBar;
602 use settings::SettingsStore;
603
604 #[gpui::test]
605 async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
606 let mut cx = VimTestContext::new(cx, true).await;
607 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
608
609 cx.simulate_keystrokes("*");
610 cx.run_until_parked();
611 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
612
613 cx.simulate_keystrokes("*");
614 cx.run_until_parked();
615 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
616
617 cx.simulate_keystrokes("#");
618 cx.run_until_parked();
619 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
620
621 cx.simulate_keystrokes("#");
622 cx.run_until_parked();
623 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
624
625 cx.simulate_keystrokes("2 *");
626 cx.run_until_parked();
627 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
628
629 cx.simulate_keystrokes("g *");
630 cx.run_until_parked();
631 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
632
633 cx.simulate_keystrokes("n");
634 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
635
636 cx.simulate_keystrokes("g #");
637 cx.run_until_parked();
638 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
639 }
640
641 #[gpui::test]
642 async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
643 let mut cx = VimTestContext::new(cx, true).await;
644
645 cx.update_global(|store: &mut SettingsStore, cx| {
646 store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
647 });
648
649 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
650
651 cx.simulate_keystrokes("*");
652 cx.run_until_parked();
653 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
654
655 cx.simulate_keystrokes("*");
656 cx.run_until_parked();
657 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
658
659 cx.simulate_keystrokes("#");
660 cx.run_until_parked();
661 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
662
663 cx.simulate_keystrokes("3 *");
664 cx.run_until_parked();
665 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
666
667 cx.simulate_keystrokes("g *");
668 cx.run_until_parked();
669 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
670
671 cx.simulate_keystrokes("n");
672 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
673
674 cx.simulate_keystrokes("g #");
675 cx.run_until_parked();
676 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
677 }
678
679 #[gpui::test]
680 async fn test_search(cx: &mut gpui::TestAppContext) {
681 let mut cx = VimTestContext::new(cx, true).await;
682
683 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
684 cx.simulate_keystrokes("/ c c");
685
686 let search_bar = cx.workspace(|workspace, _, cx| {
687 workspace
688 .active_pane()
689 .read(cx)
690 .toolbar()
691 .read(cx)
692 .item_of_type::<BufferSearchBar>()
693 .expect("Buffer search bar should be deployed")
694 });
695
696 cx.update_entity(search_bar, |bar, _window, cx| {
697 assert_eq!(bar.query(cx), "cc");
698 });
699
700 cx.run_until_parked();
701
702 cx.update_editor(|editor, window, cx| {
703 let highlights = editor.all_text_background_highlights(window, cx);
704 assert_eq!(3, highlights.len());
705 assert_eq!(
706 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
707 highlights[0].0
708 )
709 });
710
711 cx.simulate_keystrokes("enter");
712 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
713
714 // n to go to next/N to go to previous
715 cx.simulate_keystrokes("n");
716 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
717 cx.simulate_keystrokes("shift-n");
718 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
719
720 // ?<enter> to go to previous
721 cx.simulate_keystrokes("? enter");
722 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
723 cx.simulate_keystrokes("? enter");
724 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
725
726 // /<enter> to go to next
727 cx.simulate_keystrokes("/ enter");
728 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
729
730 // ?{search}<enter> to search backwards
731 cx.simulate_keystrokes("? b enter");
732 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
733
734 // works with counts
735 cx.simulate_keystrokes("4 / c");
736 cx.simulate_keystrokes("enter");
737 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
738
739 // check that searching resumes from cursor, not previous match
740 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
741 cx.simulate_keystrokes("/ d");
742 cx.simulate_keystrokes("enter");
743 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
744 cx.update_editor(|editor, window, cx| {
745 editor.move_to_beginning(&Default::default(), window, cx)
746 });
747 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
748 cx.simulate_keystrokes("/ b");
749 cx.simulate_keystrokes("enter");
750 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
751
752 // check that searching switches to normal mode if in visual mode
753 cx.set_state("ˇone two one", Mode::Normal);
754 cx.simulate_keystrokes("v l l");
755 cx.assert_editor_state("«oneˇ» two one");
756 cx.simulate_keystrokes("*");
757 cx.assert_state("one two ˇone", Mode::Normal);
758
759 // check that a backward search after last match works correctly
760 cx.set_state("aa\naa\nbbˇ", Mode::Normal);
761 cx.simulate_keystrokes("? a a");
762 cx.simulate_keystrokes("enter");
763 cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
764
765 // check that searching with unable search wrap
766 cx.update_global(|store: &mut SettingsStore, cx| {
767 store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
768 });
769 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
770 cx.simulate_keystrokes("/ c c enter");
771
772 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
773
774 // n to go to next/N to go to previous
775 cx.simulate_keystrokes("n");
776 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
777 cx.simulate_keystrokes("shift-n");
778 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
779
780 // ?<enter> to go to previous
781 cx.simulate_keystrokes("? enter");
782 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
783 cx.simulate_keystrokes("? enter");
784 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
785 }
786
787 #[gpui::test]
788 async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
789 let mut cx = VimTestContext::new(cx, false).await;
790 cx.cx.set_state("ˇone one one one");
791 cx.run_until_parked();
792 cx.simulate_keystrokes("cmd-f");
793 cx.run_until_parked();
794
795 cx.assert_editor_state("«oneˇ» one one one");
796 cx.simulate_keystrokes("enter");
797 cx.assert_editor_state("one «oneˇ» one one");
798 cx.simulate_keystrokes("shift-enter");
799 cx.assert_editor_state("«oneˇ» one one one");
800 }
801
802 #[gpui::test]
803 async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
804 let mut cx = NeovimBackedTestContext::new(cx).await;
805
806 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
807 cx.simulate_shared_keystrokes("v 3 l *").await;
808 cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
809 }
810
811 #[gpui::test]
812 async fn test_d_search(cx: &mut gpui::TestAppContext) {
813 let mut cx = NeovimBackedTestContext::new(cx).await;
814
815 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
816 cx.simulate_shared_keystrokes("d / c d").await;
817 cx.simulate_shared_keystrokes("enter").await;
818 cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
819 }
820
821 #[gpui::test]
822 async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
823 let mut cx = NeovimBackedTestContext::new(cx).await;
824
825 cx.set_shared_state("ˇa b a b a b a").await;
826 cx.simulate_shared_keystrokes("*").await;
827 cx.simulate_shared_keystrokes("n").await;
828 cx.shared_state().await.assert_eq("a b a b ˇa b a");
829 cx.simulate_shared_keystrokes("#").await;
830 cx.shared_state().await.assert_eq("a b ˇa b a b a");
831 cx.simulate_shared_keystrokes("n").await;
832 cx.shared_state().await.assert_eq("ˇa b a b a b a");
833 }
834
835 #[gpui::test]
836 async fn test_v_search(cx: &mut gpui::TestAppContext) {
837 let mut cx = NeovimBackedTestContext::new(cx).await;
838
839 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
840 cx.simulate_shared_keystrokes("v / c d").await;
841 cx.simulate_shared_keystrokes("enter").await;
842 cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
843
844 cx.set_shared_state("a a aˇ a a a").await;
845 cx.simulate_shared_keystrokes("v / a").await;
846 cx.simulate_shared_keystrokes("enter").await;
847 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
848 cx.simulate_shared_keystrokes("/ enter").await;
849 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
850 cx.simulate_shared_keystrokes("? enter").await;
851 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
852 cx.simulate_shared_keystrokes("? enter").await;
853 cx.shared_state().await.assert_eq("a a «ˇa »a a a");
854 cx.simulate_shared_keystrokes("/ enter").await;
855 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
856 cx.simulate_shared_keystrokes("/ enter").await;
857 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
858 }
859
860 #[gpui::test]
861 async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
862 let mut cx = NeovimBackedTestContext::new(cx).await;
863
864 cx.set_shared_state("ˇaa aa").await;
865 cx.simulate_shared_keystrokes("v / a a").await;
866 cx.simulate_shared_keystrokes("enter").await;
867 cx.shared_state().await.assert_eq("«aa aˇ»a");
868 }
869
870 #[gpui::test]
871 async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
872 let mut cx = NeovimBackedTestContext::new(cx).await;
873
874 cx.set_shared_state(indoc! {
875 "ˇone two
876 three four
877 five six
878 "
879 })
880 .await;
881 cx.simulate_shared_keystrokes("ctrl-v j / f").await;
882 cx.simulate_shared_keystrokes("enter").await;
883 cx.shared_state().await.assert_eq(indoc! {
884 "«one twoˇ»
885 «three fˇ»our
886 five six
887 "
888 });
889 }
890
891 // cargo test -p vim --features neovim test_replace_with_range_at_start
892 #[gpui::test]
893 async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
894 let mut cx = NeovimBackedTestContext::new(cx).await;
895
896 cx.set_shared_state(indoc! {
897 "ˇa
898 a
899 a
900 a
901 a
902 a
903 a
904 "
905 })
906 .await;
907 cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
908 cx.simulate_shared_keystrokes("enter").await;
909 cx.shared_state().await.assert_eq(indoc! {
910 "a
911 ba
912 ba
913 ba
914 ˇba
915 a
916 a
917 "
918 });
919
920 cx.simulate_shared_keystrokes("/ a").await;
921 cx.simulate_shared_keystrokes("enter").await;
922 cx.shared_state().await.assert_eq(indoc! {
923 "a
924 ba
925 ba
926 ba
927 bˇa
928 a
929 a
930 "
931 });
932 }
933
934 #[gpui::test]
935 async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
936 let mut cx = NeovimBackedTestContext::new(cx).await;
937 cx.set_shared_state(indoc! {
938 "ˇaa aa aa"
939 })
940 .await;
941
942 cx.simulate_shared_keystrokes("/ a a").await;
943 cx.simulate_shared_keystrokes("enter").await;
944
945 cx.shared_state().await.assert_eq(indoc! {
946 "aa ˇaa aa"
947 });
948
949 cx.simulate_shared_keystrokes("left / a a").await;
950 cx.simulate_shared_keystrokes("enter").await;
951
952 cx.shared_state().await.assert_eq(indoc! {
953 "aa ˇaa aa"
954 });
955 }
956
957 #[gpui::test]
958 async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
959 let mut cx = NeovimBackedTestContext::new(cx).await;
960
961 cx.set_shared_state(indoc! {
962 "ˇa
963 a
964 a
965 a
966 a
967 a
968 a
969 "
970 })
971 .await;
972 cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
973 cx.simulate_shared_keystrokes("enter").await;
974 cx.shared_state().await.assert_eq(indoc! {
975 "a
976 b
977 b
978 b
979 ˇb
980 a
981 a
982 "
983 });
984 cx.executor().advance_clock(Duration::from_millis(250));
985 cx.run_until_parked();
986
987 cx.simulate_shared_keystrokes("/ a enter").await;
988 cx.shared_state().await.assert_eq(indoc! {
989 "a
990 b
991 b
992 b
993 b
994 ˇa
995 a
996 "
997 });
998 }
999}