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 helix_select: false,
199 }
200 });
201 }
202 })
203 }
204
205 // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
206 fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
207 // Preserve the current mode when resetting search state
208 let current_mode = self.mode;
209 self.search = Default::default();
210 self.search.prior_mode = current_mode;
211 cx.propagate();
212 }
213
214 pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
215 self.store_visual_marks(window, cx);
216 let Some(pane) = self.pane(window, cx) else {
217 return;
218 };
219 let new_selections = self.editor_selections(window, cx);
220 let result = pane.update(cx, |pane, cx| {
221 let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
222 if self.search.helix_select {
223 search_bar.update(cx, |search_bar, cx| {
224 search_bar.select_all_matches(&Default::default(), window, cx)
225 });
226 return None;
227 }
228 search_bar.update(cx, |search_bar, cx| {
229 let mut count = self.search.count;
230 let direction = self.search.direction;
231 search_bar.has_active_match();
232 let new_head = new_selections.last()?.start;
233 let is_different_head = self
234 .search
235 .prior_selections
236 .last()
237 .is_none_or(|range| range.start != new_head);
238
239 if is_different_head {
240 count = count.saturating_sub(1)
241 }
242 self.search.count = 1;
243 search_bar.select_match(direction, count, window, cx);
244 search_bar.focus_editor(&Default::default(), window, cx);
245
246 let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
247 let prior_mode = self.search.prior_mode;
248 let prior_operator = self.search.prior_operator.take();
249
250 let query = search_bar.query(cx).into();
251 Vim::globals(cx).registers.insert('/', query);
252 Some((prior_selections, prior_mode, prior_operator))
253 })
254 });
255
256 let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
257 return;
258 };
259
260 let new_selections = self.editor_selections(window, cx);
261
262 // If the active editor has changed during a search, don't panic.
263 if prior_selections.iter().any(|s| {
264 self.update_editor(cx, |_, editor, cx| {
265 !s.start
266 .is_valid(&editor.snapshot(window, cx).buffer_snapshot())
267 })
268 .unwrap_or(true)
269 }) {
270 prior_selections.clear();
271 }
272
273 if prior_mode != self.mode {
274 self.switch_mode(prior_mode, true, window, cx);
275 }
276 if let Some(operator) = prior_operator {
277 self.push_operator(operator, window, cx);
278 };
279 self.search_motion(
280 Motion::ZedSearchResult {
281 prior_selections,
282 new_selections,
283 },
284 window,
285 cx,
286 );
287 }
288
289 pub fn move_to_match_internal(
290 &mut self,
291 direction: Direction,
292 window: &mut Window,
293 cx: &mut Context<Self>,
294 ) {
295 let Some(pane) = self.pane(window, cx) else {
296 return;
297 };
298 let count = Vim::take_count(cx).unwrap_or(1);
299 Vim::take_forced_motion(cx);
300 let prior_selections = self.editor_selections(window, cx);
301
302 let success = pane.update(cx, |pane, cx| {
303 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
304 return false;
305 };
306 search_bar.update(cx, |search_bar, cx| {
307 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
308 return false;
309 }
310 search_bar.select_match(direction, count, window, cx);
311 true
312 })
313 });
314 if !success {
315 return;
316 }
317
318 let new_selections = self.editor_selections(window, cx);
319 self.search_motion(
320 Motion::ZedSearchResult {
321 prior_selections,
322 new_selections,
323 },
324 window,
325 cx,
326 );
327 }
328
329 pub fn move_to_internal(
330 &mut self,
331 direction: Direction,
332 case_sensitive: bool,
333 whole_word: bool,
334 regex: bool,
335 window: &mut Window,
336 cx: &mut Context<Self>,
337 ) {
338 let Some(pane) = self.pane(window, cx) else {
339 return;
340 };
341 let count = Vim::take_count(cx).unwrap_or(1);
342 Vim::take_forced_motion(cx);
343 let prior_selections = self.editor_selections(window, cx);
344 let cursor_word = self.editor_cursor_word(window, cx);
345 let vim = cx.entity();
346
347 let searched = pane.update(cx, |pane, cx| {
348 self.search.direction = direction;
349 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
350 return false;
351 };
352 let search = search_bar.update(cx, |search_bar, cx| {
353 let mut options = SearchOptions::NONE;
354 if case_sensitive {
355 options |= SearchOptions::CASE_SENSITIVE;
356 }
357 if regex {
358 options |= SearchOptions::REGEX;
359 }
360 if whole_word {
361 options |= SearchOptions::WHOLE_WORD;
362 }
363 if !search_bar.show(window, cx) {
364 return None;
365 }
366 let Some(query) = search_bar
367 .query_suggestion(window, cx)
368 .or_else(|| cursor_word)
369 else {
370 drop(search_bar.search("", None, false, window, cx));
371 return None;
372 };
373
374 let query = regex::escape(&query);
375 Some(search_bar.search(&query, Some(options), true, window, cx))
376 });
377
378 let Some(search) = search else { return false };
379
380 let search_bar = search_bar.downgrade();
381 cx.spawn_in(window, async move |_, cx| {
382 search.await?;
383 search_bar.update_in(cx, |search_bar, window, cx| {
384 search_bar.select_match(direction, count, window, cx);
385
386 vim.update(cx, |vim, cx| {
387 let new_selections = vim.editor_selections(window, cx);
388 vim.search_motion(
389 Motion::ZedSearchResult {
390 prior_selections,
391 new_selections,
392 },
393 window,
394 cx,
395 )
396 });
397 })?;
398 anyhow::Ok(())
399 })
400 .detach_and_log_err(cx);
401 true
402 });
403 if !searched {
404 self.clear_operator(window, cx)
405 }
406
407 if self.mode.is_visual() {
408 self.switch_mode(Mode::Normal, false, window, cx)
409 }
410 }
411
412 fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
413 let Some(pane) = self.pane(window, cx) else {
414 return;
415 };
416 pane.update(cx, |pane, cx| {
417 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
418 let search = search_bar.update(cx, |search_bar, cx| {
419 if !search_bar.show(window, cx) {
420 return None;
421 }
422 let mut query = action.query.clone();
423 if query.is_empty() {
424 query = search_bar.query(cx);
425 };
426
427 let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
428 if search_bar.should_use_smartcase_search(cx) {
429 options.set(
430 SearchOptions::CASE_SENSITIVE,
431 search_bar.is_contains_uppercase(&query),
432 );
433 }
434
435 Some(search_bar.search(&query, Some(options), true, window, cx))
436 });
437 let Some(search) = search else { return };
438 let search_bar = search_bar.downgrade();
439 let direction = if action.backwards {
440 Direction::Prev
441 } else {
442 Direction::Next
443 };
444 cx.spawn_in(window, async move |_, cx| {
445 search.await?;
446 search_bar.update_in(cx, |search_bar, window, cx| {
447 search_bar.select_match(direction, 1, window, cx)
448 })?;
449 anyhow::Ok(())
450 })
451 .detach_and_log_err(cx);
452 }
453 })
454 }
455
456 fn replace_command(
457 &mut self,
458 action: &ReplaceCommand,
459 window: &mut Window,
460 cx: &mut Context<Self>,
461 ) {
462 let replacement = action.replacement.clone();
463 let Some(((pane, workspace), editor)) = self
464 .pane(window, cx)
465 .zip(self.workspace(window))
466 .zip(self.editor())
467 else {
468 return;
469 };
470 if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
471 let range = action.range.buffer_range(vim, editor, window, cx)?;
472 let snapshot = editor.snapshot(window, cx);
473 let snapshot = snapshot.buffer_snapshot();
474 let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
475 let range = snapshot.anchor_before(Point::new(range.start.0, 0))
476 ..snapshot.anchor_after(end_point);
477 editor.set_search_within_ranges(&[range], cx);
478 anyhow::Ok(())
479 }) {
480 workspace.update(cx, |workspace, cx| {
481 result.notify_err(workspace, cx);
482 })
483 }
484 let Some(search_bar) = pane.update(cx, |pane, cx| {
485 pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
486 }) else {
487 return;
488 };
489 let mut options = SearchOptions::REGEX;
490 let search = search_bar.update(cx, |search_bar, cx| {
491 if !search_bar.show(window, cx) {
492 return None;
493 }
494
495 let search = if replacement.search.is_empty() {
496 search_bar.query(cx)
497 } else {
498 replacement.search
499 };
500
501 if let Some(case) = replacement.case_sensitive {
502 options.set(SearchOptions::CASE_SENSITIVE, case)
503 } else if search_bar.should_use_smartcase_search(cx) {
504 options.set(
505 SearchOptions::CASE_SENSITIVE,
506 search_bar.is_contains_uppercase(&search),
507 );
508 } else {
509 // Fallback: no explicit i/I flags and smartcase disabled;
510 // use global editor.search.case_sensitive.
511 options.set(
512 SearchOptions::CASE_SENSITIVE,
513 EditorSettings::get_global(cx).search.case_sensitive,
514 )
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}