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