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