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