1mod registrar;
2
3use crate::{
4 FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
5 SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex,
6 ToggleReplace, ToggleSelection, ToggleWholeWord,
7 search_bar::{
8 input_base_styles, render_action_button, render_text_input, toggle_replace_button,
9 },
10};
11use any_vec::AnyVec;
12use anyhow::Context as _;
13use collections::HashMap;
14use editor::{
15 DisplayPoint, Editor, EditorSettings,
16 actions::{Backtab, Tab},
17};
18use futures::channel::oneshot;
19use gpui::{
20 Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
21 IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
22 Window, actions, div,
23};
24use language::{Language, LanguageRegistry};
25use project::{
26 search::SearchQuery,
27 search_history::{SearchHistory, SearchHistoryCursor},
28};
29use schemars::JsonSchema;
30use serde::Deserialize;
31use settings::Settings;
32use std::sync::Arc;
33use zed_actions::outline::ToggleOutline;
34
35use ui::{
36 BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*,
37 utils::SearchInputWidth,
38};
39use util::ResultExt;
40use workspace::{
41 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
42 item::ItemHandle,
43 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
44};
45
46pub use registrar::DivRegistrar;
47use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
48
49const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
50
51/// Opens the buffer search interface with the specified configuration.
52#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
53#[action(namespace = buffer_search)]
54#[serde(deny_unknown_fields)]
55pub struct Deploy {
56 #[serde(default = "util::serde::default_true")]
57 pub focus: bool,
58 #[serde(default)]
59 pub replace_enabled: bool,
60 #[serde(default)]
61 pub selection_search_enabled: bool,
62}
63
64actions!(
65 buffer_search,
66 [
67 /// Deploys the search and replace interface.
68 DeployReplace,
69 /// Dismisses the search bar.
70 Dismiss,
71 /// Focuses back on the editor.
72 FocusEditor
73 ]
74);
75
76impl Deploy {
77 pub fn find() -> Self {
78 Self {
79 focus: true,
80 replace_enabled: false,
81 selection_search_enabled: false,
82 }
83 }
84
85 pub fn replace() -> Self {
86 Self {
87 focus: true,
88 replace_enabled: true,
89 selection_search_enabled: false,
90 }
91 }
92}
93
94pub enum Event {
95 UpdateLocation,
96}
97
98pub fn init(cx: &mut App) {
99 cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
100 .detach();
101}
102
103pub struct BufferSearchBar {
104 query_editor: Entity<Editor>,
105 query_editor_focused: bool,
106 replacement_editor: Entity<Editor>,
107 replacement_editor_focused: bool,
108 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
109 active_match_index: Option<usize>,
110 active_searchable_item_subscription: Option<Subscription>,
111 active_search: Option<Arc<SearchQuery>>,
112 searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
113 pending_search: Option<Task<()>>,
114 search_options: SearchOptions,
115 default_options: SearchOptions,
116 configured_options: SearchOptions,
117 query_error: Option<String>,
118 dismissed: bool,
119 search_history: SearchHistory,
120 search_history_cursor: SearchHistoryCursor,
121 replace_enabled: bool,
122 selection_search_enabled: bool,
123 scroll_handle: ScrollHandle,
124 editor_scroll_handle: ScrollHandle,
125 editor_needed_width: Pixels,
126 regex_language: Option<Arc<Language>>,
127}
128
129impl BufferSearchBar {
130 pub fn query_editor_focused(&self) -> bool {
131 self.query_editor_focused
132 }
133}
134
135impl EventEmitter<Event> for BufferSearchBar {}
136impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
137impl Render for BufferSearchBar {
138 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
139 if self.dismissed {
140 return div().id("search_bar");
141 }
142
143 let focus_handle = self.focus_handle(cx);
144
145 let narrow_mode =
146 self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
147 let hide_inline_icons = self.editor_needed_width
148 > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
149
150 let workspace::searchable::SearchOptions {
151 case,
152 word,
153 regex,
154 replacement,
155 selection,
156 find_in_results,
157 } = self.supported_options(cx);
158
159 if self.query_editor.update(cx, |query_editor, _cx| {
160 query_editor.placeholder_text().is_none()
161 }) {
162 self.query_editor.update(cx, |editor, cx| {
163 editor.set_placeholder_text("Search…", cx);
164 });
165 }
166
167 self.replacement_editor.update(cx, |editor, cx| {
168 editor.set_placeholder_text("Replace with…", cx);
169 });
170
171 let mut color_override = None;
172 let match_text = self
173 .active_searchable_item
174 .as_ref()
175 .and_then(|searchable_item| {
176 if self.query(cx).is_empty() {
177 return None;
178 }
179 let matches_count = self
180 .searchable_items_with_matches
181 .get(&searchable_item.downgrade())
182 .map(AnyVec::len)
183 .unwrap_or(0);
184 if let Some(match_ix) = self.active_match_index {
185 Some(format!("{}/{}", match_ix + 1, matches_count))
186 } else {
187 color_override = Some(Color::Error); // No matches found
188 None
189 }
190 })
191 .unwrap_or_else(|| "0/0".to_string());
192 let should_show_replace_input = self.replace_enabled && replacement;
193 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
194
195 let theme_colors = cx.theme().colors();
196 let query_border = if self.query_error.is_some() {
197 Color::Error.color(cx)
198 } else {
199 theme_colors.border
200 };
201 let replacement_border = theme_colors.border;
202
203 let container_width = window.viewport_size().width;
204 let input_width = SearchInputWidth::calc_width(container_width);
205
206 let input_base_styles =
207 |border_color| input_base_styles(border_color, |div| div.w(input_width));
208
209 let query_column = input_base_styles(query_border)
210 .id("editor-scroll")
211 .track_scroll(&self.editor_scroll_handle)
212 .child(render_text_input(&self.query_editor, color_override, cx))
213 .when(!hide_inline_icons, |div| {
214 div.child(
215 h_flex()
216 .gap_1()
217 .when(case, |div| {
218 div.child(SearchOptions::CASE_SENSITIVE.as_button(
219 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
220 focus_handle.clone(),
221 cx.listener(|this, _, window, cx| {
222 this.toggle_case_sensitive(&ToggleCaseSensitive, window, cx)
223 }),
224 ))
225 })
226 .when(word, |div| {
227 div.child(SearchOptions::WHOLE_WORD.as_button(
228 self.search_options.contains(SearchOptions::WHOLE_WORD),
229 focus_handle.clone(),
230 cx.listener(|this, _, window, cx| {
231 this.toggle_whole_word(&ToggleWholeWord, window, cx)
232 }),
233 ))
234 })
235 .when(regex, |div| {
236 div.child(SearchOptions::REGEX.as_button(
237 self.search_options.contains(SearchOptions::REGEX),
238 focus_handle.clone(),
239 cx.listener(|this, _, window, cx| {
240 this.toggle_regex(&ToggleRegex, window, cx)
241 }),
242 ))
243 }),
244 )
245 });
246
247 let mode_column = h_flex()
248 .gap_1()
249 .min_w_64()
250 .when(replacement, |this| {
251 this.child(toggle_replace_button(
252 "buffer-search-bar-toggle-replace-button",
253 focus_handle.clone(),
254 self.replace_enabled,
255 cx.listener(|this, _: &ClickEvent, window, cx| {
256 this.toggle_replace(&ToggleReplace, window, cx);
257 }),
258 ))
259 })
260 .when(selection, |this| {
261 this.child(
262 IconButton::new(
263 "buffer-search-bar-toggle-search-selection-button",
264 IconName::Quote,
265 )
266 .style(ButtonStyle::Subtle)
267 .shape(IconButtonShape::Square)
268 .when(self.selection_search_enabled, |button| {
269 button.style(ButtonStyle::Filled)
270 })
271 .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
272 this.toggle_selection(&ToggleSelection, window, cx);
273 }))
274 .toggle_state(self.selection_search_enabled)
275 .tooltip({
276 let focus_handle = focus_handle.clone();
277 move |window, cx| {
278 Tooltip::for_action_in(
279 "Toggle Search Selection",
280 &ToggleSelection,
281 &focus_handle,
282 window,
283 cx,
284 )
285 }
286 }),
287 )
288 })
289 .when(!find_in_results, |el| {
290 let query_focus = self.query_editor.focus_handle(cx);
291 let matches_column = h_flex()
292 .pl_2()
293 .ml_2()
294 .border_l_1()
295 .border_color(theme_colors.border_variant)
296 .child(render_action_button(
297 "buffer-search-nav-button",
298 ui::IconName::ChevronLeft,
299 self.active_match_index.is_some(),
300 "Select Previous Match",
301 &SelectPreviousMatch,
302 query_focus.clone(),
303 ))
304 .child(render_action_button(
305 "buffer-search-nav-button",
306 ui::IconName::ChevronRight,
307 self.active_match_index.is_some(),
308 "Select Next Match",
309 &SelectNextMatch,
310 query_focus.clone(),
311 ))
312 .when(!narrow_mode, |this| {
313 this.child(div().ml_2().min_w(rems_from_px(40.)).child(
314 Label::new(match_text).size(LabelSize::Small).color(
315 if self.active_match_index.is_some() {
316 Color::Default
317 } else {
318 Color::Disabled
319 },
320 ),
321 ))
322 });
323
324 el.child(render_action_button(
325 "buffer-search-nav-button",
326 IconName::SelectAll,
327 true,
328 "Select All Matches",
329 &SelectAllMatches,
330 query_focus,
331 ))
332 .child(matches_column)
333 })
334 .when(find_in_results, |el| {
335 el.child(render_action_button(
336 "buffer-search",
337 IconName::Close,
338 true,
339 "Close Search Bar",
340 &Dismiss,
341 focus_handle.clone(),
342 ))
343 });
344
345 let search_line = h_flex()
346 .w_full()
347 .gap_2()
348 .when(find_in_results, |el| {
349 el.child(Label::new("Find in results").color(Color::Hint))
350 })
351 .child(query_column)
352 .child(mode_column);
353
354 let replace_line =
355 should_show_replace_input.then(|| {
356 let replace_column = input_base_styles(replacement_border)
357 .child(render_text_input(&self.replacement_editor, None, cx));
358 let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
359
360 let replace_actions = h_flex()
361 .min_w_64()
362 .gap_1()
363 .child(render_action_button(
364 "buffer-search-replace-button",
365 IconName::ReplaceNext,
366 true,
367 "Replace Next Match",
368 &ReplaceNext,
369 focus_handle.clone(),
370 ))
371 .child(render_action_button(
372 "buffer-search-replace-button",
373 IconName::ReplaceAll,
374 true,
375 "Replace All Matches",
376 &ReplaceAll,
377 focus_handle,
378 ));
379 h_flex()
380 .w_full()
381 .gap_2()
382 .child(replace_column)
383 .child(replace_actions)
384 });
385
386 let mut key_context = KeyContext::new_with_defaults();
387 key_context.add("BufferSearchBar");
388 if in_replace {
389 key_context.add("in_replace");
390 }
391
392 let query_error_line = self.query_error.as_ref().map(|error| {
393 Label::new(error)
394 .size(LabelSize::Small)
395 .color(Color::Error)
396 .mt_neg_1()
397 .ml_2()
398 });
399
400 let search_line =
401 h_flex()
402 .relative()
403 .child(search_line)
404 .when(!narrow_mode && !find_in_results, |div| {
405 div.child(h_flex().absolute().right_0().child(render_action_button(
406 "buffer-search",
407 IconName::Close,
408 true,
409 "Close Search Bar",
410 &Dismiss,
411 focus_handle.clone(),
412 )))
413 .w_full()
414 });
415 v_flex()
416 .id("buffer_search")
417 .gap_2()
418 .py(px(1.0))
419 .w_full()
420 .track_scroll(&self.scroll_handle)
421 .key_context(key_context)
422 .capture_action(cx.listener(Self::tab))
423 .capture_action(cx.listener(Self::backtab))
424 .on_action(cx.listener(Self::previous_history_query))
425 .on_action(cx.listener(Self::next_history_query))
426 .on_action(cx.listener(Self::dismiss))
427 .on_action(cx.listener(Self::select_next_match))
428 .on_action(cx.listener(Self::select_prev_match))
429 .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
430 if let Some(active_searchable_item) = &mut this.active_searchable_item {
431 active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
432 }
433 }))
434 .when(replacement, |this| {
435 this.on_action(cx.listener(Self::toggle_replace))
436 .when(in_replace, |this| {
437 this.on_action(cx.listener(Self::replace_next))
438 .on_action(cx.listener(Self::replace_all))
439 })
440 })
441 .when(case, |this| {
442 this.on_action(cx.listener(Self::toggle_case_sensitive))
443 })
444 .when(word, |this| {
445 this.on_action(cx.listener(Self::toggle_whole_word))
446 })
447 .when(regex, |this| {
448 this.on_action(cx.listener(Self::toggle_regex))
449 })
450 .when(selection, |this| {
451 this.on_action(cx.listener(Self::toggle_selection))
452 })
453 .child(search_line)
454 .children(query_error_line)
455 .children(replace_line)
456 }
457}
458
459impl Focusable for BufferSearchBar {
460 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
461 self.query_editor.focus_handle(cx)
462 }
463}
464
465impl ToolbarItemView for BufferSearchBar {
466 fn set_active_pane_item(
467 &mut self,
468 item: Option<&dyn ItemHandle>,
469 window: &mut Window,
470 cx: &mut Context<Self>,
471 ) -> ToolbarItemLocation {
472 cx.notify();
473 self.active_searchable_item_subscription.take();
474 self.active_searchable_item.take();
475
476 self.pending_search.take();
477
478 if let Some(searchable_item_handle) =
479 item.and_then(|item| item.to_searchable_item_handle(cx))
480 {
481 let this = cx.entity().downgrade();
482
483 self.active_searchable_item_subscription =
484 Some(searchable_item_handle.subscribe_to_search_events(
485 window,
486 cx,
487 Box::new(move |search_event, window, cx| {
488 if let Some(this) = this.upgrade() {
489 this.update(cx, |this, cx| {
490 this.on_active_searchable_item_event(search_event, window, cx)
491 });
492 }
493 }),
494 ));
495
496 let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
497 self.active_searchable_item = Some(searchable_item_handle);
498 drop(self.update_matches(true, window, cx));
499 if !self.dismissed {
500 if is_project_search {
501 self.dismiss(&Default::default(), window, cx);
502 } else {
503 return ToolbarItemLocation::Secondary;
504 }
505 }
506 }
507 ToolbarItemLocation::Hidden
508 }
509}
510
511impl BufferSearchBar {
512 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
513 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
514 this.query_editor.focus_handle(cx).focus(window);
515 this.select_query(window, cx);
516 }));
517 registrar.register_handler(ForDeployed(
518 |this, action: &ToggleCaseSensitive, window, cx| {
519 if this.supported_options(cx).case {
520 this.toggle_case_sensitive(action, window, cx);
521 }
522 },
523 ));
524 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
525 if this.supported_options(cx).word {
526 this.toggle_whole_word(action, window, cx);
527 }
528 }));
529 registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
530 if this.supported_options(cx).regex {
531 this.toggle_regex(action, window, cx);
532 }
533 }));
534 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
535 if this.supported_options(cx).selection {
536 this.toggle_selection(action, window, cx);
537 } else {
538 cx.propagate();
539 }
540 }));
541 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
542 if this.supported_options(cx).replacement {
543 this.toggle_replace(action, window, cx);
544 } else {
545 cx.propagate();
546 }
547 }));
548 registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| {
549 if this.supported_options(cx).find_in_results {
550 cx.propagate();
551 } else {
552 this.select_next_match(action, window, cx);
553 }
554 }));
555 registrar.register_handler(WithResults(
556 |this, action: &SelectPreviousMatch, window, cx| {
557 if this.supported_options(cx).find_in_results {
558 cx.propagate();
559 } else {
560 this.select_prev_match(action, window, cx);
561 }
562 },
563 ));
564 registrar.register_handler(WithResults(
565 |this, action: &SelectAllMatches, window, cx| {
566 if this.supported_options(cx).find_in_results {
567 cx.propagate();
568 } else {
569 this.select_all_matches(action, window, cx);
570 }
571 },
572 ));
573 registrar.register_handler(ForDeployed(
574 |this, _: &editor::actions::Cancel, window, cx| {
575 this.dismiss(&Dismiss, window, cx);
576 },
577 ));
578 registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
579 this.dismiss(&Dismiss, window, cx);
580 }));
581
582 // register deploy buffer search for both search bar states, since we want to focus into the search bar
583 // when the deploy action is triggered in the buffer.
584 registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
585 this.deploy(deploy, window, cx);
586 }));
587 registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
588 this.deploy(deploy, window, cx);
589 }));
590 registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
591 if this.supported_options(cx).find_in_results {
592 cx.propagate();
593 } else {
594 this.deploy(&Deploy::replace(), window, cx);
595 }
596 }));
597 registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
598 if this.supported_options(cx).find_in_results {
599 cx.propagate();
600 } else {
601 this.deploy(&Deploy::replace(), window, cx);
602 }
603 }));
604 }
605
606 pub fn new(
607 languages: Option<Arc<LanguageRegistry>>,
608 window: &mut Window,
609 cx: &mut Context<Self>,
610 ) -> Self {
611 let query_editor = cx.new(|cx| {
612 let mut editor = Editor::single_line(window, cx);
613 editor.set_use_autoclose(false);
614 editor
615 });
616 cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
617 .detach();
618 let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
619 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
620 .detach();
621
622 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
623 if let Some(languages) = languages {
624 let query_buffer = query_editor
625 .read(cx)
626 .buffer()
627 .read(cx)
628 .as_singleton()
629 .expect("query editor should be backed by a singleton buffer");
630 query_buffer
631 .read(cx)
632 .set_language_registry(languages.clone());
633
634 cx.spawn(async move |buffer_search_bar, cx| {
635 let regex_language = languages
636 .language_for_name("regex")
637 .await
638 .context("loading regex language")?;
639 buffer_search_bar
640 .update(cx, |buffer_search_bar, cx| {
641 buffer_search_bar.regex_language = Some(regex_language);
642 buffer_search_bar.adjust_query_regex_language(cx);
643 })
644 .ok();
645 anyhow::Ok(())
646 })
647 .detach_and_log_err(cx);
648 }
649
650 Self {
651 query_editor,
652 query_editor_focused: false,
653 replacement_editor,
654 replacement_editor_focused: false,
655 active_searchable_item: None,
656 active_searchable_item_subscription: None,
657 active_match_index: None,
658 searchable_items_with_matches: Default::default(),
659 default_options: search_options,
660 configured_options: search_options,
661 search_options,
662 pending_search: None,
663 query_error: None,
664 dismissed: true,
665 search_history: SearchHistory::new(
666 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
667 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
668 ),
669 search_history_cursor: Default::default(),
670 active_search: None,
671 replace_enabled: false,
672 selection_search_enabled: false,
673 scroll_handle: ScrollHandle::new(),
674 editor_scroll_handle: ScrollHandle::new(),
675 editor_needed_width: px(0.),
676 regex_language: None,
677 }
678 }
679
680 pub fn is_dismissed(&self) -> bool {
681 self.dismissed
682 }
683
684 pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
685 self.dismissed = true;
686 self.query_error = None;
687 for searchable_item in self.searchable_items_with_matches.keys() {
688 if let Some(searchable_item) =
689 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
690 {
691 searchable_item.clear_matches(window, cx);
692 }
693 }
694 if let Some(active_editor) = self.active_searchable_item.as_mut() {
695 self.selection_search_enabled = false;
696 self.replace_enabled = false;
697 active_editor.search_bar_visibility_changed(false, window, cx);
698 active_editor.toggle_filtered_search_ranges(false, window, cx);
699 let handle = active_editor.item_focus_handle(cx);
700 self.focus(&handle, window);
701 }
702 cx.emit(Event::UpdateLocation);
703 cx.emit(ToolbarItemEvent::ChangeLocation(
704 ToolbarItemLocation::Hidden,
705 ));
706 cx.notify();
707 }
708
709 pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
710 if self.show(window, cx) {
711 if let Some(active_item) = self.active_searchable_item.as_mut() {
712 active_item.toggle_filtered_search_ranges(
713 deploy.selection_search_enabled,
714 window,
715 cx,
716 );
717 }
718 self.search_suggested(window, cx);
719 self.smartcase(window, cx);
720 self.replace_enabled = deploy.replace_enabled;
721 self.selection_search_enabled = deploy.selection_search_enabled;
722 if deploy.focus {
723 let mut handle = self.query_editor.focus_handle(cx).clone();
724 let mut select_query = true;
725 if deploy.replace_enabled && handle.is_focused(window) {
726 handle = self.replacement_editor.focus_handle(cx).clone();
727 select_query = false;
728 };
729
730 if select_query {
731 self.select_query(window, cx);
732 }
733
734 window.focus(&handle);
735 }
736 return true;
737 }
738
739 cx.propagate();
740 false
741 }
742
743 pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
744 if self.is_dismissed() {
745 self.deploy(action, window, cx);
746 } else {
747 self.dismiss(&Dismiss, window, cx);
748 }
749 }
750
751 pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
752 let Some(handle) = self.active_searchable_item.as_ref() else {
753 return false;
754 };
755
756 self.configured_options =
757 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
758 if self.dismissed
759 && (self.configured_options != self.default_options
760 || self.configured_options != self.search_options)
761 {
762 self.search_options = self.configured_options;
763 self.default_options = self.configured_options;
764 }
765
766 self.dismissed = false;
767 self.adjust_query_regex_language(cx);
768 handle.search_bar_visibility_changed(true, window, cx);
769 cx.notify();
770 cx.emit(Event::UpdateLocation);
771 cx.emit(ToolbarItemEvent::ChangeLocation(
772 ToolbarItemLocation::Secondary,
773 ));
774 true
775 }
776
777 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
778 self.active_searchable_item
779 .as_ref()
780 .map(|item| item.supported_options(cx))
781 .unwrap_or_default()
782 }
783
784 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
785 let search = self
786 .query_suggestion(window, cx)
787 .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
788
789 if let Some(search) = search {
790 cx.spawn_in(window, async move |this, cx| {
791 search.await?;
792 this.update_in(cx, |this, window, cx| {
793 this.activate_current_match(window, cx)
794 })
795 })
796 .detach_and_log_err(cx);
797 }
798 }
799
800 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
801 if let Some(match_ix) = self.active_match_index {
802 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
803 if let Some(matches) = self
804 .searchable_items_with_matches
805 .get(&active_searchable_item.downgrade())
806 {
807 active_searchable_item.activate_match(match_ix, matches, window, cx)
808 }
809 }
810 }
811 }
812
813 pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
814 self.query_editor.update(cx, |query_editor, cx| {
815 query_editor.select_all(&Default::default(), window, cx);
816 });
817 }
818
819 pub fn query(&self, cx: &App) -> String {
820 self.query_editor.read(cx).text(cx)
821 }
822
823 pub fn replacement(&self, cx: &mut App) -> String {
824 self.replacement_editor.read(cx).text(cx)
825 }
826
827 pub fn query_suggestion(
828 &mut self,
829 window: &mut Window,
830 cx: &mut Context<Self>,
831 ) -> Option<String> {
832 self.active_searchable_item
833 .as_ref()
834 .map(|searchable_item| searchable_item.query_suggestion(window, cx))
835 .filter(|suggestion| !suggestion.is_empty())
836 }
837
838 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
839 if replacement.is_none() {
840 self.replace_enabled = false;
841 return;
842 }
843 self.replace_enabled = true;
844 self.replacement_editor
845 .update(cx, |replacement_editor, cx| {
846 replacement_editor
847 .buffer()
848 .update(cx, |replacement_buffer, cx| {
849 let len = replacement_buffer.len(cx);
850 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
851 });
852 });
853 }
854
855 pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
856 self.focus(&self.replacement_editor.focus_handle(cx), window);
857 cx.notify();
858 }
859
860 pub fn search(
861 &mut self,
862 query: &str,
863 options: Option<SearchOptions>,
864 window: &mut Window,
865 cx: &mut Context<Self>,
866 ) -> oneshot::Receiver<()> {
867 let options = options.unwrap_or(self.default_options);
868 let updated = query != self.query(cx) || self.search_options != options;
869 if updated {
870 self.query_editor.update(cx, |query_editor, cx| {
871 query_editor.buffer().update(cx, |query_buffer, cx| {
872 let len = query_buffer.len(cx);
873 query_buffer.edit([(0..len, query)], None, cx);
874 });
875 });
876 self.set_search_options(options, cx);
877 self.clear_matches(window, cx);
878 cx.notify();
879 }
880 self.update_matches(!updated, window, cx)
881 }
882
883 pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
884 if let Some(active_editor) = self.active_searchable_item.as_ref() {
885 let handle = active_editor.item_focus_handle(cx);
886 window.focus(&handle);
887 }
888 }
889
890 pub fn toggle_search_option(
891 &mut self,
892 search_option: SearchOptions,
893 window: &mut Window,
894 cx: &mut Context<Self>,
895 ) {
896 self.search_options.toggle(search_option);
897 self.default_options = self.search_options;
898 drop(self.update_matches(false, window, cx));
899 self.adjust_query_regex_language(cx);
900 cx.notify();
901 }
902
903 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
904 self.search_options.contains(search_option)
905 }
906
907 pub fn enable_search_option(
908 &mut self,
909 search_option: SearchOptions,
910 window: &mut Window,
911 cx: &mut Context<Self>,
912 ) {
913 if !self.search_options.contains(search_option) {
914 self.toggle_search_option(search_option, window, cx)
915 }
916 }
917
918 pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
919 self.search_options = search_options;
920 self.adjust_query_regex_language(cx);
921 cx.notify();
922 }
923
924 pub fn clear_search_within_ranges(
925 &mut self,
926 search_options: SearchOptions,
927 cx: &mut Context<Self>,
928 ) {
929 self.search_options = search_options;
930 self.adjust_query_regex_language(cx);
931 cx.notify();
932 }
933
934 fn select_next_match(
935 &mut self,
936 _: &SelectNextMatch,
937 window: &mut Window,
938 cx: &mut Context<Self>,
939 ) {
940 self.select_match(Direction::Next, 1, window, cx);
941 }
942
943 fn select_prev_match(
944 &mut self,
945 _: &SelectPreviousMatch,
946 window: &mut Window,
947 cx: &mut Context<Self>,
948 ) {
949 self.select_match(Direction::Prev, 1, window, cx);
950 }
951
952 fn select_all_matches(
953 &mut self,
954 _: &SelectAllMatches,
955 window: &mut Window,
956 cx: &mut Context<Self>,
957 ) {
958 if !self.dismissed && self.active_match_index.is_some() {
959 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
960 if let Some(matches) = self
961 .searchable_items_with_matches
962 .get(&searchable_item.downgrade())
963 {
964 searchable_item.select_matches(matches, window, cx);
965 self.focus_editor(&FocusEditor, window, cx);
966 }
967 }
968 }
969 }
970
971 pub fn select_match(
972 &mut self,
973 direction: Direction,
974 count: usize,
975 window: &mut Window,
976 cx: &mut Context<Self>,
977 ) {
978 if let Some(index) = self.active_match_index {
979 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
980 if let Some(matches) = self
981 .searchable_items_with_matches
982 .get(&searchable_item.downgrade())
983 .filter(|matches| !matches.is_empty())
984 {
985 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
986 if !EditorSettings::get_global(cx).search_wrap
987 && ((direction == Direction::Next && index + count >= matches.len())
988 || (direction == Direction::Prev && index < count))
989 {
990 crate::show_no_more_matches(window, cx);
991 return;
992 }
993 let new_match_index = searchable_item
994 .match_index_for_direction(matches, index, direction, count, window, cx);
995
996 searchable_item.update_matches(matches, window, cx);
997 searchable_item.activate_match(new_match_index, matches, window, cx);
998 }
999 }
1000 }
1001 }
1002
1003 pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1004 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1005 if let Some(matches) = self
1006 .searchable_items_with_matches
1007 .get(&searchable_item.downgrade())
1008 {
1009 if matches.is_empty() {
1010 return;
1011 }
1012 searchable_item.update_matches(matches, window, cx);
1013 searchable_item.activate_match(0, matches, window, cx);
1014 }
1015 }
1016 }
1017
1018 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1019 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1020 if let Some(matches) = self
1021 .searchable_items_with_matches
1022 .get(&searchable_item.downgrade())
1023 {
1024 if matches.is_empty() {
1025 return;
1026 }
1027 let new_match_index = matches.len() - 1;
1028 searchable_item.update_matches(matches, window, cx);
1029 searchable_item.activate_match(new_match_index, matches, window, cx);
1030 }
1031 }
1032 }
1033
1034 fn on_query_editor_event(
1035 &mut self,
1036 editor: &Entity<Editor>,
1037 event: &editor::EditorEvent,
1038 window: &mut Window,
1039 cx: &mut Context<Self>,
1040 ) {
1041 match event {
1042 editor::EditorEvent::Focused => self.query_editor_focused = true,
1043 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1044 editor::EditorEvent::Edited { .. } => {
1045 self.smartcase(window, cx);
1046 self.clear_matches(window, cx);
1047 let search = self.update_matches(false, window, cx);
1048
1049 let width = editor.update(cx, |editor, cx| {
1050 let text_layout_details = editor.text_layout_details(window);
1051 let snapshot = editor.snapshot(window, cx).display_snapshot;
1052
1053 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1054 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1055 });
1056 self.editor_needed_width = width;
1057 cx.notify();
1058
1059 cx.spawn_in(window, async move |this, cx| {
1060 search.await?;
1061 this.update_in(cx, |this, window, cx| {
1062 this.activate_current_match(window, cx)
1063 })
1064 })
1065 .detach_and_log_err(cx);
1066 }
1067 _ => {}
1068 }
1069 }
1070
1071 fn on_replacement_editor_event(
1072 &mut self,
1073 _: Entity<Editor>,
1074 event: &editor::EditorEvent,
1075 _: &mut Context<Self>,
1076 ) {
1077 match event {
1078 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1079 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1080 _ => {}
1081 }
1082 }
1083
1084 fn on_active_searchable_item_event(
1085 &mut self,
1086 event: &SearchEvent,
1087 window: &mut Window,
1088 cx: &mut Context<Self>,
1089 ) {
1090 match event {
1091 SearchEvent::MatchesInvalidated => {
1092 drop(self.update_matches(false, window, cx));
1093 }
1094 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1095 }
1096 }
1097
1098 fn toggle_case_sensitive(
1099 &mut self,
1100 _: &ToggleCaseSensitive,
1101 window: &mut Window,
1102 cx: &mut Context<Self>,
1103 ) {
1104 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1105 }
1106
1107 fn toggle_whole_word(
1108 &mut self,
1109 _: &ToggleWholeWord,
1110 window: &mut Window,
1111 cx: &mut Context<Self>,
1112 ) {
1113 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1114 }
1115
1116 fn toggle_selection(
1117 &mut self,
1118 _: &ToggleSelection,
1119 window: &mut Window,
1120 cx: &mut Context<Self>,
1121 ) {
1122 if let Some(active_item) = self.active_searchable_item.as_mut() {
1123 self.selection_search_enabled = !self.selection_search_enabled;
1124 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1125 drop(self.update_matches(false, window, cx));
1126 cx.notify();
1127 }
1128 }
1129
1130 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1131 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1132 }
1133
1134 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1135 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1136 self.active_match_index = None;
1137 self.searchable_items_with_matches
1138 .remove(&active_searchable_item.downgrade());
1139 active_searchable_item.clear_matches(window, cx);
1140 }
1141 }
1142
1143 pub fn has_active_match(&self) -> bool {
1144 self.active_match_index.is_some()
1145 }
1146
1147 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1148 let mut active_item_matches = None;
1149 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1150 if let Some(searchable_item) =
1151 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1152 {
1153 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1154 active_item_matches = Some((searchable_item.downgrade(), matches));
1155 } else {
1156 searchable_item.clear_matches(window, cx);
1157 }
1158 }
1159 }
1160
1161 self.searchable_items_with_matches
1162 .extend(active_item_matches);
1163 }
1164
1165 fn update_matches(
1166 &mut self,
1167 reuse_existing_query: bool,
1168 window: &mut Window,
1169 cx: &mut Context<Self>,
1170 ) -> oneshot::Receiver<()> {
1171 let (done_tx, done_rx) = oneshot::channel();
1172 let query = self.query(cx);
1173 self.pending_search.take();
1174
1175 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1176 self.query_error = None;
1177 if query.is_empty() {
1178 self.clear_active_searchable_item_matches(window, cx);
1179 let _ = done_tx.send(());
1180 cx.notify();
1181 } else {
1182 let query: Arc<_> = if let Some(search) =
1183 self.active_search.take().filter(|_| reuse_existing_query)
1184 {
1185 search
1186 } else {
1187 if self.search_options.contains(SearchOptions::REGEX) {
1188 match SearchQuery::regex(
1189 query,
1190 self.search_options.contains(SearchOptions::WHOLE_WORD),
1191 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1192 false,
1193 self.search_options
1194 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1195 Default::default(),
1196 Default::default(),
1197 false,
1198 None,
1199 ) {
1200 Ok(query) => query.with_replacement(self.replacement(cx)),
1201 Err(e) => {
1202 self.query_error = Some(e.to_string());
1203 self.clear_active_searchable_item_matches(window, cx);
1204 cx.notify();
1205 return done_rx;
1206 }
1207 }
1208 } else {
1209 match SearchQuery::text(
1210 query,
1211 self.search_options.contains(SearchOptions::WHOLE_WORD),
1212 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1213 false,
1214 Default::default(),
1215 Default::default(),
1216 false,
1217 None,
1218 ) {
1219 Ok(query) => query.with_replacement(self.replacement(cx)),
1220 Err(e) => {
1221 self.query_error = Some(e.to_string());
1222 self.clear_active_searchable_item_matches(window, cx);
1223 cx.notify();
1224 return done_rx;
1225 }
1226 }
1227 }
1228 .into()
1229 };
1230
1231 self.active_search = Some(query.clone());
1232 let query_text = query.as_str().to_string();
1233
1234 let matches = active_searchable_item.find_matches(query, window, cx);
1235
1236 let active_searchable_item = active_searchable_item.downgrade();
1237 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1238 let matches = matches.await;
1239
1240 this.update_in(cx, |this, window, cx| {
1241 if let Some(active_searchable_item) =
1242 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1243 {
1244 this.searchable_items_with_matches
1245 .insert(active_searchable_item.downgrade(), matches);
1246
1247 this.update_match_index(window, cx);
1248 this.search_history
1249 .add(&mut this.search_history_cursor, query_text);
1250 if !this.dismissed {
1251 let matches = this
1252 .searchable_items_with_matches
1253 .get(&active_searchable_item.downgrade())
1254 .unwrap();
1255 if matches.is_empty() {
1256 active_searchable_item.clear_matches(window, cx);
1257 } else {
1258 active_searchable_item.update_matches(matches, window, cx);
1259 }
1260 let _ = done_tx.send(());
1261 }
1262 cx.notify();
1263 }
1264 })
1265 .log_err();
1266 }));
1267 }
1268 }
1269 done_rx
1270 }
1271
1272 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1273 if self.search_options.contains(SearchOptions::BACKWARDS) {
1274 direction.opposite()
1275 } else {
1276 direction
1277 }
1278 }
1279
1280 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1281 let direction = self.reverse_direction_if_backwards(Direction::Next);
1282 let new_index = self
1283 .active_searchable_item
1284 .as_ref()
1285 .and_then(|searchable_item| {
1286 let matches = self
1287 .searchable_items_with_matches
1288 .get(&searchable_item.downgrade())?;
1289 searchable_item.active_match_index(direction, matches, window, cx)
1290 });
1291 if new_index != self.active_match_index {
1292 self.active_match_index = new_index;
1293 cx.notify();
1294 }
1295 }
1296
1297 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1298 self.cycle_field(Direction::Next, window, cx);
1299 }
1300
1301 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1302 self.cycle_field(Direction::Prev, window, cx);
1303 }
1304 fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1305 let mut handles = vec![self.query_editor.focus_handle(cx)];
1306 if self.replace_enabled {
1307 handles.push(self.replacement_editor.focus_handle(cx));
1308 }
1309 if let Some(item) = self.active_searchable_item.as_ref() {
1310 handles.push(item.item_focus_handle(cx));
1311 }
1312 let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1313 Some(index) => index,
1314 None => return,
1315 };
1316
1317 let new_index = match direction {
1318 Direction::Next => (current_index + 1) % handles.len(),
1319 Direction::Prev if current_index == 0 => handles.len() - 1,
1320 Direction::Prev => (current_index - 1) % handles.len(),
1321 };
1322 let next_focus_handle = &handles[new_index];
1323 self.focus(next_focus_handle, window);
1324 cx.stop_propagation();
1325 }
1326
1327 fn next_history_query(
1328 &mut self,
1329 _: &NextHistoryQuery,
1330 window: &mut Window,
1331 cx: &mut Context<Self>,
1332 ) {
1333 if let Some(new_query) = self
1334 .search_history
1335 .next(&mut self.search_history_cursor)
1336 .map(str::to_string)
1337 {
1338 drop(self.search(&new_query, Some(self.search_options), window, cx));
1339 } else {
1340 self.search_history_cursor.reset();
1341 drop(self.search("", Some(self.search_options), window, cx));
1342 }
1343 }
1344
1345 fn previous_history_query(
1346 &mut self,
1347 _: &PreviousHistoryQuery,
1348 window: &mut Window,
1349 cx: &mut Context<Self>,
1350 ) {
1351 if self.query(cx).is_empty() {
1352 if let Some(new_query) = self
1353 .search_history
1354 .current(&mut self.search_history_cursor)
1355 .map(str::to_string)
1356 {
1357 drop(self.search(&new_query, Some(self.search_options), window, cx));
1358 return;
1359 }
1360 }
1361
1362 if let Some(new_query) = self
1363 .search_history
1364 .previous(&mut self.search_history_cursor)
1365 .map(str::to_string)
1366 {
1367 drop(self.search(&new_query, Some(self.search_options), window, cx));
1368 }
1369 }
1370
1371 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1372 window.invalidate_character_coordinates();
1373 window.focus(handle);
1374 }
1375
1376 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1377 if self.active_searchable_item.is_some() {
1378 self.replace_enabled = !self.replace_enabled;
1379 let handle = if self.replace_enabled {
1380 self.replacement_editor.focus_handle(cx)
1381 } else {
1382 self.query_editor.focus_handle(cx)
1383 };
1384 self.focus(&handle, window);
1385 cx.notify();
1386 }
1387 }
1388
1389 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1390 let mut should_propagate = true;
1391 if !self.dismissed && self.active_search.is_some() {
1392 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1393 if let Some(query) = self.active_search.as_ref() {
1394 if let Some(matches) = self
1395 .searchable_items_with_matches
1396 .get(&searchable_item.downgrade())
1397 {
1398 if let Some(active_index) = self.active_match_index {
1399 let query = query
1400 .as_ref()
1401 .clone()
1402 .with_replacement(self.replacement(cx));
1403 searchable_item.replace(matches.at(active_index), &query, window, cx);
1404 self.select_next_match(&SelectNextMatch, window, cx);
1405 }
1406 should_propagate = false;
1407 }
1408 }
1409 }
1410 }
1411 if !should_propagate {
1412 cx.stop_propagation();
1413 }
1414 }
1415
1416 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1417 if !self.dismissed && self.active_search.is_some() {
1418 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1419 if let Some(query) = self.active_search.as_ref() {
1420 if let Some(matches) = self
1421 .searchable_items_with_matches
1422 .get(&searchable_item.downgrade())
1423 {
1424 let query = query
1425 .as_ref()
1426 .clone()
1427 .with_replacement(self.replacement(cx));
1428 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1429 }
1430 }
1431 }
1432 }
1433 }
1434
1435 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1436 self.update_match_index(window, cx);
1437 self.active_match_index.is_some()
1438 }
1439
1440 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1441 EditorSettings::get_global(cx).use_smartcase_search
1442 }
1443
1444 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1445 str.chars().any(|c| c.is_uppercase())
1446 }
1447
1448 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1449 if self.should_use_smartcase_search(cx) {
1450 let query = self.query(cx);
1451 if !query.is_empty() {
1452 let is_case = self.is_contains_uppercase(&query);
1453 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1454 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1455 }
1456 }
1457 }
1458 }
1459
1460 fn adjust_query_regex_language(&self, cx: &mut App) {
1461 let enable = self.search_options.contains(SearchOptions::REGEX);
1462 let query_buffer = self
1463 .query_editor
1464 .read(cx)
1465 .buffer()
1466 .read(cx)
1467 .as_singleton()
1468 .expect("query editor should be backed by a singleton buffer");
1469 if enable {
1470 if let Some(regex_language) = self.regex_language.clone() {
1471 query_buffer.update(cx, |query_buffer, cx| {
1472 query_buffer.set_language(Some(regex_language), cx);
1473 })
1474 }
1475 } else {
1476 query_buffer.update(cx, |query_buffer, cx| {
1477 query_buffer.set_language(None, cx);
1478 })
1479 }
1480 }
1481}
1482
1483#[cfg(test)]
1484mod tests {
1485 use std::ops::Range;
1486
1487 use super::*;
1488 use editor::{
1489 DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1490 display_map::DisplayRow,
1491 };
1492 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1493 use language::{Buffer, Point};
1494 use project::Project;
1495 use settings::SettingsStore;
1496 use smol::stream::StreamExt as _;
1497 use unindent::Unindent as _;
1498
1499 fn init_globals(cx: &mut TestAppContext) {
1500 cx.update(|cx| {
1501 let store = settings::SettingsStore::test(cx);
1502 cx.set_global(store);
1503 workspace::init_settings(cx);
1504 editor::init(cx);
1505
1506 language::init(cx);
1507 Project::init_settings(cx);
1508 theme::init(theme::LoadThemes::JustBase, cx);
1509 crate::init(cx);
1510 });
1511 }
1512
1513 fn init_test(
1514 cx: &mut TestAppContext,
1515 ) -> (
1516 Entity<Editor>,
1517 Entity<BufferSearchBar>,
1518 &mut VisualTestContext,
1519 ) {
1520 init_globals(cx);
1521 let buffer = cx.new(|cx| {
1522 Buffer::local(
1523 r#"
1524 A regular expression (shortened as regex or regexp;[1] also referred to as
1525 rational expression[2][3]) is a sequence of characters that specifies a search
1526 pattern in text. Usually such patterns are used by string-searching algorithms
1527 for "find" or "find and replace" operations on strings, or for input validation.
1528 "#
1529 .unindent(),
1530 cx,
1531 )
1532 });
1533 let cx = cx.add_empty_window();
1534 let editor =
1535 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1536
1537 let search_bar = cx.new_window_entity(|window, cx| {
1538 let mut search_bar = BufferSearchBar::new(None, window, cx);
1539 search_bar.set_active_pane_item(Some(&editor), window, cx);
1540 search_bar.show(window, cx);
1541 search_bar
1542 });
1543
1544 (editor, search_bar, cx)
1545 }
1546
1547 #[gpui::test]
1548 async fn test_search_simple(cx: &mut TestAppContext) {
1549 let (editor, search_bar, cx) = init_test(cx);
1550 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1551 background_highlights
1552 .into_iter()
1553 .map(|(range, _)| range)
1554 .collect::<Vec<_>>()
1555 };
1556 // Search for a string that appears with different casing.
1557 // By default, search is case-insensitive.
1558 search_bar
1559 .update_in(cx, |search_bar, window, cx| {
1560 search_bar.search("us", None, window, cx)
1561 })
1562 .await
1563 .unwrap();
1564 editor.update_in(cx, |editor, window, cx| {
1565 assert_eq!(
1566 display_points_of(editor.all_text_background_highlights(window, cx)),
1567 &[
1568 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1569 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1570 ]
1571 );
1572 });
1573
1574 // Switch to a case sensitive search.
1575 search_bar.update_in(cx, |search_bar, window, cx| {
1576 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1577 });
1578 let mut editor_notifications = cx.notifications(&editor);
1579 editor_notifications.next().await;
1580 editor.update_in(cx, |editor, window, cx| {
1581 assert_eq!(
1582 display_points_of(editor.all_text_background_highlights(window, cx)),
1583 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1584 );
1585 });
1586
1587 // Search for a string that appears both as a whole word and
1588 // within other words. By default, all results are found.
1589 search_bar
1590 .update_in(cx, |search_bar, window, cx| {
1591 search_bar.search("or", None, window, cx)
1592 })
1593 .await
1594 .unwrap();
1595 editor.update_in(cx, |editor, window, cx| {
1596 assert_eq!(
1597 display_points_of(editor.all_text_background_highlights(window, cx)),
1598 &[
1599 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1600 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1601 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1602 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1603 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1604 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1605 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1606 ]
1607 );
1608 });
1609
1610 // Switch to a whole word search.
1611 search_bar.update_in(cx, |search_bar, window, cx| {
1612 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1613 });
1614 let mut editor_notifications = cx.notifications(&editor);
1615 editor_notifications.next().await;
1616 editor.update_in(cx, |editor, window, cx| {
1617 assert_eq!(
1618 display_points_of(editor.all_text_background_highlights(window, cx)),
1619 &[
1620 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1621 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1622 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1623 ]
1624 );
1625 });
1626
1627 editor.update_in(cx, |editor, window, cx| {
1628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1629 s.select_display_ranges([
1630 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1631 ])
1632 });
1633 });
1634 search_bar.update_in(cx, |search_bar, window, cx| {
1635 assert_eq!(search_bar.active_match_index, Some(0));
1636 search_bar.select_next_match(&SelectNextMatch, window, cx);
1637 assert_eq!(
1638 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1639 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1640 );
1641 });
1642 search_bar.read_with(cx, |search_bar, _| {
1643 assert_eq!(search_bar.active_match_index, Some(0));
1644 });
1645
1646 search_bar.update_in(cx, |search_bar, window, cx| {
1647 search_bar.select_next_match(&SelectNextMatch, window, cx);
1648 assert_eq!(
1649 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1650 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1651 );
1652 });
1653 search_bar.read_with(cx, |search_bar, _| {
1654 assert_eq!(search_bar.active_match_index, Some(1));
1655 });
1656
1657 search_bar.update_in(cx, |search_bar, window, cx| {
1658 search_bar.select_next_match(&SelectNextMatch, window, cx);
1659 assert_eq!(
1660 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1661 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1662 );
1663 });
1664 search_bar.read_with(cx, |search_bar, _| {
1665 assert_eq!(search_bar.active_match_index, Some(2));
1666 });
1667
1668 search_bar.update_in(cx, |search_bar, window, cx| {
1669 search_bar.select_next_match(&SelectNextMatch, window, cx);
1670 assert_eq!(
1671 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1672 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1673 );
1674 });
1675 search_bar.read_with(cx, |search_bar, _| {
1676 assert_eq!(search_bar.active_match_index, Some(0));
1677 });
1678
1679 search_bar.update_in(cx, |search_bar, window, cx| {
1680 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1681 assert_eq!(
1682 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1683 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1684 );
1685 });
1686 search_bar.read_with(cx, |search_bar, _| {
1687 assert_eq!(search_bar.active_match_index, Some(2));
1688 });
1689
1690 search_bar.update_in(cx, |search_bar, window, cx| {
1691 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1692 assert_eq!(
1693 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1694 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1695 );
1696 });
1697 search_bar.read_with(cx, |search_bar, _| {
1698 assert_eq!(search_bar.active_match_index, Some(1));
1699 });
1700
1701 search_bar.update_in(cx, |search_bar, window, cx| {
1702 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1703 assert_eq!(
1704 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1705 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1706 );
1707 });
1708 search_bar.read_with(cx, |search_bar, _| {
1709 assert_eq!(search_bar.active_match_index, Some(0));
1710 });
1711
1712 // Park the cursor in between matches and ensure that going to the previous match selects
1713 // the closest match to the left.
1714 editor.update_in(cx, |editor, window, cx| {
1715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1716 s.select_display_ranges([
1717 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1718 ])
1719 });
1720 });
1721 search_bar.update_in(cx, |search_bar, window, cx| {
1722 assert_eq!(search_bar.active_match_index, Some(1));
1723 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1724 assert_eq!(
1725 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1726 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1727 );
1728 });
1729 search_bar.read_with(cx, |search_bar, _| {
1730 assert_eq!(search_bar.active_match_index, Some(0));
1731 });
1732
1733 // Park the cursor in between matches and ensure that going to the next match selects the
1734 // closest match to the right.
1735 editor.update_in(cx, |editor, window, cx| {
1736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1737 s.select_display_ranges([
1738 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1739 ])
1740 });
1741 });
1742 search_bar.update_in(cx, |search_bar, window, cx| {
1743 assert_eq!(search_bar.active_match_index, Some(1));
1744 search_bar.select_next_match(&SelectNextMatch, window, cx);
1745 assert_eq!(
1746 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1747 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1748 );
1749 });
1750 search_bar.read_with(cx, |search_bar, _| {
1751 assert_eq!(search_bar.active_match_index, Some(1));
1752 });
1753
1754 // Park the cursor after the last match and ensure that going to the previous match selects
1755 // the last match.
1756 editor.update_in(cx, |editor, window, cx| {
1757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1758 s.select_display_ranges([
1759 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1760 ])
1761 });
1762 });
1763 search_bar.update_in(cx, |search_bar, window, cx| {
1764 assert_eq!(search_bar.active_match_index, Some(2));
1765 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1766 assert_eq!(
1767 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1768 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1769 );
1770 });
1771 search_bar.read_with(cx, |search_bar, _| {
1772 assert_eq!(search_bar.active_match_index, Some(2));
1773 });
1774
1775 // Park the cursor after the last match and ensure that going to the next match selects the
1776 // first match.
1777 editor.update_in(cx, |editor, window, cx| {
1778 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1779 s.select_display_ranges([
1780 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1781 ])
1782 });
1783 });
1784 search_bar.update_in(cx, |search_bar, window, cx| {
1785 assert_eq!(search_bar.active_match_index, Some(2));
1786 search_bar.select_next_match(&SelectNextMatch, window, cx);
1787 assert_eq!(
1788 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1789 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1790 );
1791 });
1792 search_bar.read_with(cx, |search_bar, _| {
1793 assert_eq!(search_bar.active_match_index, Some(0));
1794 });
1795
1796 // Park the cursor before the first match and ensure that going to the previous match
1797 // selects the last match.
1798 editor.update_in(cx, |editor, window, cx| {
1799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1800 s.select_display_ranges([
1801 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1802 ])
1803 });
1804 });
1805 search_bar.update_in(cx, |search_bar, window, cx| {
1806 assert_eq!(search_bar.active_match_index, Some(0));
1807 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1808 assert_eq!(
1809 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1810 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1811 );
1812 });
1813 search_bar.read_with(cx, |search_bar, _| {
1814 assert_eq!(search_bar.active_match_index, Some(2));
1815 });
1816 }
1817
1818 fn display_points_of(
1819 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1820 ) -> Vec<Range<DisplayPoint>> {
1821 background_highlights
1822 .into_iter()
1823 .map(|(range, _)| range)
1824 .collect::<Vec<_>>()
1825 }
1826
1827 #[gpui::test]
1828 async fn test_search_option_handling(cx: &mut TestAppContext) {
1829 let (editor, search_bar, cx) = init_test(cx);
1830
1831 // show with options should make current search case sensitive
1832 search_bar
1833 .update_in(cx, |search_bar, window, cx| {
1834 search_bar.show(window, cx);
1835 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1836 })
1837 .await
1838 .unwrap();
1839 editor.update_in(cx, |editor, window, cx| {
1840 assert_eq!(
1841 display_points_of(editor.all_text_background_highlights(window, cx)),
1842 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1843 );
1844 });
1845
1846 // search_suggested should restore default options
1847 search_bar.update_in(cx, |search_bar, window, cx| {
1848 search_bar.search_suggested(window, cx);
1849 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1850 });
1851
1852 // toggling a search option should update the defaults
1853 search_bar
1854 .update_in(cx, |search_bar, window, cx| {
1855 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1856 })
1857 .await
1858 .unwrap();
1859 search_bar.update_in(cx, |search_bar, window, cx| {
1860 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1861 });
1862 let mut editor_notifications = cx.notifications(&editor);
1863 editor_notifications.next().await;
1864 editor.update_in(cx, |editor, window, cx| {
1865 assert_eq!(
1866 display_points_of(editor.all_text_background_highlights(window, cx)),
1867 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1868 );
1869 });
1870
1871 // defaults should still include whole word
1872 search_bar.update_in(cx, |search_bar, window, cx| {
1873 search_bar.search_suggested(window, cx);
1874 assert_eq!(
1875 search_bar.search_options,
1876 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1877 )
1878 });
1879 }
1880
1881 #[gpui::test]
1882 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1883 init_globals(cx);
1884 let buffer_text = r#"
1885 A regular expression (shortened as regex or regexp;[1] also referred to as
1886 rational expression[2][3]) is a sequence of characters that specifies a search
1887 pattern in text. Usually such patterns are used by string-searching algorithms
1888 for "find" or "find and replace" operations on strings, or for input validation.
1889 "#
1890 .unindent();
1891 let expected_query_matches_count = buffer_text
1892 .chars()
1893 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1894 .count();
1895 assert!(
1896 expected_query_matches_count > 1,
1897 "Should pick a query with multiple results"
1898 );
1899 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1900 let window = cx.add_window(|_, _| gpui::Empty);
1901
1902 let editor = window.build_entity(cx, |window, cx| {
1903 Editor::for_buffer(buffer.clone(), None, window, cx)
1904 });
1905
1906 let search_bar = window.build_entity(cx, |window, cx| {
1907 let mut search_bar = BufferSearchBar::new(None, window, cx);
1908 search_bar.set_active_pane_item(Some(&editor), window, cx);
1909 search_bar.show(window, cx);
1910 search_bar
1911 });
1912
1913 window
1914 .update(cx, |_, window, cx| {
1915 search_bar.update(cx, |search_bar, cx| {
1916 search_bar.search("a", None, window, cx)
1917 })
1918 })
1919 .unwrap()
1920 .await
1921 .unwrap();
1922 let initial_selections = window
1923 .update(cx, |_, window, cx| {
1924 search_bar.update(cx, |search_bar, cx| {
1925 let handle = search_bar.query_editor.focus_handle(cx);
1926 window.focus(&handle);
1927 search_bar.activate_current_match(window, cx);
1928 });
1929 assert!(
1930 !editor.read(cx).is_focused(window),
1931 "Initially, the editor should not be focused"
1932 );
1933 let initial_selections = editor.update(cx, |editor, cx| {
1934 let initial_selections = editor.selections.display_ranges(cx);
1935 assert_eq!(
1936 initial_selections.len(), 1,
1937 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1938 );
1939 initial_selections
1940 });
1941 search_bar.update(cx, |search_bar, cx| {
1942 assert_eq!(search_bar.active_match_index, Some(0));
1943 let handle = search_bar.query_editor.focus_handle(cx);
1944 window.focus(&handle);
1945 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1946 });
1947 assert!(
1948 editor.read(cx).is_focused(window),
1949 "Should focus editor after successful SelectAllMatches"
1950 );
1951 search_bar.update(cx, |search_bar, cx| {
1952 let all_selections =
1953 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1954 assert_eq!(
1955 all_selections.len(),
1956 expected_query_matches_count,
1957 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1958 );
1959 assert_eq!(
1960 search_bar.active_match_index,
1961 Some(0),
1962 "Match index should not change after selecting all matches"
1963 );
1964 });
1965
1966 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1967 initial_selections
1968 }).unwrap();
1969
1970 window
1971 .update(cx, |_, window, cx| {
1972 assert!(
1973 editor.read(cx).is_focused(window),
1974 "Should still have editor focused after SelectNextMatch"
1975 );
1976 search_bar.update(cx, |search_bar, cx| {
1977 let all_selections =
1978 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1979 assert_eq!(
1980 all_selections.len(),
1981 1,
1982 "On next match, should deselect items and select the next match"
1983 );
1984 assert_ne!(
1985 all_selections, initial_selections,
1986 "Next match should be different from the first selection"
1987 );
1988 assert_eq!(
1989 search_bar.active_match_index,
1990 Some(1),
1991 "Match index should be updated to the next one"
1992 );
1993 let handle = search_bar.query_editor.focus_handle(cx);
1994 window.focus(&handle);
1995 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1996 });
1997 })
1998 .unwrap();
1999 window
2000 .update(cx, |_, window, cx| {
2001 assert!(
2002 editor.read(cx).is_focused(window),
2003 "Should focus editor after successful SelectAllMatches"
2004 );
2005 search_bar.update(cx, |search_bar, cx| {
2006 let all_selections =
2007 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2008 assert_eq!(
2009 all_selections.len(),
2010 expected_query_matches_count,
2011 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2012 );
2013 assert_eq!(
2014 search_bar.active_match_index,
2015 Some(1),
2016 "Match index should not change after selecting all matches"
2017 );
2018 });
2019 search_bar.update(cx, |search_bar, cx| {
2020 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2021 });
2022 })
2023 .unwrap();
2024 let last_match_selections = window
2025 .update(cx, |_, window, cx| {
2026 assert!(
2027 editor.read(cx).is_focused(window),
2028 "Should still have editor focused after SelectPreviousMatch"
2029 );
2030
2031 search_bar.update(cx, |search_bar, cx| {
2032 let all_selections =
2033 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2034 assert_eq!(
2035 all_selections.len(),
2036 1,
2037 "On previous match, should deselect items and select the previous item"
2038 );
2039 assert_eq!(
2040 all_selections, initial_selections,
2041 "Previous match should be the same as the first selection"
2042 );
2043 assert_eq!(
2044 search_bar.active_match_index,
2045 Some(0),
2046 "Match index should be updated to the previous one"
2047 );
2048 all_selections
2049 })
2050 })
2051 .unwrap();
2052
2053 window
2054 .update(cx, |_, window, cx| {
2055 search_bar.update(cx, |search_bar, cx| {
2056 let handle = search_bar.query_editor.focus_handle(cx);
2057 window.focus(&handle);
2058 search_bar.search("abas_nonexistent_match", None, window, cx)
2059 })
2060 })
2061 .unwrap()
2062 .await
2063 .unwrap();
2064 window
2065 .update(cx, |_, window, cx| {
2066 search_bar.update(cx, |search_bar, cx| {
2067 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2068 });
2069 assert!(
2070 editor.update(cx, |this, _cx| !this.is_focused(window)),
2071 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2072 );
2073 search_bar.update(cx, |search_bar, cx| {
2074 let all_selections =
2075 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2076 assert_eq!(
2077 all_selections, last_match_selections,
2078 "Should not select anything new if there are no matches"
2079 );
2080 assert!(
2081 search_bar.active_match_index.is_none(),
2082 "For no matches, there should be no active match index"
2083 );
2084 });
2085 })
2086 .unwrap();
2087 }
2088
2089 #[gpui::test]
2090 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2091 init_globals(cx);
2092 let buffer_text = r#"
2093 self.buffer.update(cx, |buffer, cx| {
2094 buffer.edit(
2095 edits,
2096 Some(AutoindentMode::Block {
2097 original_indent_columns,
2098 }),
2099 cx,
2100 )
2101 });
2102
2103 this.buffer.update(cx, |buffer, cx| {
2104 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2105 });
2106 "#
2107 .unindent();
2108 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2109 let cx = cx.add_empty_window();
2110
2111 let editor =
2112 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2113
2114 let search_bar = cx.new_window_entity(|window, cx| {
2115 let mut search_bar = BufferSearchBar::new(None, window, cx);
2116 search_bar.set_active_pane_item(Some(&editor), window, cx);
2117 search_bar.show(window, cx);
2118 search_bar
2119 });
2120
2121 search_bar
2122 .update_in(cx, |search_bar, window, cx| {
2123 search_bar.search(
2124 "edit\\(",
2125 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2126 window,
2127 cx,
2128 )
2129 })
2130 .await
2131 .unwrap();
2132
2133 search_bar.update_in(cx, |search_bar, window, cx| {
2134 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2135 });
2136 search_bar.update(cx, |_, cx| {
2137 let all_selections =
2138 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2139 assert_eq!(
2140 all_selections.len(),
2141 2,
2142 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2143 );
2144 });
2145
2146 search_bar
2147 .update_in(cx, |search_bar, window, cx| {
2148 search_bar.search(
2149 "edit(",
2150 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2151 window,
2152 cx,
2153 )
2154 })
2155 .await
2156 .unwrap();
2157
2158 search_bar.update_in(cx, |search_bar, window, cx| {
2159 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2160 });
2161 search_bar.update(cx, |_, cx| {
2162 let all_selections =
2163 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2164 assert_eq!(
2165 all_selections.len(),
2166 2,
2167 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2168 );
2169 });
2170 }
2171
2172 #[gpui::test]
2173 async fn test_search_query_history(cx: &mut TestAppContext) {
2174 init_globals(cx);
2175 let buffer_text = r#"
2176 A regular expression (shortened as regex or regexp;[1] also referred to as
2177 rational expression[2][3]) is a sequence of characters that specifies a search
2178 pattern in text. Usually such patterns are used by string-searching algorithms
2179 for "find" or "find and replace" operations on strings, or for input validation.
2180 "#
2181 .unindent();
2182 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2183 let cx = cx.add_empty_window();
2184
2185 let editor =
2186 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2187
2188 let search_bar = cx.new_window_entity(|window, cx| {
2189 let mut search_bar = BufferSearchBar::new(None, window, cx);
2190 search_bar.set_active_pane_item(Some(&editor), window, cx);
2191 search_bar.show(window, cx);
2192 search_bar
2193 });
2194
2195 // Add 3 search items into the history.
2196 search_bar
2197 .update_in(cx, |search_bar, window, cx| {
2198 search_bar.search("a", None, window, cx)
2199 })
2200 .await
2201 .unwrap();
2202 search_bar
2203 .update_in(cx, |search_bar, window, cx| {
2204 search_bar.search("b", None, window, cx)
2205 })
2206 .await
2207 .unwrap();
2208 search_bar
2209 .update_in(cx, |search_bar, window, cx| {
2210 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2211 })
2212 .await
2213 .unwrap();
2214 // Ensure that the latest search is active.
2215 search_bar.update(cx, |search_bar, cx| {
2216 assert_eq!(search_bar.query(cx), "c");
2217 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2218 });
2219
2220 // Next history query after the latest should set the query to the empty string.
2221 search_bar.update_in(cx, |search_bar, window, cx| {
2222 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2223 });
2224 search_bar.update(cx, |search_bar, cx| {
2225 assert_eq!(search_bar.query(cx), "");
2226 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2227 });
2228 search_bar.update_in(cx, |search_bar, window, cx| {
2229 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2230 });
2231 search_bar.update(cx, |search_bar, cx| {
2232 assert_eq!(search_bar.query(cx), "");
2233 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2234 });
2235
2236 // First previous query for empty current query should set the query to the latest.
2237 search_bar.update_in(cx, |search_bar, window, cx| {
2238 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2239 });
2240 search_bar.update(cx, |search_bar, cx| {
2241 assert_eq!(search_bar.query(cx), "c");
2242 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2243 });
2244
2245 // Further previous items should go over the history in reverse order.
2246 search_bar.update_in(cx, |search_bar, window, cx| {
2247 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2248 });
2249 search_bar.update(cx, |search_bar, cx| {
2250 assert_eq!(search_bar.query(cx), "b");
2251 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2252 });
2253
2254 // Previous items should never go behind the first history item.
2255 search_bar.update_in(cx, |search_bar, window, cx| {
2256 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2257 });
2258 search_bar.update(cx, |search_bar, cx| {
2259 assert_eq!(search_bar.query(cx), "a");
2260 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2261 });
2262 search_bar.update_in(cx, |search_bar, window, cx| {
2263 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2264 });
2265 search_bar.update(cx, |search_bar, cx| {
2266 assert_eq!(search_bar.query(cx), "a");
2267 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2268 });
2269
2270 // Next items should go over the history in the original order.
2271 search_bar.update_in(cx, |search_bar, window, cx| {
2272 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2273 });
2274 search_bar.update(cx, |search_bar, cx| {
2275 assert_eq!(search_bar.query(cx), "b");
2276 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2277 });
2278
2279 search_bar
2280 .update_in(cx, |search_bar, window, cx| {
2281 search_bar.search("ba", None, window, cx)
2282 })
2283 .await
2284 .unwrap();
2285 search_bar.update(cx, |search_bar, cx| {
2286 assert_eq!(search_bar.query(cx), "ba");
2287 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2288 });
2289
2290 // New search input should add another entry to history and move the selection to the end of the history.
2291 search_bar.update_in(cx, |search_bar, window, cx| {
2292 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2293 });
2294 search_bar.update(cx, |search_bar, cx| {
2295 assert_eq!(search_bar.query(cx), "c");
2296 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2297 });
2298 search_bar.update_in(cx, |search_bar, window, cx| {
2299 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2300 });
2301 search_bar.update(cx, |search_bar, cx| {
2302 assert_eq!(search_bar.query(cx), "b");
2303 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2304 });
2305 search_bar.update_in(cx, |search_bar, window, cx| {
2306 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2307 });
2308 search_bar.update(cx, |search_bar, cx| {
2309 assert_eq!(search_bar.query(cx), "c");
2310 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2311 });
2312 search_bar.update_in(cx, |search_bar, window, cx| {
2313 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2314 });
2315 search_bar.update(cx, |search_bar, cx| {
2316 assert_eq!(search_bar.query(cx), "ba");
2317 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2318 });
2319 search_bar.update_in(cx, |search_bar, window, cx| {
2320 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2321 });
2322 search_bar.update(cx, |search_bar, cx| {
2323 assert_eq!(search_bar.query(cx), "");
2324 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2325 });
2326 }
2327
2328 #[gpui::test]
2329 async fn test_replace_simple(cx: &mut TestAppContext) {
2330 let (editor, search_bar, cx) = init_test(cx);
2331
2332 search_bar
2333 .update_in(cx, |search_bar, window, cx| {
2334 search_bar.search("expression", None, window, cx)
2335 })
2336 .await
2337 .unwrap();
2338
2339 search_bar.update_in(cx, |search_bar, window, cx| {
2340 search_bar.replacement_editor.update(cx, |editor, cx| {
2341 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2342 editor.set_text("expr$1", window, cx);
2343 });
2344 search_bar.replace_all(&ReplaceAll, window, cx)
2345 });
2346 assert_eq!(
2347 editor.read_with(cx, |this, cx| { this.text(cx) }),
2348 r#"
2349 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2350 rational expr$1[2][3]) is a sequence of characters that specifies a search
2351 pattern in text. Usually such patterns are used by string-searching algorithms
2352 for "find" or "find and replace" operations on strings, or for input validation.
2353 "#
2354 .unindent()
2355 );
2356
2357 // Search for word boundaries and replace just a single one.
2358 search_bar
2359 .update_in(cx, |search_bar, window, cx| {
2360 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2361 })
2362 .await
2363 .unwrap();
2364
2365 search_bar.update_in(cx, |search_bar, window, cx| {
2366 search_bar.replacement_editor.update(cx, |editor, cx| {
2367 editor.set_text("banana", window, cx);
2368 });
2369 search_bar.replace_next(&ReplaceNext, window, cx)
2370 });
2371 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2372 assert_eq!(
2373 editor.read_with(cx, |this, cx| { this.text(cx) }),
2374 r#"
2375 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2376 rational expr$1[2][3]) is a sequence of characters that specifies a search
2377 pattern in text. Usually such patterns are used by string-searching algorithms
2378 for "find" or "find and replace" operations on strings, or for input validation.
2379 "#
2380 .unindent()
2381 );
2382 // Let's turn on regex mode.
2383 search_bar
2384 .update_in(cx, |search_bar, window, cx| {
2385 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2386 })
2387 .await
2388 .unwrap();
2389 search_bar.update_in(cx, |search_bar, window, cx| {
2390 search_bar.replacement_editor.update(cx, |editor, cx| {
2391 editor.set_text("${1}number", window, cx);
2392 });
2393 search_bar.replace_all(&ReplaceAll, window, cx)
2394 });
2395 assert_eq!(
2396 editor.read_with(cx, |this, cx| { this.text(cx) }),
2397 r#"
2398 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2399 rational expr$12number3number) is a sequence of characters that specifies a search
2400 pattern in text. Usually such patterns are used by string-searching algorithms
2401 for "find" or "find and replace" operations on strings, or for input validation.
2402 "#
2403 .unindent()
2404 );
2405 // Now with a whole-word twist.
2406 search_bar
2407 .update_in(cx, |search_bar, window, cx| {
2408 search_bar.search(
2409 "a\\w+s",
2410 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2411 window,
2412 cx,
2413 )
2414 })
2415 .await
2416 .unwrap();
2417 search_bar.update_in(cx, |search_bar, window, cx| {
2418 search_bar.replacement_editor.update(cx, |editor, cx| {
2419 editor.set_text("things", window, cx);
2420 });
2421 search_bar.replace_all(&ReplaceAll, window, cx)
2422 });
2423 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2424 // of words in this text that would match this regex if not for WHOLE_WORD.
2425 assert_eq!(
2426 editor.read_with(cx, |this, cx| { this.text(cx) }),
2427 r#"
2428 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2429 rational expr$12number3number) is a sequence of characters that specifies a search
2430 pattern in text. Usually such patterns are used by string-searching things
2431 for "find" or "find and replace" operations on strings, or for input validation.
2432 "#
2433 .unindent()
2434 );
2435 }
2436
2437 struct ReplacementTestParams<'a> {
2438 editor: &'a Entity<Editor>,
2439 search_bar: &'a Entity<BufferSearchBar>,
2440 cx: &'a mut VisualTestContext,
2441 search_text: &'static str,
2442 search_options: Option<SearchOptions>,
2443 replacement_text: &'static str,
2444 replace_all: bool,
2445 expected_text: String,
2446 }
2447
2448 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2449 options
2450 .search_bar
2451 .update_in(options.cx, |search_bar, window, cx| {
2452 if let Some(options) = options.search_options {
2453 search_bar.set_search_options(options, cx);
2454 }
2455 search_bar.search(options.search_text, options.search_options, window, cx)
2456 })
2457 .await
2458 .unwrap();
2459
2460 options
2461 .search_bar
2462 .update_in(options.cx, |search_bar, window, cx| {
2463 search_bar.replacement_editor.update(cx, |editor, cx| {
2464 editor.set_text(options.replacement_text, window, cx);
2465 });
2466
2467 if options.replace_all {
2468 search_bar.replace_all(&ReplaceAll, window, cx)
2469 } else {
2470 search_bar.replace_next(&ReplaceNext, window, cx)
2471 }
2472 });
2473
2474 assert_eq!(
2475 options
2476 .editor
2477 .read_with(options.cx, |this, cx| { this.text(cx) }),
2478 options.expected_text
2479 );
2480 }
2481
2482 #[gpui::test]
2483 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2484 let (editor, search_bar, cx) = init_test(cx);
2485
2486 run_replacement_test(ReplacementTestParams {
2487 editor: &editor,
2488 search_bar: &search_bar,
2489 cx,
2490 search_text: "expression",
2491 search_options: None,
2492 replacement_text: r"\n",
2493 replace_all: true,
2494 expected_text: r#"
2495 A regular \n (shortened as regex or regexp;[1] also referred to as
2496 rational \n[2][3]) is a sequence of characters that specifies a search
2497 pattern in text. Usually such patterns are used by string-searching algorithms
2498 for "find" or "find and replace" operations on strings, or for input validation.
2499 "#
2500 .unindent(),
2501 })
2502 .await;
2503
2504 run_replacement_test(ReplacementTestParams {
2505 editor: &editor,
2506 search_bar: &search_bar,
2507 cx,
2508 search_text: "or",
2509 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2510 replacement_text: r"\\\n\\\\",
2511 replace_all: false,
2512 expected_text: r#"
2513 A regular \n (shortened as regex \
2514 \\ regexp;[1] also referred to as
2515 rational \n[2][3]) is a sequence of characters that specifies a search
2516 pattern in text. Usually such patterns are used by string-searching algorithms
2517 for "find" or "find and replace" operations on strings, or for input validation.
2518 "#
2519 .unindent(),
2520 })
2521 .await;
2522
2523 run_replacement_test(ReplacementTestParams {
2524 editor: &editor,
2525 search_bar: &search_bar,
2526 cx,
2527 search_text: r"(that|used) ",
2528 search_options: Some(SearchOptions::REGEX),
2529 replacement_text: r"$1\n",
2530 replace_all: true,
2531 expected_text: r#"
2532 A regular \n (shortened as regex \
2533 \\ regexp;[1] also referred to as
2534 rational \n[2][3]) is a sequence of characters that
2535 specifies a search
2536 pattern in text. Usually such patterns are used
2537 by string-searching algorithms
2538 for "find" or "find and replace" operations on strings, or for input validation.
2539 "#
2540 .unindent(),
2541 })
2542 .await;
2543 }
2544
2545 #[gpui::test]
2546 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2547 cx: &mut TestAppContext,
2548 ) {
2549 init_globals(cx);
2550 let buffer = cx.new(|cx| {
2551 Buffer::local(
2552 r#"
2553 aaa bbb aaa ccc
2554 aaa bbb aaa ccc
2555 aaa bbb aaa ccc
2556 aaa bbb aaa ccc
2557 aaa bbb aaa ccc
2558 aaa bbb aaa ccc
2559 "#
2560 .unindent(),
2561 cx,
2562 )
2563 });
2564 let cx = cx.add_empty_window();
2565 let editor =
2566 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2567
2568 let search_bar = cx.new_window_entity(|window, cx| {
2569 let mut search_bar = BufferSearchBar::new(None, window, cx);
2570 search_bar.set_active_pane_item(Some(&editor), window, cx);
2571 search_bar.show(window, cx);
2572 search_bar
2573 });
2574
2575 editor.update_in(cx, |editor, window, cx| {
2576 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2577 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2578 })
2579 });
2580
2581 search_bar.update_in(cx, |search_bar, window, cx| {
2582 let deploy = Deploy {
2583 focus: true,
2584 replace_enabled: false,
2585 selection_search_enabled: true,
2586 };
2587 search_bar.deploy(&deploy, window, cx);
2588 });
2589
2590 cx.run_until_parked();
2591
2592 search_bar
2593 .update_in(cx, |search_bar, window, cx| {
2594 search_bar.search("aaa", None, window, cx)
2595 })
2596 .await
2597 .unwrap();
2598
2599 editor.update(cx, |editor, cx| {
2600 assert_eq!(
2601 editor.search_background_highlights(cx),
2602 &[
2603 Point::new(1, 0)..Point::new(1, 3),
2604 Point::new(1, 8)..Point::new(1, 11),
2605 Point::new(2, 0)..Point::new(2, 3),
2606 ]
2607 );
2608 });
2609 }
2610
2611 #[gpui::test]
2612 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2613 cx: &mut TestAppContext,
2614 ) {
2615 init_globals(cx);
2616 let text = r#"
2617 aaa bbb aaa ccc
2618 aaa bbb aaa ccc
2619 aaa bbb aaa ccc
2620 aaa bbb aaa ccc
2621 aaa bbb aaa ccc
2622 aaa bbb aaa ccc
2623
2624 aaa bbb aaa ccc
2625 aaa bbb aaa ccc
2626 aaa bbb aaa ccc
2627 aaa bbb aaa ccc
2628 aaa bbb aaa ccc
2629 aaa bbb aaa ccc
2630 "#
2631 .unindent();
2632
2633 let cx = cx.add_empty_window();
2634 let editor = cx.new_window_entity(|window, cx| {
2635 let multibuffer = MultiBuffer::build_multi(
2636 [
2637 (
2638 &text,
2639 vec![
2640 Point::new(0, 0)..Point::new(2, 0),
2641 Point::new(4, 0)..Point::new(5, 0),
2642 ],
2643 ),
2644 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2645 ],
2646 cx,
2647 );
2648 Editor::for_multibuffer(multibuffer, None, window, cx)
2649 });
2650
2651 let search_bar = cx.new_window_entity(|window, cx| {
2652 let mut search_bar = BufferSearchBar::new(None, window, cx);
2653 search_bar.set_active_pane_item(Some(&editor), window, cx);
2654 search_bar.show(window, cx);
2655 search_bar
2656 });
2657
2658 editor.update_in(cx, |editor, window, cx| {
2659 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2660 s.select_ranges(vec![
2661 Point::new(1, 0)..Point::new(1, 4),
2662 Point::new(5, 3)..Point::new(6, 4),
2663 ])
2664 })
2665 });
2666
2667 search_bar.update_in(cx, |search_bar, window, cx| {
2668 let deploy = Deploy {
2669 focus: true,
2670 replace_enabled: false,
2671 selection_search_enabled: true,
2672 };
2673 search_bar.deploy(&deploy, window, cx);
2674 });
2675
2676 cx.run_until_parked();
2677
2678 search_bar
2679 .update_in(cx, |search_bar, window, cx| {
2680 search_bar.search("aaa", None, window, cx)
2681 })
2682 .await
2683 .unwrap();
2684
2685 editor.update(cx, |editor, cx| {
2686 assert_eq!(
2687 editor.search_background_highlights(cx),
2688 &[
2689 Point::new(1, 0)..Point::new(1, 3),
2690 Point::new(5, 8)..Point::new(5, 11),
2691 Point::new(6, 0)..Point::new(6, 3),
2692 ]
2693 );
2694 });
2695 }
2696
2697 #[gpui::test]
2698 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2699 let (editor, search_bar, cx) = init_test(cx);
2700 // Search using valid regexp
2701 search_bar
2702 .update_in(cx, |search_bar, window, cx| {
2703 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2704 search_bar.search("expression", None, window, cx)
2705 })
2706 .await
2707 .unwrap();
2708 editor.update_in(cx, |editor, window, cx| {
2709 assert_eq!(
2710 display_points_of(editor.all_text_background_highlights(window, cx)),
2711 &[
2712 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2713 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2714 ],
2715 );
2716 });
2717
2718 // Now, the expression is invalid
2719 search_bar
2720 .update_in(cx, |search_bar, window, cx| {
2721 search_bar.search("expression (", None, window, cx)
2722 })
2723 .await
2724 .unwrap_err();
2725 editor.update_in(cx, |editor, window, cx| {
2726 assert!(
2727 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2728 );
2729 });
2730 }
2731
2732 #[gpui::test]
2733 async fn test_search_options_changes(cx: &mut TestAppContext) {
2734 let (_editor, search_bar, cx) = init_test(cx);
2735 update_search_settings(
2736 SearchSettings {
2737 button: true,
2738 whole_word: false,
2739 case_sensitive: false,
2740 include_ignored: false,
2741 regex: false,
2742 },
2743 cx,
2744 );
2745
2746 let deploy = Deploy {
2747 focus: true,
2748 replace_enabled: false,
2749 selection_search_enabled: true,
2750 };
2751
2752 search_bar.update_in(cx, |search_bar, window, cx| {
2753 assert_eq!(
2754 search_bar.search_options,
2755 SearchOptions::NONE,
2756 "Should have no search options enabled by default"
2757 );
2758 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2759 assert_eq!(
2760 search_bar.search_options,
2761 SearchOptions::WHOLE_WORD,
2762 "Should enable the option toggled"
2763 );
2764 assert!(
2765 !search_bar.dismissed,
2766 "Search bar should be present and visible"
2767 );
2768 search_bar.deploy(&deploy, window, cx);
2769 assert_eq!(
2770 search_bar.configured_options,
2771 SearchOptions::NONE,
2772 "Should have configured search options matching the settings"
2773 );
2774 assert_eq!(
2775 search_bar.search_options,
2776 SearchOptions::WHOLE_WORD,
2777 "After (re)deploying, the option should still be enabled"
2778 );
2779
2780 search_bar.dismiss(&Dismiss, window, cx);
2781 search_bar.deploy(&deploy, window, cx);
2782 assert_eq!(
2783 search_bar.search_options,
2784 SearchOptions::NONE,
2785 "After hiding and showing the search bar, default options should be used"
2786 );
2787
2788 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2789 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2790 assert_eq!(
2791 search_bar.search_options,
2792 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2793 "Should enable the options toggled"
2794 );
2795 assert!(
2796 !search_bar.dismissed,
2797 "Search bar should be present and visible"
2798 );
2799 });
2800
2801 update_search_settings(
2802 SearchSettings {
2803 button: true,
2804 whole_word: false,
2805 case_sensitive: true,
2806 include_ignored: false,
2807 regex: false,
2808 },
2809 cx,
2810 );
2811 search_bar.update_in(cx, |search_bar, window, cx| {
2812 assert_eq!(
2813 search_bar.search_options,
2814 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2815 "Should have no search options enabled by default"
2816 );
2817
2818 search_bar.deploy(&deploy, window, cx);
2819 assert_eq!(
2820 search_bar.configured_options,
2821 SearchOptions::CASE_SENSITIVE,
2822 "Should have configured search options matching the settings"
2823 );
2824 assert_eq!(
2825 search_bar.search_options,
2826 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2827 "Toggling a non-dismissed search bar with custom options should not change the default options"
2828 );
2829 search_bar.dismiss(&Dismiss, window, cx);
2830 search_bar.deploy(&deploy, window, cx);
2831 assert_eq!(
2832 search_bar.search_options,
2833 SearchOptions::CASE_SENSITIVE,
2834 "After hiding and showing the search bar, default options should be used"
2835 );
2836 });
2837 }
2838
2839 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2840 cx.update(|cx| {
2841 SettingsStore::update_global(cx, |store, cx| {
2842 store.update_user_settings::<EditorSettings>(cx, |settings| {
2843 settings.search = Some(search_settings);
2844 });
2845 });
2846 });
2847 }
2848}