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