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