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