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