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