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