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