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