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()?.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::{DisplayPoint, display_map::DisplayRow};
649
650 use indoc::indoc;
651 use search::BufferSearchBar;
652 use settings::SettingsStore;
653
654 #[gpui::test]
655 async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
656 let mut cx = VimTestContext::new(cx, true).await;
657 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
658
659 cx.simulate_keystrokes("*");
660 cx.run_until_parked();
661 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
662
663 cx.simulate_keystrokes("*");
664 cx.run_until_parked();
665 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
666
667 cx.simulate_keystrokes("#");
668 cx.run_until_parked();
669 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
670
671 cx.simulate_keystrokes("#");
672 cx.run_until_parked();
673 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
674
675 cx.simulate_keystrokes("2 *");
676 cx.run_until_parked();
677 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
678
679 cx.simulate_keystrokes("g *");
680 cx.run_until_parked();
681 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
682
683 cx.simulate_keystrokes("n");
684 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
685
686 cx.simulate_keystrokes("g #");
687 cx.run_until_parked();
688 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
689 }
690
691 #[gpui::test]
692 async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
693 let mut cx = VimTestContext::new(cx, true).await;
694
695 cx.update_global(|store: &mut SettingsStore, cx| {
696 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
697 });
698
699 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
700
701 cx.simulate_keystrokes("*");
702 cx.run_until_parked();
703 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
704
705 cx.simulate_keystrokes("*");
706 cx.run_until_parked();
707 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
708
709 cx.simulate_keystrokes("#");
710 cx.run_until_parked();
711 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
712
713 cx.simulate_keystrokes("3 *");
714 cx.run_until_parked();
715 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
716
717 cx.simulate_keystrokes("g *");
718 cx.run_until_parked();
719 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
720
721 cx.simulate_keystrokes("n");
722 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
723
724 cx.simulate_keystrokes("g #");
725 cx.run_until_parked();
726 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
727 }
728
729 #[gpui::test]
730 async fn test_search(cx: &mut gpui::TestAppContext) {
731 let mut cx = VimTestContext::new(cx, true).await;
732
733 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
734 cx.simulate_keystrokes("/ c c");
735
736 let search_bar = cx.workspace(|workspace, _, cx| {
737 workspace
738 .active_pane()
739 .read(cx)
740 .toolbar()
741 .read(cx)
742 .item_of_type::<BufferSearchBar>()
743 .expect("Buffer search bar should be deployed")
744 });
745
746 cx.update_entity(search_bar, |bar, _window, cx| {
747 assert_eq!(bar.query(cx), "cc");
748 });
749
750 cx.run_until_parked();
751
752 cx.update_editor(|editor, window, cx| {
753 let highlights = editor.all_text_background_highlights(window, cx);
754 assert_eq!(3, highlights.len());
755 assert_eq!(
756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
757 highlights[0].0
758 )
759 });
760
761 cx.simulate_keystrokes("enter");
762 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
763
764 // n to go to next/N to go to previous
765 cx.simulate_keystrokes("n");
766 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
767 cx.simulate_keystrokes("shift-n");
768 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
769
770 // ?<enter> to go to previous
771 cx.simulate_keystrokes("? enter");
772 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
773 cx.simulate_keystrokes("? enter");
774 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
775
776 // /<enter> to go to next
777 cx.simulate_keystrokes("/ enter");
778 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
779
780 // ?{search}<enter> to search backwards
781 cx.simulate_keystrokes("? b enter");
782 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
783
784 // works with counts
785 cx.simulate_keystrokes("4 / c");
786 cx.simulate_keystrokes("enter");
787 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
788
789 // check that searching resumes from cursor, not previous match
790 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
791 cx.simulate_keystrokes("/ d");
792 cx.simulate_keystrokes("enter");
793 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
794 cx.update_editor(|editor, window, cx| {
795 editor.move_to_beginning(&Default::default(), window, cx)
796 });
797 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
798 cx.simulate_keystrokes("/ b");
799 cx.simulate_keystrokes("enter");
800 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
801
802 // check that searching switches to normal mode if in visual mode
803 cx.set_state("ˇone two one", Mode::Normal);
804 cx.simulate_keystrokes("v l l");
805 cx.assert_editor_state("«oneˇ» two one");
806 cx.simulate_keystrokes("*");
807 cx.assert_state("one two ˇone", Mode::Normal);
808
809 // check that a backward search after last match works correctly
810 cx.set_state("aa\naa\nbbˇ", Mode::Normal);
811 cx.simulate_keystrokes("? a a");
812 cx.simulate_keystrokes("enter");
813 cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
814
815 // check that searching with unable search wrap
816 cx.update_global(|store: &mut SettingsStore, cx| {
817 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
818 });
819 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
820 cx.simulate_keystrokes("/ c c enter");
821
822 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
823
824 // n to go to next/N to go to previous
825 cx.simulate_keystrokes("n");
826 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
827 cx.simulate_keystrokes("shift-n");
828 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
829
830 // ?<enter> to go to previous
831 cx.simulate_keystrokes("? enter");
832 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
833 cx.simulate_keystrokes("? enter");
834 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
835 }
836
837 #[gpui::test]
838 async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
839 let mut cx = VimTestContext::new(cx, false).await;
840 cx.cx.set_state("ˇone one one one");
841 cx.run_until_parked();
842 cx.simulate_keystrokes("cmd-f");
843 cx.run_until_parked();
844
845 cx.assert_editor_state("«oneˇ» one one one");
846 cx.simulate_keystrokes("enter");
847 cx.assert_editor_state("one «oneˇ» one one");
848 cx.simulate_keystrokes("shift-enter");
849 cx.assert_editor_state("«oneˇ» one one one");
850 }
851
852 #[gpui::test]
853 async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
854 let mut cx = NeovimBackedTestContext::new(cx).await;
855
856 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
857 cx.simulate_shared_keystrokes("v 3 l *").await;
858 cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
859 }
860
861 #[gpui::test]
862 async fn test_d_search(cx: &mut gpui::TestAppContext) {
863 let mut cx = NeovimBackedTestContext::new(cx).await;
864
865 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
866 cx.simulate_shared_keystrokes("d / c d").await;
867 cx.simulate_shared_keystrokes("enter").await;
868 cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
869 }
870
871 #[gpui::test]
872 async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
873 let mut cx = NeovimBackedTestContext::new(cx).await;
874
875 cx.set_shared_state("ˇa b a b a b a").await;
876 cx.simulate_shared_keystrokes("*").await;
877 cx.simulate_shared_keystrokes("n").await;
878 cx.shared_state().await.assert_eq("a b a b ˇa b a");
879 cx.simulate_shared_keystrokes("#").await;
880 cx.shared_state().await.assert_eq("a b ˇa b a b a");
881 cx.simulate_shared_keystrokes("n").await;
882 cx.shared_state().await.assert_eq("ˇa b a b a b a");
883 }
884
885 #[gpui::test]
886 async fn test_v_search(cx: &mut gpui::TestAppContext) {
887 let mut cx = NeovimBackedTestContext::new(cx).await;
888
889 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
890 cx.simulate_shared_keystrokes("v / c d").await;
891 cx.simulate_shared_keystrokes("enter").await;
892 cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
893
894 cx.set_shared_state("a a aˇ a a a").await;
895 cx.simulate_shared_keystrokes("v / a").await;
896 cx.simulate_shared_keystrokes("enter").await;
897 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
898 cx.simulate_shared_keystrokes("/ enter").await;
899 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
900 cx.simulate_shared_keystrokes("? enter").await;
901 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
902 cx.simulate_shared_keystrokes("? enter").await;
903 cx.shared_state().await.assert_eq("a a «ˇa »a a a");
904 cx.simulate_shared_keystrokes("/ enter").await;
905 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
906 cx.simulate_shared_keystrokes("/ enter").await;
907 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
908 }
909
910 #[gpui::test]
911 async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
912 let mut cx = NeovimBackedTestContext::new(cx).await;
913
914 cx.set_shared_state("ˇaa aa").await;
915 cx.simulate_shared_keystrokes("v / a a").await;
916 cx.simulate_shared_keystrokes("enter").await;
917 cx.shared_state().await.assert_eq("«aa aˇ»a");
918 }
919
920 #[gpui::test]
921 async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
922 let mut cx = NeovimBackedTestContext::new(cx).await;
923
924 cx.set_shared_state(indoc! {
925 "ˇone two
926 three four
927 five six
928 "
929 })
930 .await;
931 cx.simulate_shared_keystrokes("ctrl-v j / f").await;
932 cx.simulate_shared_keystrokes("enter").await;
933 cx.shared_state().await.assert_eq(indoc! {
934 "«one twoˇ»
935 «three fˇ»our
936 five six
937 "
938 });
939 }
940
941 #[gpui::test]
942 async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
943 let mut cx = NeovimBackedTestContext::new(cx).await;
944
945 cx.set_shared_state(indoc! {
946 "ˇa
947 a
948 a
949 a
950 a
951 a
952 a
953 "
954 })
955 .await;
956 cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
957 cx.simulate_shared_keystrokes("enter").await;
958 cx.shared_state().await.assert_eq(indoc! {
959 "a
960 ba
961 ba
962 ba
963 ˇba
964 a
965 a
966 "
967 });
968
969 cx.simulate_shared_keystrokes("/ a").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 bˇa
977 a
978 a
979 "
980 });
981 }
982
983 #[gpui::test]
984 async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
985 let mut cx = NeovimBackedTestContext::new(cx).await;
986 cx.set_shared_state(indoc! {
987 "ˇaa aa aa"
988 })
989 .await;
990
991 cx.simulate_shared_keystrokes("/ a a").await;
992 cx.simulate_shared_keystrokes("enter").await;
993
994 cx.shared_state().await.assert_eq(indoc! {
995 "aa ˇaa aa"
996 });
997
998 cx.simulate_shared_keystrokes("left / a a").await;
999 cx.simulate_shared_keystrokes("enter").await;
1000
1001 cx.shared_state().await.assert_eq(indoc! {
1002 "aa ˇaa aa"
1003 });
1004 }
1005
1006 #[gpui::test]
1007 async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1008 let mut cx = NeovimBackedTestContext::new(cx).await;
1009 cx.set_shared_state(indoc! {
1010 "ˇaa
1011 bb
1012 aa"
1013 })
1014 .await;
1015
1016 cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1017 cx.simulate_shared_keystrokes("enter").await;
1018
1019 cx.shared_state().await.assert_eq(indoc! {
1020 "ˇaa
1021 bb
1022 aa"
1023 });
1024
1025 let search_bar = cx.update_workspace(|workspace, _, cx| {
1026 workspace.active_pane().update(cx, |pane, cx| {
1027 pane.toolbar()
1028 .read(cx)
1029 .item_of_type::<BufferSearchBar>()
1030 .unwrap()
1031 })
1032 });
1033 cx.update_entity(search_bar, |search_bar, _, cx| {
1034 assert!(!search_bar.is_dismissed());
1035 assert_eq!(search_bar.query(cx), "bb".to_string());
1036 assert_eq!(search_bar.replacement(cx), "dd".to_string());
1037 })
1038 }
1039
1040 #[gpui::test]
1041 async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1042 let mut cx = NeovimBackedTestContext::new(cx).await;
1043 cx.set_shared_state(indoc! {
1044 "ˇaa aa aa aa
1045 aa
1046 aa"
1047 })
1048 .await;
1049
1050 cx.simulate_shared_keystrokes(": s / a a / b b").await;
1051 cx.simulate_shared_keystrokes("enter").await;
1052 cx.shared_state().await.assert_eq(indoc! {
1053 "ˇbb aa aa aa
1054 aa
1055 aa"
1056 });
1057 cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1058 cx.simulate_shared_keystrokes("enter").await;
1059 cx.shared_state().await.assert_eq(indoc! {
1060 "ˇbb bb bb bb
1061 aa
1062 aa"
1063 });
1064 }
1065
1066 #[gpui::test]
1067 async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1068 let mut cx = VimTestContext::new(cx, true).await;
1069 cx.set_state(
1070 indoc! {
1071 "ˇaa
1072 aa
1073 aa"
1074 },
1075 Mode::Normal,
1076 );
1077
1078 cx.simulate_keystrokes("v j : s / a a / d d / c");
1079 cx.simulate_keystrokes("enter");
1080
1081 cx.assert_state(
1082 indoc! {
1083 "ˇaa
1084 aa
1085 aa"
1086 },
1087 Mode::Normal,
1088 );
1089
1090 cx.simulate_keystrokes("enter");
1091
1092 cx.assert_state(
1093 indoc! {
1094 "dd
1095 ˇaa
1096 aa"
1097 },
1098 Mode::Normal,
1099 );
1100
1101 cx.simulate_keystrokes("enter");
1102 cx.assert_state(
1103 indoc! {
1104 "dd
1105 ddˇ
1106 aa"
1107 },
1108 Mode::Normal,
1109 );
1110 cx.simulate_keystrokes("enter");
1111 cx.assert_state(
1112 indoc! {
1113 "dd
1114 ddˇ
1115 aa"
1116 },
1117 Mode::Normal,
1118 );
1119 }
1120
1121 #[gpui::test]
1122 async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1123 let mut cx = NeovimBackedTestContext::new(cx).await;
1124
1125 cx.set_shared_state(indoc! {
1126 "ˇa
1127 a
1128 a
1129 a
1130 a
1131 a
1132 a
1133 "
1134 })
1135 .await;
1136 cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1137 cx.simulate_shared_keystrokes("enter").await;
1138 cx.shared_state().await.assert_eq(indoc! {
1139 "a
1140 b
1141 b
1142 b
1143 ˇb
1144 a
1145 a
1146 "
1147 });
1148 cx.executor().advance_clock(Duration::from_millis(250));
1149 cx.run_until_parked();
1150
1151 cx.simulate_shared_keystrokes("/ a enter").await;
1152 cx.shared_state().await.assert_eq(indoc! {
1153 "a
1154 b
1155 b
1156 b
1157 b
1158 ˇa
1159 a
1160 "
1161 });
1162 }
1163}