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