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