1use crate::{
2 history::SearchHistory,
3 mode::{next_mode, SearchMode},
4 search_bar::{render_nav_button, render_search_mode_button},
5 ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery,
6 ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
7 ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
8};
9use collections::HashMap;
10use editor::{Editor, EditorMode};
11use futures::channel::oneshot;
12use gpui::{
13 actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
14 ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
15 WeakView, WindowContext,
16};
17use project::search::SearchQuery;
18use serde::Deserialize;
19use std::{any::Any, sync::Arc};
20
21use ui::{h_stack, Icon, IconButton, IconElement};
22use util::ResultExt;
23use workspace::{
24 item::ItemHandle,
25 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
26 ToolbarItemLocation, ToolbarItemView,
27};
28
29#[derive(PartialEq, Clone, Deserialize, Default, Action)]
30pub struct Deploy {
31 pub focus: bool,
32}
33
34actions!(Dismiss, FocusEditor);
35
36pub enum Event {
37 UpdateLocation,
38}
39
40pub fn init(cx: &mut AppContext) {
41 cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx))
42 .detach();
43}
44
45pub struct BufferSearchBar {
46 query_editor: View<Editor>,
47 replacement_editor: View<Editor>,
48 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
49 active_match_index: Option<usize>,
50 active_searchable_item_subscription: Option<Subscription>,
51 active_search: Option<Arc<SearchQuery>>,
52 searchable_items_with_matches:
53 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
54 pending_search: Option<Task<()>>,
55 search_options: SearchOptions,
56 default_options: SearchOptions,
57 query_contains_error: bool,
58 dismissed: bool,
59 search_history: SearchHistory,
60 current_mode: SearchMode,
61 replace_enabled: bool,
62}
63
64impl EventEmitter<Event> for BufferSearchBar {}
65impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
66impl Render for BufferSearchBar {
67 type Element = Div;
68 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
69 // let query_container_style = if self.query_contains_error {
70 // theme.search.invalid_editor
71 // } else {
72 // theme.search.editor.input.container
73 // };
74 if self.dismissed {
75 return div();
76 }
77 let supported_options = self.supported_options();
78
79 let previous_query_keystrokes = cx
80 .bindings_for_action(&PreviousHistoryQuery {})
81 .into_iter()
82 .next()
83 .map(|binding| {
84 binding
85 .keystrokes()
86 .iter()
87 .map(|k| k.to_string())
88 .collect::<Vec<_>>()
89 });
90 let next_query_keystrokes = cx
91 .bindings_for_action(&NextHistoryQuery {})
92 .into_iter()
93 .next()
94 .map(|binding| {
95 binding
96 .keystrokes()
97 .iter()
98 .map(|k| k.to_string())
99 .collect::<Vec<_>>()
100 });
101 let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
102 (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
103 format!(
104 "Search ({}/{} for previous/next query)",
105 previous_query_keystrokes.join(" "),
106 next_query_keystrokes.join(" ")
107 )
108 }
109 (None, Some(next_query_keystrokes)) => {
110 format!(
111 "Search ({} for next query)",
112 next_query_keystrokes.join(" ")
113 )
114 }
115 (Some(previous_query_keystrokes), None) => {
116 format!(
117 "Search ({} for previous query)",
118 previous_query_keystrokes.join(" ")
119 )
120 }
121 (None, None) => String::new(),
122 };
123 let new_placeholder_text = Arc::from(new_placeholder_text);
124 self.query_editor.update(cx, |editor, cx| {
125 editor.set_placeholder_text(new_placeholder_text, cx);
126 });
127 self.replacement_editor.update(cx, |editor, cx| {
128 editor.set_placeholder_text("Replace with...", cx);
129 });
130
131 let search_button_for_mode = |mode| {
132 let is_active = self.current_mode == mode;
133
134 render_search_mode_button(
135 mode,
136 is_active,
137 cx.listener(move |this, _, cx| {
138 this.activate_search_mode(mode, cx);
139 }),
140 )
141 };
142 let search_option_button = |option| {
143 let is_active = self.search_options.contains(option);
144 option.as_button(is_active)
145 };
146 let match_count = self
147 .active_searchable_item
148 .as_ref()
149 .and_then(|searchable_item| {
150 if self.query(cx).is_empty() {
151 return None;
152 }
153 let matches = self
154 .searchable_items_with_matches
155 .get(&searchable_item.downgrade())?;
156 let message = if let Some(match_ix) = self.active_match_index {
157 format!("{}/{}", match_ix + 1, matches.len())
158 } else {
159 "No matches".to_string()
160 };
161
162 Some(ui::Label::new(message))
163 });
164 let nav_button_for_direction = |icon, direction| {
165 render_nav_button(
166 icon,
167 self.active_match_index.is_some(),
168 cx.listener(move |this, _, cx| match direction {
169 Direction::Prev => this.select_prev_match(&Default::default(), cx),
170 Direction::Next => this.select_next_match(&Default::default(), cx),
171 }),
172 )
173 };
174 let should_show_replace_input = self.replace_enabled && supported_options.replacement;
175 let replace_all = should_show_replace_input
176 .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
177 let replace_next = should_show_replace_input
178 .then(|| super::render_replace_button(ReplaceNext, ui::Icon::Replace));
179 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
180
181 h_stack()
182 .key_context("BufferSearchBar")
183 .when(in_replace, |this| {
184 this.key_context("in_replace")
185 .on_action(cx.listener(Self::replace_next))
186 .on_action(cx.listener(Self::replace_all))
187 })
188 .on_action(cx.listener(Self::previous_history_query))
189 .on_action(cx.listener(Self::next_history_query))
190 .on_action(cx.listener(Self::dismiss))
191 .w_full()
192 .p_1()
193 .child(
194 div()
195 .flex()
196 .flex_1()
197 .border_1()
198 .border_color(red())
199 .rounded_md()
200 .items_center()
201 .child(IconElement::new(Icon::MagnifyingGlass))
202 .child(self.query_editor.clone())
203 .children(
204 supported_options
205 .case
206 .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
207 )
208 .children(
209 supported_options
210 .word
211 .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
212 ),
213 )
214 .child(
215 h_stack()
216 .flex_none()
217 .child(
218 h_stack()
219 .child(search_button_for_mode(SearchMode::Text))
220 .child(search_button_for_mode(SearchMode::Regex)),
221 )
222 .when(supported_options.replacement, |this| {
223 this.child(super::toggle_replace_button(self.replace_enabled))
224 }),
225 )
226 .child(
227 h_stack()
228 .gap_0p5()
229 .flex_1()
230 .when(self.replace_enabled, |this| {
231 this.child(self.replacement_editor.clone())
232 .children(replace_next)
233 .children(replace_all)
234 }),
235 )
236 .child(
237 h_stack()
238 .gap_0p5()
239 .flex_none()
240 .child(self.render_action_button())
241 .children(match_count)
242 .child(nav_button_for_direction(
243 ui::Icon::ChevronLeft,
244 Direction::Prev,
245 ))
246 .child(nav_button_for_direction(
247 ui::Icon::ChevronRight,
248 Direction::Next,
249 )),
250 )
251 }
252}
253
254impl ToolbarItemView for BufferSearchBar {
255 fn set_active_pane_item(
256 &mut self,
257 item: Option<&dyn ItemHandle>,
258 cx: &mut ViewContext<Self>,
259 ) -> ToolbarItemLocation {
260 cx.notify();
261 self.active_searchable_item_subscription.take();
262 self.active_searchable_item.take();
263
264 self.pending_search.take();
265
266 if let Some(searchable_item_handle) =
267 item.and_then(|item| item.to_searchable_item_handle(cx))
268 {
269 let this = cx.view().downgrade();
270
271 searchable_item_handle
272 .subscribe_to_search_events(
273 cx,
274 Box::new(move |search_event, cx| {
275 if let Some(this) = this.upgrade() {
276 this.update(cx, |this, cx| {
277 this.on_active_searchable_item_event(search_event, cx)
278 });
279 }
280 }),
281 )
282 .detach();
283
284 self.active_searchable_item = Some(searchable_item_handle);
285 let _ = self.update_matches(cx);
286 if !self.dismissed {
287 return ToolbarItemLocation::Secondary;
288 }
289 }
290 ToolbarItemLocation::Hidden
291 }
292
293 fn row_count(&self, _: &WindowContext<'_>) -> usize {
294 1
295 }
296}
297
298impl BufferSearchBar {
299 pub fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
300 if editor.mode() != EditorMode::Full {
301 return;
302 };
303
304 let handle = cx.view().downgrade();
305
306 editor.register_action(move |a: &Deploy, cx| {
307 let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else {
308 return;
309 };
310
311 pane.update(cx, |this, cx| {
312 this.toolbar().update(cx, |this, cx| {
313 if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
314 search_bar.update(cx, |this, cx| {
315 if this.is_dismissed() {
316 this.show(cx);
317 } else {
318 this.dismiss(&Dismiss, cx);
319 }
320 });
321 return;
322 }
323 let view = cx.build_view(|cx| BufferSearchBar::new(cx));
324 this.add_item(view.clone(), cx);
325 view.update(cx, |this, cx| this.deploy(a, cx));
326 cx.notify();
327 })
328 });
329 });
330 fn register_action<A: Action>(
331 editor: &mut Editor,
332 handle: WeakView<Editor>,
333 update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
334 ) {
335 editor.register_action(move |action: &A, cx| {
336 let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx))
337 else {
338 return;
339 };
340 pane.update(cx, move |this, cx| {
341 this.toolbar().update(cx, move |this, cx| {
342 if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
343 search_bar.update(cx, move |this, cx| update(this, action, cx));
344 cx.notify();
345 }
346 })
347 });
348 });
349 }
350
351 let handle = cx.view().downgrade();
352 register_action(
353 editor,
354 handle.clone(),
355 |this, action: &ToggleCaseSensitive, cx| {
356 if this.supported_options().case {
357 this.toggle_case_sensitive(action, cx);
358 }
359 },
360 );
361 register_action(
362 editor,
363 handle.clone(),
364 |this, action: &ToggleWholeWord, cx| {
365 if this.supported_options().word {
366 this.toggle_whole_word(action, cx);
367 }
368 },
369 );
370 register_action(
371 editor,
372 handle.clone(),
373 |this, action: &ToggleReplace, cx| {
374 if this.supported_options().replacement {
375 this.toggle_replace(action, cx);
376 }
377 },
378 );
379 register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| {
380 if this.supported_options().regex {
381 this.activate_search_mode(SearchMode::Regex, cx);
382 }
383 });
384 register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| {
385 this.activate_search_mode(SearchMode::Text, cx);
386 });
387 register_action(editor, handle.clone(), |this, action: &CycleMode, cx| {
388 if this.supported_options().regex {
389 // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
390 // cycling.
391 this.cycle_mode(action, cx)
392 }
393 });
394 register_action(
395 editor,
396 handle.clone(),
397 |this, action: &SelectNextMatch, cx| {
398 this.select_next_match(action, cx);
399 },
400 );
401 register_action(
402 editor,
403 handle.clone(),
404 |this, action: &SelectPrevMatch, cx| {
405 this.select_prev_match(action, cx);
406 },
407 );
408 register_action(
409 editor,
410 handle.clone(),
411 |this, action: &SelectAllMatches, cx| {
412 this.select_all_matches(action, cx);
413 },
414 );
415 register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| {
416 if !this.dismissed {
417 this.dismiss(&Dismiss, cx);
418 return;
419 }
420 cx.propagate();
421 });
422 }
423 pub fn new(cx: &mut ViewContext<Self>) -> Self {
424 let query_editor = cx.build_view(|cx| Editor::single_line(cx));
425 cx.subscribe(&query_editor, Self::on_query_editor_event)
426 .detach();
427 let replacement_editor = cx.build_view(|cx| Editor::single_line(cx));
428 cx.subscribe(&replacement_editor, Self::on_query_editor_event)
429 .detach();
430 Self {
431 query_editor,
432 replacement_editor,
433 active_searchable_item: None,
434 active_searchable_item_subscription: None,
435 active_match_index: None,
436 searchable_items_with_matches: Default::default(),
437 default_options: SearchOptions::NONE,
438 search_options: SearchOptions::NONE,
439 pending_search: None,
440 query_contains_error: false,
441 dismissed: true,
442 search_history: SearchHistory::default(),
443 current_mode: SearchMode::default(),
444 active_search: None,
445 replace_enabled: false,
446 }
447 }
448
449 pub fn is_dismissed(&self) -> bool {
450 self.dismissed
451 }
452
453 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
454 self.dismissed = true;
455 for searchable_item in self.searchable_items_with_matches.keys() {
456 if let Some(searchable_item) =
457 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
458 {
459 searchable_item.clear_matches(cx);
460 }
461 }
462 if let Some(active_editor) = self.active_searchable_item.as_ref() {
463 let handle = active_editor.focus_handle(cx);
464 cx.focus(&handle);
465 }
466 cx.emit(Event::UpdateLocation);
467 cx.notify();
468 }
469
470 pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
471 if self.show(cx) {
472 self.search_suggested(cx);
473 if deploy.focus {
474 self.select_query(cx);
475 let handle = cx.focus_handle();
476 cx.focus(&handle);
477 }
478 return true;
479 }
480
481 false
482 }
483
484 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
485 if self.active_searchable_item.is_none() {
486 return false;
487 }
488 self.dismissed = false;
489 cx.notify();
490 cx.emit(Event::UpdateLocation);
491 true
492 }
493
494 fn supported_options(&self) -> workspace::searchable::SearchOptions {
495 self.active_searchable_item
496 .as_deref()
497 .map(SearchableItemHandle::supported_options)
498 .unwrap_or_default()
499 }
500 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
501 let search = self
502 .query_suggestion(cx)
503 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
504
505 if let Some(search) = search {
506 cx.spawn(|this, mut cx| async move {
507 search.await?;
508 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
509 })
510 .detach_and_log_err(cx);
511 }
512 }
513
514 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
515 if let Some(match_ix) = self.active_match_index {
516 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
517 if let Some(matches) = self
518 .searchable_items_with_matches
519 .get(&active_searchable_item.downgrade())
520 {
521 active_searchable_item.activate_match(match_ix, matches, cx)
522 }
523 }
524 }
525 }
526
527 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
528 self.query_editor.update(cx, |query_editor, cx| {
529 query_editor.select_all(&Default::default(), cx);
530 });
531 }
532
533 pub fn query(&self, cx: &WindowContext) -> String {
534 self.query_editor.read(cx).text(cx)
535 }
536 pub fn replacement(&self, cx: &WindowContext) -> String {
537 self.replacement_editor.read(cx).text(cx)
538 }
539 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
540 self.active_searchable_item
541 .as_ref()
542 .map(|searchable_item| searchable_item.query_suggestion(cx))
543 .filter(|suggestion| !suggestion.is_empty())
544 }
545
546 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
547 if replacement.is_none() {
548 self.replace_enabled = false;
549 return;
550 }
551 self.replace_enabled = true;
552 self.replacement_editor
553 .update(cx, |replacement_editor, cx| {
554 replacement_editor
555 .buffer()
556 .update(cx, |replacement_buffer, cx| {
557 let len = replacement_buffer.len(cx);
558 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
559 });
560 });
561 }
562
563 pub fn search(
564 &mut self,
565 query: &str,
566 options: Option<SearchOptions>,
567 cx: &mut ViewContext<Self>,
568 ) -> oneshot::Receiver<()> {
569 let options = options.unwrap_or(self.default_options);
570 if query != self.query(cx) || self.search_options != options {
571 self.query_editor.update(cx, |query_editor, cx| {
572 query_editor.buffer().update(cx, |query_buffer, cx| {
573 let len = query_buffer.len(cx);
574 query_buffer.edit([(0..len, query)], None, cx);
575 });
576 });
577 self.search_options = options;
578 self.query_contains_error = false;
579 self.clear_matches(cx);
580 cx.notify();
581 }
582 self.update_matches(cx)
583 }
584
585 fn render_action_button(&self) -> impl IntoElement {
586 // let tooltip_style = theme.tooltip.clone();
587
588 // let style = theme.search.action_button.clone();
589
590 IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches))
591 }
592
593 pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
594 assert_ne!(
595 mode,
596 SearchMode::Semantic,
597 "Semantic search is not supported in buffer search"
598 );
599 if mode == self.current_mode {
600 return;
601 }
602 self.current_mode = mode;
603 let _ = self.update_matches(cx);
604 cx.notify();
605 }
606
607 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
608 if let Some(active_editor) = self.active_searchable_item.as_ref() {
609 let handle = active_editor.focus_handle(cx);
610 cx.focus(&handle);
611 }
612 }
613
614 fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
615 self.search_options.toggle(search_option);
616 self.default_options = self.search_options;
617 let _ = self.update_matches(cx);
618 cx.notify();
619 }
620
621 pub fn set_search_options(
622 &mut self,
623 search_options: SearchOptions,
624 cx: &mut ViewContext<Self>,
625 ) {
626 self.search_options = search_options;
627 cx.notify();
628 }
629
630 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
631 self.select_match(Direction::Next, 1, cx);
632 }
633
634 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
635 self.select_match(Direction::Prev, 1, cx);
636 }
637
638 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
639 if !self.dismissed && self.active_match_index.is_some() {
640 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
641 if let Some(matches) = self
642 .searchable_items_with_matches
643 .get(&searchable_item.downgrade())
644 {
645 searchable_item.select_matches(matches, cx);
646 self.focus_editor(&FocusEditor, cx);
647 }
648 }
649 }
650 }
651
652 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
653 if let Some(index) = self.active_match_index {
654 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
655 if let Some(matches) = self
656 .searchable_items_with_matches
657 .get(&searchable_item.downgrade())
658 {
659 let new_match_index = searchable_item
660 .match_index_for_direction(matches, index, direction, count, cx);
661
662 searchable_item.update_matches(matches, cx);
663 searchable_item.activate_match(new_match_index, matches, cx);
664 }
665 }
666 }
667 }
668
669 pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
670 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
671 if let Some(matches) = self
672 .searchable_items_with_matches
673 .get(&searchable_item.downgrade())
674 {
675 if matches.len() == 0 {
676 return;
677 }
678 let new_match_index = matches.len() - 1;
679 searchable_item.update_matches(matches, cx);
680 searchable_item.activate_match(new_match_index, matches, cx);
681 }
682 }
683 }
684
685 fn on_query_editor_event(
686 &mut self,
687 _: View<Editor>,
688 event: &editor::EditorEvent,
689 cx: &mut ViewContext<Self>,
690 ) {
691 if let editor::EditorEvent::Edited { .. } = event {
692 self.query_contains_error = false;
693 self.clear_matches(cx);
694 let search = self.update_matches(cx);
695 cx.spawn(|this, mut cx| async move {
696 search.await?;
697 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
698 })
699 .detach_and_log_err(cx);
700 }
701 }
702
703 fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
704 match event {
705 SearchEvent::MatchesInvalidated => {
706 let _ = self.update_matches(cx);
707 }
708 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
709 }
710 }
711
712 fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
713 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
714 }
715 fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
716 self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
717 }
718 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
719 let mut active_item_matches = None;
720 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
721 if let Some(searchable_item) =
722 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
723 {
724 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
725 active_item_matches = Some((searchable_item.downgrade(), matches));
726 } else {
727 searchable_item.clear_matches(cx);
728 }
729 }
730 }
731
732 self.searchable_items_with_matches
733 .extend(active_item_matches);
734 }
735
736 fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
737 let (done_tx, done_rx) = oneshot::channel();
738 let query = self.query(cx);
739 self.pending_search.take();
740
741 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
742 if query.is_empty() {
743 self.active_match_index.take();
744 active_searchable_item.clear_matches(cx);
745 let _ = done_tx.send(());
746 cx.notify();
747 } else {
748 let query: Arc<_> = if self.current_mode == SearchMode::Regex {
749 match SearchQuery::regex(
750 query,
751 self.search_options.contains(SearchOptions::WHOLE_WORD),
752 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
753 false,
754 Vec::new(),
755 Vec::new(),
756 ) {
757 Ok(query) => query.with_replacement(self.replacement(cx)),
758 Err(_) => {
759 self.query_contains_error = true;
760 cx.notify();
761 return done_rx;
762 }
763 }
764 } else {
765 match SearchQuery::text(
766 query,
767 self.search_options.contains(SearchOptions::WHOLE_WORD),
768 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
769 false,
770 Vec::new(),
771 Vec::new(),
772 ) {
773 Ok(query) => query.with_replacement(self.replacement(cx)),
774 Err(_) => {
775 self.query_contains_error = true;
776 cx.notify();
777 return done_rx;
778 }
779 }
780 }
781 .into();
782 self.active_search = Some(query.clone());
783 let query_text = query.as_str().to_string();
784
785 let matches = active_searchable_item.find_matches(query, cx);
786
787 let active_searchable_item = active_searchable_item.downgrade();
788 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
789 let matches = matches.await;
790
791 this.update(&mut cx, |this, cx| {
792 if let Some(active_searchable_item) =
793 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
794 {
795 this.searchable_items_with_matches
796 .insert(active_searchable_item.downgrade(), matches);
797
798 this.update_match_index(cx);
799 this.search_history.add(query_text);
800 if !this.dismissed {
801 let matches = this
802 .searchable_items_with_matches
803 .get(&active_searchable_item.downgrade())
804 .unwrap();
805 active_searchable_item.update_matches(matches, cx);
806 let _ = done_tx.send(());
807 }
808 cx.notify();
809 }
810 })
811 .log_err();
812 }));
813 }
814 }
815 done_rx
816 }
817
818 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
819 let new_index = self
820 .active_searchable_item
821 .as_ref()
822 .and_then(|searchable_item| {
823 let matches = self
824 .searchable_items_with_matches
825 .get(&searchable_item.downgrade())?;
826 searchable_item.active_match_index(matches, cx)
827 });
828 if new_index != self.active_match_index {
829 self.active_match_index = new_index;
830 cx.notify();
831 }
832 }
833
834 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
835 if let Some(new_query) = self.search_history.next().map(str::to_string) {
836 let _ = self.search(&new_query, Some(self.search_options), cx);
837 } else {
838 self.search_history.reset_selection();
839 let _ = self.search("", Some(self.search_options), cx);
840 }
841 }
842
843 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
844 if self.query(cx).is_empty() {
845 if let Some(new_query) = self.search_history.current().map(str::to_string) {
846 let _ = self.search(&new_query, Some(self.search_options), cx);
847 return;
848 }
849 }
850
851 if let Some(new_query) = self.search_history.previous().map(str::to_string) {
852 let _ = self.search(&new_query, Some(self.search_options), cx);
853 }
854 }
855 fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
856 self.activate_search_mode(next_mode(&self.current_mode, false), cx);
857 }
858 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
859 if let Some(_) = &self.active_searchable_item {
860 self.replace_enabled = !self.replace_enabled;
861 if !self.replace_enabled {
862 let handle = self.query_editor.focus_handle(cx);
863 cx.focus(&handle);
864 }
865 cx.notify();
866 }
867 }
868 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
869 let mut should_propagate = true;
870 if !self.dismissed && self.active_search.is_some() {
871 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
872 if let Some(query) = self.active_search.as_ref() {
873 if let Some(matches) = self
874 .searchable_items_with_matches
875 .get(&searchable_item.downgrade())
876 {
877 if let Some(active_index) = self.active_match_index {
878 let query = query
879 .as_ref()
880 .clone()
881 .with_replacement(self.replacement(cx));
882 searchable_item.replace(&matches[active_index], &query, cx);
883 self.select_next_match(&SelectNextMatch, cx);
884 }
885 should_propagate = false;
886 self.focus_editor(&FocusEditor, cx);
887 }
888 }
889 }
890 }
891 if !should_propagate {
892 cx.stop_propagation();
893 }
894 }
895 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
896 if !self.dismissed && self.active_search.is_some() {
897 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
898 if let Some(query) = self.active_search.as_ref() {
899 if let Some(matches) = self
900 .searchable_items_with_matches
901 .get(&searchable_item.downgrade())
902 {
903 let query = query
904 .as_ref()
905 .clone()
906 .with_replacement(self.replacement(cx));
907 for m in matches {
908 searchable_item.replace(m, &query, cx);
909 }
910 }
911 }
912 }
913 }
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use std::ops::Range;
920
921 use super::*;
922 use editor::{DisplayPoint, Editor};
923 use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
924 use language::Buffer;
925 use smol::stream::StreamExt as _;
926 use unindent::Unindent as _;
927
928 fn init_globals(cx: &mut TestAppContext) {
929 cx.update(|cx| {
930 let store = settings::SettingsStore::test(cx);
931 cx.set_global(store);
932 editor::init(cx);
933
934 language::init(cx);
935 theme::init(theme::LoadThemes::JustBase, cx);
936 });
937 }
938 fn init_test(
939 cx: &mut TestAppContext,
940 ) -> (
941 View<Editor>,
942 View<BufferSearchBar>,
943 &mut VisualTestContext<'_>,
944 ) {
945 init_globals(cx);
946 let buffer = cx.build_model(|cx| {
947 Buffer::new(
948 0,
949 cx.entity_id().as_u64(),
950 r#"
951 A regular expression (shortened as regex or regexp;[1] also referred to as
952 rational expression[2][3]) is a sequence of characters that specifies a search
953 pattern in text. Usually such patterns are used by string-searching algorithms
954 for "find" or "find and replace" operations on strings, or for input validation.
955 "#
956 .unindent(),
957 )
958 });
959 let (_, cx) = cx.add_window_view(|_| EmptyView {});
960 let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
961
962 let search_bar = cx.build_view(|cx| {
963 let mut search_bar = BufferSearchBar::new(cx);
964 search_bar.set_active_pane_item(Some(&editor), cx);
965 search_bar.show(cx);
966 search_bar
967 });
968
969 (editor, search_bar, cx)
970 }
971
972 #[gpui::test]
973 async fn test_search_simple(cx: &mut TestAppContext) {
974 let (editor, search_bar, cx) = init_test(cx);
975 // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
976 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
977 background_highlights
978 .into_iter()
979 .map(|(range, _)| range)
980 .collect::<Vec<_>>()
981 };
982 // Search for a string that appears with different casing.
983 // By default, search is case-insensitive.
984 search_bar
985 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
986 .await
987 .unwrap();
988 editor.update(cx, |editor, cx| {
989 assert_eq!(
990 display_points_of(editor.all_text_background_highlights(cx)),
991 &[
992 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
993 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
994 ]
995 );
996 });
997
998 // Switch to a case sensitive search.
999 search_bar.update(cx, |search_bar, cx| {
1000 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1001 });
1002 let mut editor_notifications = cx.notifications(&editor);
1003 editor_notifications.next().await;
1004 editor.update(cx, |editor, cx| {
1005 assert_eq!(
1006 display_points_of(editor.all_text_background_highlights(cx)),
1007 &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1008 );
1009 });
1010
1011 // Search for a string that appears both as a whole word and
1012 // within other words. By default, all results are found.
1013 search_bar
1014 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1015 .await
1016 .unwrap();
1017 editor.update(cx, |editor, cx| {
1018 assert_eq!(
1019 display_points_of(editor.all_text_background_highlights(cx)),
1020 &[
1021 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1022 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1023 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1024 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1025 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1026 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1027 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1028 ]
1029 );
1030 });
1031
1032 // Switch to a whole word search.
1033 search_bar.update(cx, |search_bar, cx| {
1034 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1035 });
1036 let mut editor_notifications = cx.notifications(&editor);
1037 editor_notifications.next().await;
1038 editor.update(cx, |editor, cx| {
1039 assert_eq!(
1040 display_points_of(editor.all_text_background_highlights(cx)),
1041 &[
1042 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1043 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1044 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1045 ]
1046 );
1047 });
1048
1049 editor.update(cx, |editor, cx| {
1050 editor.change_selections(None, cx, |s| {
1051 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1052 });
1053 });
1054 search_bar.update(cx, |search_bar, cx| {
1055 assert_eq!(search_bar.active_match_index, Some(0));
1056 search_bar.select_next_match(&SelectNextMatch, cx);
1057 assert_eq!(
1058 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1059 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1060 );
1061 });
1062 search_bar.update(cx, |search_bar, _| {
1063 assert_eq!(search_bar.active_match_index, Some(0));
1064 });
1065
1066 search_bar.update(cx, |search_bar, cx| {
1067 search_bar.select_next_match(&SelectNextMatch, cx);
1068 assert_eq!(
1069 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1070 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1071 );
1072 });
1073 search_bar.update(cx, |search_bar, _| {
1074 assert_eq!(search_bar.active_match_index, Some(1));
1075 });
1076
1077 search_bar.update(cx, |search_bar, cx| {
1078 search_bar.select_next_match(&SelectNextMatch, cx);
1079 assert_eq!(
1080 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1081 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1082 );
1083 });
1084 search_bar.update(cx, |search_bar, _| {
1085 assert_eq!(search_bar.active_match_index, Some(2));
1086 });
1087
1088 search_bar.update(cx, |search_bar, cx| {
1089 search_bar.select_next_match(&SelectNextMatch, cx);
1090 assert_eq!(
1091 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1092 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1093 );
1094 });
1095 search_bar.update(cx, |search_bar, _| {
1096 assert_eq!(search_bar.active_match_index, Some(0));
1097 });
1098
1099 search_bar.update(cx, |search_bar, cx| {
1100 search_bar.select_prev_match(&SelectPrevMatch, cx);
1101 assert_eq!(
1102 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1103 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1104 );
1105 });
1106 search_bar.update(cx, |search_bar, _| {
1107 assert_eq!(search_bar.active_match_index, Some(2));
1108 });
1109
1110 search_bar.update(cx, |search_bar, cx| {
1111 search_bar.select_prev_match(&SelectPrevMatch, cx);
1112 assert_eq!(
1113 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1114 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1115 );
1116 });
1117 search_bar.update(cx, |search_bar, _| {
1118 assert_eq!(search_bar.active_match_index, Some(1));
1119 });
1120
1121 search_bar.update(cx, |search_bar, cx| {
1122 search_bar.select_prev_match(&SelectPrevMatch, cx);
1123 assert_eq!(
1124 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1125 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1126 );
1127 });
1128 search_bar.update(cx, |search_bar, _| {
1129 assert_eq!(search_bar.active_match_index, Some(0));
1130 });
1131
1132 // Park the cursor in between matches and ensure that going to the previous match selects
1133 // the closest match to the left.
1134 editor.update(cx, |editor, cx| {
1135 editor.change_selections(None, cx, |s| {
1136 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1137 });
1138 });
1139 search_bar.update(cx, |search_bar, cx| {
1140 assert_eq!(search_bar.active_match_index, Some(1));
1141 search_bar.select_prev_match(&SelectPrevMatch, cx);
1142 assert_eq!(
1143 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1144 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1145 );
1146 });
1147 search_bar.update(cx, |search_bar, _| {
1148 assert_eq!(search_bar.active_match_index, Some(0));
1149 });
1150
1151 // Park the cursor in between matches and ensure that going to the next match selects the
1152 // closest match to the right.
1153 editor.update(cx, |editor, cx| {
1154 editor.change_selections(None, cx, |s| {
1155 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1156 });
1157 });
1158 search_bar.update(cx, |search_bar, cx| {
1159 assert_eq!(search_bar.active_match_index, Some(1));
1160 search_bar.select_next_match(&SelectNextMatch, cx);
1161 assert_eq!(
1162 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1163 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1164 );
1165 });
1166 search_bar.update(cx, |search_bar, _| {
1167 assert_eq!(search_bar.active_match_index, Some(1));
1168 });
1169
1170 // Park the cursor after the last match and ensure that going to the previous match selects
1171 // the last match.
1172 editor.update(cx, |editor, cx| {
1173 editor.change_selections(None, cx, |s| {
1174 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1175 });
1176 });
1177 search_bar.update(cx, |search_bar, cx| {
1178 assert_eq!(search_bar.active_match_index, Some(2));
1179 search_bar.select_prev_match(&SelectPrevMatch, cx);
1180 assert_eq!(
1181 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1182 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1183 );
1184 });
1185 search_bar.update(cx, |search_bar, _| {
1186 assert_eq!(search_bar.active_match_index, Some(2));
1187 });
1188
1189 // Park the cursor after the last match and ensure that going to the next match selects the
1190 // first match.
1191 editor.update(cx, |editor, cx| {
1192 editor.change_selections(None, cx, |s| {
1193 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1194 });
1195 });
1196 search_bar.update(cx, |search_bar, cx| {
1197 assert_eq!(search_bar.active_match_index, Some(2));
1198 search_bar.select_next_match(&SelectNextMatch, cx);
1199 assert_eq!(
1200 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1201 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1202 );
1203 });
1204 search_bar.update(cx, |search_bar, _| {
1205 assert_eq!(search_bar.active_match_index, Some(0));
1206 });
1207
1208 // Park the cursor before the first match and ensure that going to the previous match
1209 // selects the last match.
1210 editor.update(cx, |editor, cx| {
1211 editor.change_selections(None, cx, |s| {
1212 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1213 });
1214 });
1215 search_bar.update(cx, |search_bar, cx| {
1216 assert_eq!(search_bar.active_match_index, Some(0));
1217 search_bar.select_prev_match(&SelectPrevMatch, cx);
1218 assert_eq!(
1219 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1220 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1221 );
1222 });
1223 search_bar.update(cx, |search_bar, _| {
1224 assert_eq!(search_bar.active_match_index, Some(2));
1225 });
1226 }
1227
1228 #[gpui::test]
1229 async fn test_search_option_handling(cx: &mut TestAppContext) {
1230 let (editor, search_bar, cx) = init_test(cx);
1231
1232 // show with options should make current search case sensitive
1233 search_bar
1234 .update(cx, |search_bar, cx| {
1235 search_bar.show(cx);
1236 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1237 })
1238 .await
1239 .unwrap();
1240 // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
1241 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1242 background_highlights
1243 .into_iter()
1244 .map(|(range, _)| range)
1245 .collect::<Vec<_>>()
1246 };
1247 editor.update(cx, |editor, cx| {
1248 assert_eq!(
1249 display_points_of(editor.all_text_background_highlights(cx)),
1250 &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1251 );
1252 });
1253
1254 // search_suggested should restore default options
1255 search_bar.update(cx, |search_bar, cx| {
1256 search_bar.search_suggested(cx);
1257 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1258 });
1259
1260 // toggling a search option should update the defaults
1261 search_bar
1262 .update(cx, |search_bar, cx| {
1263 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1264 })
1265 .await
1266 .unwrap();
1267 search_bar.update(cx, |search_bar, cx| {
1268 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1269 });
1270 let mut editor_notifications = cx.notifications(&editor);
1271 editor_notifications.next().await;
1272 editor.update(cx, |editor, cx| {
1273 assert_eq!(
1274 display_points_of(editor.all_text_background_highlights(cx)),
1275 &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
1276 );
1277 });
1278
1279 // defaults should still include whole word
1280 search_bar.update(cx, |search_bar, cx| {
1281 search_bar.search_suggested(cx);
1282 assert_eq!(
1283 search_bar.search_options,
1284 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1285 )
1286 });
1287 }
1288
1289 #[gpui::test]
1290 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1291 init_globals(cx);
1292 let buffer_text = r#"
1293 A regular expression (shortened as regex or regexp;[1] also referred to as
1294 rational expression[2][3]) is a sequence of characters that specifies a search
1295 pattern in text. Usually such patterns are used by string-searching algorithms
1296 for "find" or "find and replace" operations on strings, or for input validation.
1297 "#
1298 .unindent();
1299 let expected_query_matches_count = buffer_text
1300 .chars()
1301 .filter(|c| c.to_ascii_lowercase() == 'a')
1302 .count();
1303 assert!(
1304 expected_query_matches_count > 1,
1305 "Should pick a query with multiple results"
1306 );
1307 let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1308 let window = cx.add_window(|_| EmptyView {});
1309
1310 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1311
1312 let search_bar = window.build_view(cx, |cx| {
1313 let mut search_bar = BufferSearchBar::new(cx);
1314 search_bar.set_active_pane_item(Some(&editor), cx);
1315 search_bar.show(cx);
1316 search_bar
1317 });
1318
1319 window
1320 .update(cx, |_, cx| {
1321 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1322 })
1323 .unwrap()
1324 .await
1325 .unwrap();
1326 let initial_selections = window
1327 .update(cx, |_, cx| {
1328 search_bar.update(cx, |search_bar, cx| {
1329 let handle = search_bar.query_editor.focus_handle(cx);
1330 cx.focus(&handle);
1331 search_bar.activate_current_match(cx);
1332 });
1333 assert!(
1334 !editor.read(cx).is_focused(cx),
1335 "Initially, the editor should not be focused"
1336 );
1337 let initial_selections = editor.update(cx, |editor, cx| {
1338 let initial_selections = editor.selections.display_ranges(cx);
1339 assert_eq!(
1340 initial_selections.len(), 1,
1341 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1342 );
1343 initial_selections
1344 });
1345 search_bar.update(cx, |search_bar, cx| {
1346 assert_eq!(search_bar.active_match_index, Some(0));
1347 let handle = search_bar.query_editor.focus_handle(cx);
1348 cx.focus(&handle);
1349 search_bar.select_all_matches(&SelectAllMatches, cx);
1350 });
1351 assert!(
1352 editor.read(cx).is_focused(cx),
1353 "Should focus editor after successful SelectAllMatches"
1354 );
1355 search_bar.update(cx, |search_bar, cx| {
1356 let all_selections =
1357 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1358 assert_eq!(
1359 all_selections.len(),
1360 expected_query_matches_count,
1361 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1362 );
1363 assert_eq!(
1364 search_bar.active_match_index,
1365 Some(0),
1366 "Match index should not change after selecting all matches"
1367 );
1368 });
1369
1370 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1371 initial_selections
1372 }).unwrap();
1373
1374 window
1375 .update(cx, |_, cx| {
1376 assert!(
1377 editor.read(cx).is_focused(cx),
1378 "Should still have editor focused after SelectNextMatch"
1379 );
1380 search_bar.update(cx, |search_bar, cx| {
1381 let all_selections =
1382 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1383 assert_eq!(
1384 all_selections.len(),
1385 1,
1386 "On next match, should deselect items and select the next match"
1387 );
1388 assert_ne!(
1389 all_selections, initial_selections,
1390 "Next match should be different from the first selection"
1391 );
1392 assert_eq!(
1393 search_bar.active_match_index,
1394 Some(1),
1395 "Match index should be updated to the next one"
1396 );
1397 let handle = search_bar.query_editor.focus_handle(cx);
1398 cx.focus(&handle);
1399 search_bar.select_all_matches(&SelectAllMatches, cx);
1400 });
1401 })
1402 .unwrap();
1403 window
1404 .update(cx, |_, cx| {
1405 assert!(
1406 editor.read(cx).is_focused(cx),
1407 "Should focus editor after successful SelectAllMatches"
1408 );
1409 search_bar.update(cx, |search_bar, cx| {
1410 let all_selections =
1411 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1412 assert_eq!(
1413 all_selections.len(),
1414 expected_query_matches_count,
1415 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1416 );
1417 assert_eq!(
1418 search_bar.active_match_index,
1419 Some(1),
1420 "Match index should not change after selecting all matches"
1421 );
1422 });
1423 search_bar.update(cx, |search_bar, cx| {
1424 search_bar.select_prev_match(&SelectPrevMatch, cx);
1425 });
1426 })
1427 .unwrap();
1428 let last_match_selections = window
1429 .update(cx, |_, cx| {
1430 assert!(
1431 editor.read(cx).is_focused(&cx),
1432 "Should still have editor focused after SelectPrevMatch"
1433 );
1434
1435 search_bar.update(cx, |search_bar, cx| {
1436 let all_selections =
1437 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1438 assert_eq!(
1439 all_selections.len(),
1440 1,
1441 "On previous match, should deselect items and select the previous item"
1442 );
1443 assert_eq!(
1444 all_selections, initial_selections,
1445 "Previous match should be the same as the first selection"
1446 );
1447 assert_eq!(
1448 search_bar.active_match_index,
1449 Some(0),
1450 "Match index should be updated to the previous one"
1451 );
1452 all_selections
1453 })
1454 })
1455 .unwrap();
1456
1457 window
1458 .update(cx, |_, cx| {
1459 search_bar.update(cx, |search_bar, cx| {
1460 let handle = search_bar.query_editor.focus_handle(cx);
1461 cx.focus(&handle);
1462 search_bar.search("abas_nonexistent_match", None, cx)
1463 })
1464 })
1465 .unwrap()
1466 .await
1467 .unwrap();
1468 window
1469 .update(cx, |_, cx| {
1470 search_bar.update(cx, |search_bar, cx| {
1471 search_bar.select_all_matches(&SelectAllMatches, cx);
1472 });
1473 assert!(
1474 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1475 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1476 );
1477 search_bar.update(cx, |search_bar, cx| {
1478 let all_selections =
1479 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1480 assert_eq!(
1481 all_selections, last_match_selections,
1482 "Should not select anything new if there are no matches"
1483 );
1484 assert!(
1485 search_bar.active_match_index.is_none(),
1486 "For no matches, there should be no active match index"
1487 );
1488 });
1489 })
1490 .unwrap();
1491 }
1492
1493 #[gpui::test]
1494 async fn test_search_query_history(cx: &mut TestAppContext) {
1495 //crate::project_search::tests::init_test(cx);
1496 init_globals(cx);
1497 let buffer_text = r#"
1498 A regular expression (shortened as regex or regexp;[1] also referred to as
1499 rational expression[2][3]) is a sequence of characters that specifies a search
1500 pattern in text. Usually such patterns are used by string-searching algorithms
1501 for "find" or "find and replace" operations on strings, or for input validation.
1502 "#
1503 .unindent();
1504 let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1505 let (_, cx) = cx.add_window_view(|_| EmptyView {});
1506
1507 let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1508
1509 let search_bar = cx.build_view(|cx| {
1510 let mut search_bar = BufferSearchBar::new(cx);
1511 search_bar.set_active_pane_item(Some(&editor), cx);
1512 search_bar.show(cx);
1513 search_bar
1514 });
1515
1516 // Add 3 search items into the history.
1517 search_bar
1518 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1519 .await
1520 .unwrap();
1521 search_bar
1522 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1523 .await
1524 .unwrap();
1525 search_bar
1526 .update(cx, |search_bar, cx| {
1527 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1528 })
1529 .await
1530 .unwrap();
1531 // Ensure that the latest search is active.
1532 search_bar.update(cx, |search_bar, cx| {
1533 assert_eq!(search_bar.query(cx), "c");
1534 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1535 });
1536
1537 // Next history query after the latest should set the query to the empty string.
1538 search_bar.update(cx, |search_bar, cx| {
1539 search_bar.next_history_query(&NextHistoryQuery, cx);
1540 });
1541 search_bar.update(cx, |search_bar, cx| {
1542 assert_eq!(search_bar.query(cx), "");
1543 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1544 });
1545 search_bar.update(cx, |search_bar, cx| {
1546 search_bar.next_history_query(&NextHistoryQuery, cx);
1547 });
1548 search_bar.update(cx, |search_bar, cx| {
1549 assert_eq!(search_bar.query(cx), "");
1550 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1551 });
1552
1553 // First previous query for empty current query should set the query to the latest.
1554 search_bar.update(cx, |search_bar, cx| {
1555 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1556 });
1557 search_bar.update(cx, |search_bar, cx| {
1558 assert_eq!(search_bar.query(cx), "c");
1559 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1560 });
1561
1562 // Further previous items should go over the history in reverse order.
1563 search_bar.update(cx, |search_bar, cx| {
1564 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1565 });
1566 search_bar.update(cx, |search_bar, cx| {
1567 assert_eq!(search_bar.query(cx), "b");
1568 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1569 });
1570
1571 // Previous items should never go behind the first history item.
1572 search_bar.update(cx, |search_bar, cx| {
1573 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1574 });
1575 search_bar.update(cx, |search_bar, cx| {
1576 assert_eq!(search_bar.query(cx), "a");
1577 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1578 });
1579 search_bar.update(cx, |search_bar, cx| {
1580 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1581 });
1582 search_bar.update(cx, |search_bar, cx| {
1583 assert_eq!(search_bar.query(cx), "a");
1584 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1585 });
1586
1587 // Next items should go over the history in the original order.
1588 search_bar.update(cx, |search_bar, cx| {
1589 search_bar.next_history_query(&NextHistoryQuery, cx);
1590 });
1591 search_bar.update(cx, |search_bar, cx| {
1592 assert_eq!(search_bar.query(cx), "b");
1593 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1594 });
1595
1596 search_bar
1597 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1598 .await
1599 .unwrap();
1600 search_bar.update(cx, |search_bar, cx| {
1601 assert_eq!(search_bar.query(cx), "ba");
1602 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1603 });
1604
1605 // New search input should add another entry to history and move the selection to the end of the history.
1606 search_bar.update(cx, |search_bar, cx| {
1607 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1608 });
1609 search_bar.update(cx, |search_bar, cx| {
1610 assert_eq!(search_bar.query(cx), "c");
1611 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1612 });
1613 search_bar.update(cx, |search_bar, cx| {
1614 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1615 });
1616 search_bar.update(cx, |search_bar, cx| {
1617 assert_eq!(search_bar.query(cx), "b");
1618 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1619 });
1620 search_bar.update(cx, |search_bar, cx| {
1621 search_bar.next_history_query(&NextHistoryQuery, cx);
1622 });
1623 search_bar.update(cx, |search_bar, cx| {
1624 assert_eq!(search_bar.query(cx), "c");
1625 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1626 });
1627 search_bar.update(cx, |search_bar, cx| {
1628 search_bar.next_history_query(&NextHistoryQuery, cx);
1629 });
1630 search_bar.update(cx, |search_bar, cx| {
1631 assert_eq!(search_bar.query(cx), "ba");
1632 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1633 });
1634 search_bar.update(cx, |search_bar, cx| {
1635 search_bar.next_history_query(&NextHistoryQuery, cx);
1636 });
1637 search_bar.update(cx, |search_bar, cx| {
1638 assert_eq!(search_bar.query(cx), "");
1639 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1640 });
1641 }
1642 #[gpui::test]
1643 async fn test_replace_simple(cx: &mut TestAppContext) {
1644 let (editor, search_bar, cx) = init_test(cx);
1645
1646 search_bar
1647 .update(cx, |search_bar, cx| {
1648 search_bar.search("expression", None, cx)
1649 })
1650 .await
1651 .unwrap();
1652
1653 search_bar.update(cx, |search_bar, cx| {
1654 search_bar.replacement_editor.update(cx, |editor, cx| {
1655 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1656 editor.set_text("expr$1", cx);
1657 });
1658 search_bar.replace_all(&ReplaceAll, cx)
1659 });
1660 assert_eq!(
1661 editor.update(cx, |this, cx| { this.text(cx) }),
1662 r#"
1663 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1664 rational expr$1[2][3]) is a sequence of characters that specifies a search
1665 pattern in text. Usually such patterns are used by string-searching algorithms
1666 for "find" or "find and replace" operations on strings, or for input validation.
1667 "#
1668 .unindent()
1669 );
1670
1671 // Search for word boundaries and replace just a single one.
1672 search_bar
1673 .update(cx, |search_bar, cx| {
1674 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1675 })
1676 .await
1677 .unwrap();
1678
1679 search_bar.update(cx, |search_bar, cx| {
1680 search_bar.replacement_editor.update(cx, |editor, cx| {
1681 editor.set_text("banana", cx);
1682 });
1683 search_bar.replace_next(&ReplaceNext, cx)
1684 });
1685 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1686 assert_eq!(
1687 editor.update(cx, |this, cx| { this.text(cx) }),
1688 r#"
1689 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1690 rational expr$1[2][3]) is a sequence of characters that specifies a search
1691 pattern in text. Usually such patterns are used by string-searching algorithms
1692 for "find" or "find and replace" operations on strings, or for input validation.
1693 "#
1694 .unindent()
1695 );
1696 // Let's turn on regex mode.
1697 search_bar
1698 .update(cx, |search_bar, cx| {
1699 search_bar.activate_search_mode(SearchMode::Regex, cx);
1700 search_bar.search("\\[([^\\]]+)\\]", None, cx)
1701 })
1702 .await
1703 .unwrap();
1704 search_bar.update(cx, |search_bar, cx| {
1705 search_bar.replacement_editor.update(cx, |editor, cx| {
1706 editor.set_text("${1}number", cx);
1707 });
1708 search_bar.replace_all(&ReplaceAll, cx)
1709 });
1710 assert_eq!(
1711 editor.update(cx, |this, cx| { this.text(cx) }),
1712 r#"
1713 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1714 rational expr$12number3number) is a sequence of characters that specifies a search
1715 pattern in text. Usually such patterns are used by string-searching algorithms
1716 for "find" or "find and replace" operations on strings, or for input validation.
1717 "#
1718 .unindent()
1719 );
1720 // Now with a whole-word twist.
1721 search_bar
1722 .update(cx, |search_bar, cx| {
1723 search_bar.activate_search_mode(SearchMode::Regex, cx);
1724 search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1725 })
1726 .await
1727 .unwrap();
1728 search_bar.update(cx, |search_bar, cx| {
1729 search_bar.replacement_editor.update(cx, |editor, cx| {
1730 editor.set_text("things", cx);
1731 });
1732 search_bar.replace_all(&ReplaceAll, cx)
1733 });
1734 // The only word affected by this edit should be `algorithms`, even though there's a bunch
1735 // of words in this text that would match this regex if not for WHOLE_WORD.
1736 assert_eq!(
1737 editor.update(cx, |this, cx| { this.text(cx) }),
1738 r#"
1739 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1740 rational expr$12number3number) is a sequence of characters that specifies a search
1741 pattern in text. Usually such patterns are used by string-searching things
1742 for "find" or "find and replace" operations on strings, or for input validation.
1743 "#
1744 .unindent()
1745 );
1746 }
1747}