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);
720 let mut select_query = true;
721 if deploy.replace_enabled && handle.is_focused(window) {
722 handle = self.replacement_editor.focus_handle(cx);
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 let configured_options =
753 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
754 let settings_changed = configured_options != self.configured_options;
755
756 if self.dismissed && settings_changed {
757 // Only update configuration options when search bar is dismissed,
758 // so we don't miss updates even after calling show twice
759 self.configured_options = configured_options;
760 self.search_options = configured_options;
761 self.default_options = configured_options;
762 }
763
764 self.dismissed = false;
765 self.adjust_query_regex_language(cx);
766 handle.search_bar_visibility_changed(true, window, cx);
767 cx.notify();
768 cx.emit(Event::UpdateLocation);
769 cx.emit(ToolbarItemEvent::ChangeLocation(
770 ToolbarItemLocation::Secondary,
771 ));
772 true
773 }
774
775 fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
776 self.active_searchable_item
777 .as_ref()
778 .map(|item| item.supported_options(cx))
779 .unwrap_or_default()
780 }
781
782 pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
783 let search = self
784 .query_suggestion(window, cx)
785 .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
786
787 if let Some(search) = search {
788 cx.spawn_in(window, async move |this, cx| {
789 search.await?;
790 this.update_in(cx, |this, window, cx| {
791 this.activate_current_match(window, cx)
792 })
793 })
794 .detach_and_log_err(cx);
795 }
796 }
797
798 pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
799 if let Some(match_ix) = self.active_match_index
800 && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
801 && let Some(matches) = self
802 .searchable_items_with_matches
803 .get(&active_searchable_item.downgrade())
804 {
805 active_searchable_item.activate_match(match_ix, matches, window, cx)
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
955 && self.active_match_index.is_some()
956 && let Some(searchable_item) = self.active_searchable_item.as_ref()
957 && let Some(matches) = self
958 .searchable_items_with_matches
959 .get(&searchable_item.downgrade())
960 {
961 searchable_item.select_matches(matches, window, cx);
962 self.focus_editor(&FocusEditor, window, cx);
963 }
964 }
965
966 pub fn select_match(
967 &mut self,
968 direction: Direction,
969 count: usize,
970 window: &mut Window,
971 cx: &mut Context<Self>,
972 ) {
973 if let Some(index) = self.active_match_index
974 && let Some(searchable_item) = self.active_searchable_item.as_ref()
975 && let Some(matches) = self
976 .searchable_items_with_matches
977 .get(&searchable_item.downgrade())
978 .filter(|matches| !matches.is_empty())
979 {
980 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
981 if !EditorSettings::get_global(cx).search_wrap
982 && ((direction == Direction::Next && index + count >= matches.len())
983 || (direction == Direction::Prev && index < count))
984 {
985 crate::show_no_more_matches(window, cx);
986 return;
987 }
988 let new_match_index = searchable_item
989 .match_index_for_direction(matches, index, direction, count, window, cx);
990
991 searchable_item.update_matches(matches, window, cx);
992 searchable_item.activate_match(new_match_index, matches, window, cx);
993 }
994 }
995
996 pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
997 if let Some(searchable_item) = self.active_searchable_item.as_ref()
998 && let Some(matches) = self
999 .searchable_items_with_matches
1000 .get(&searchable_item.downgrade())
1001 {
1002 if matches.is_empty() {
1003 return;
1004 }
1005 searchable_item.update_matches(matches, window, cx);
1006 searchable_item.activate_match(0, matches, window, cx);
1007 }
1008 }
1009
1010 pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1011 if let Some(searchable_item) = self.active_searchable_item.as_ref()
1012 && let Some(matches) = self
1013 .searchable_items_with_matches
1014 .get(&searchable_item.downgrade())
1015 {
1016 if matches.is_empty() {
1017 return;
1018 }
1019 let new_match_index = matches.len() - 1;
1020 searchable_item.update_matches(matches, window, cx);
1021 searchable_item.activate_match(new_match_index, matches, window, cx);
1022 }
1023 }
1024
1025 fn on_query_editor_event(
1026 &mut self,
1027 editor: &Entity<Editor>,
1028 event: &editor::EditorEvent,
1029 window: &mut Window,
1030 cx: &mut Context<Self>,
1031 ) {
1032 match event {
1033 editor::EditorEvent::Focused => self.query_editor_focused = true,
1034 editor::EditorEvent::Blurred => self.query_editor_focused = false,
1035 editor::EditorEvent::Edited { .. } => {
1036 self.smartcase(window, cx);
1037 self.clear_matches(window, cx);
1038 let search = self.update_matches(false, window, cx);
1039
1040 let width = editor.update(cx, |editor, cx| {
1041 let text_layout_details = editor.text_layout_details(window);
1042 let snapshot = editor.snapshot(window, cx).display_snapshot;
1043
1044 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1045 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1046 });
1047 self.editor_needed_width = width;
1048 cx.notify();
1049
1050 cx.spawn_in(window, async move |this, cx| {
1051 search.await?;
1052 this.update_in(cx, |this, window, cx| {
1053 this.activate_current_match(window, cx)
1054 })
1055 })
1056 .detach_and_log_err(cx);
1057 }
1058 _ => {}
1059 }
1060 }
1061
1062 fn on_replacement_editor_event(
1063 &mut self,
1064 _: Entity<Editor>,
1065 event: &editor::EditorEvent,
1066 _: &mut Context<Self>,
1067 ) {
1068 match event {
1069 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1070 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1071 _ => {}
1072 }
1073 }
1074
1075 fn on_active_searchable_item_event(
1076 &mut self,
1077 event: &SearchEvent,
1078 window: &mut Window,
1079 cx: &mut Context<Self>,
1080 ) {
1081 match event {
1082 SearchEvent::MatchesInvalidated => {
1083 drop(self.update_matches(false, window, cx));
1084 }
1085 SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1086 }
1087 }
1088
1089 fn toggle_case_sensitive(
1090 &mut self,
1091 _: &ToggleCaseSensitive,
1092 window: &mut Window,
1093 cx: &mut Context<Self>,
1094 ) {
1095 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1096 }
1097
1098 fn toggle_whole_word(
1099 &mut self,
1100 _: &ToggleWholeWord,
1101 window: &mut Window,
1102 cx: &mut Context<Self>,
1103 ) {
1104 self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1105 }
1106
1107 fn toggle_selection(
1108 &mut self,
1109 _: &ToggleSelection,
1110 window: &mut Window,
1111 cx: &mut Context<Self>,
1112 ) {
1113 if let Some(active_item) = self.active_searchable_item.as_mut() {
1114 self.selection_search_enabled = !self.selection_search_enabled;
1115 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1116 drop(self.update_matches(false, window, cx));
1117 cx.notify();
1118 }
1119 }
1120
1121 fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1122 self.toggle_search_option(SearchOptions::REGEX, window, cx)
1123 }
1124
1125 fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1126 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1127 self.active_match_index = None;
1128 self.searchable_items_with_matches
1129 .remove(&active_searchable_item.downgrade());
1130 active_searchable_item.clear_matches(window, cx);
1131 }
1132 }
1133
1134 pub fn has_active_match(&self) -> bool {
1135 self.active_match_index.is_some()
1136 }
1137
1138 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1139 let mut active_item_matches = None;
1140 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1141 if let Some(searchable_item) =
1142 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1143 {
1144 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1145 active_item_matches = Some((searchable_item.downgrade(), matches));
1146 } else {
1147 searchable_item.clear_matches(window, cx);
1148 }
1149 }
1150 }
1151
1152 self.searchable_items_with_matches
1153 .extend(active_item_matches);
1154 }
1155
1156 fn update_matches(
1157 &mut self,
1158 reuse_existing_query: bool,
1159 window: &mut Window,
1160 cx: &mut Context<Self>,
1161 ) -> oneshot::Receiver<()> {
1162 let (done_tx, done_rx) = oneshot::channel();
1163 let query = self.query(cx);
1164 self.pending_search.take();
1165
1166 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1167 self.query_error = None;
1168 if query.is_empty() {
1169 self.clear_active_searchable_item_matches(window, cx);
1170 let _ = done_tx.send(());
1171 cx.notify();
1172 } else {
1173 let query: Arc<_> = if let Some(search) =
1174 self.active_search.take().filter(|_| reuse_existing_query)
1175 {
1176 search
1177 } else {
1178 if self.search_options.contains(SearchOptions::REGEX) {
1179 match SearchQuery::regex(
1180 query,
1181 self.search_options.contains(SearchOptions::WHOLE_WORD),
1182 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1183 false,
1184 self.search_options
1185 .contains(SearchOptions::ONE_MATCH_PER_LINE),
1186 Default::default(),
1187 Default::default(),
1188 false,
1189 None,
1190 ) {
1191 Ok(query) => query.with_replacement(self.replacement(cx)),
1192 Err(e) => {
1193 self.query_error = Some(e.to_string());
1194 self.clear_active_searchable_item_matches(window, cx);
1195 cx.notify();
1196 return done_rx;
1197 }
1198 }
1199 } else {
1200 match SearchQuery::text(
1201 query,
1202 self.search_options.contains(SearchOptions::WHOLE_WORD),
1203 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1204 false,
1205 Default::default(),
1206 Default::default(),
1207 false,
1208 None,
1209 ) {
1210 Ok(query) => query.with_replacement(self.replacement(cx)),
1211 Err(e) => {
1212 self.query_error = Some(e.to_string());
1213 self.clear_active_searchable_item_matches(window, cx);
1214 cx.notify();
1215 return done_rx;
1216 }
1217 }
1218 }
1219 .into()
1220 };
1221
1222 self.active_search = Some(query.clone());
1223 let query_text = query.as_str().to_string();
1224
1225 let matches = active_searchable_item.find_matches(query, window, cx);
1226
1227 let active_searchable_item = active_searchable_item.downgrade();
1228 self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1229 let matches = matches.await;
1230
1231 this.update_in(cx, |this, window, cx| {
1232 if let Some(active_searchable_item) =
1233 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1234 {
1235 this.searchable_items_with_matches
1236 .insert(active_searchable_item.downgrade(), matches);
1237
1238 this.update_match_index(window, cx);
1239 this.search_history
1240 .add(&mut this.search_history_cursor, query_text);
1241 if !this.dismissed {
1242 let matches = this
1243 .searchable_items_with_matches
1244 .get(&active_searchable_item.downgrade())
1245 .unwrap();
1246 if matches.is_empty() {
1247 active_searchable_item.clear_matches(window, cx);
1248 } else {
1249 active_searchable_item.update_matches(matches, window, cx);
1250 }
1251 let _ = done_tx.send(());
1252 }
1253 cx.notify();
1254 }
1255 })
1256 .log_err();
1257 }));
1258 }
1259 }
1260 done_rx
1261 }
1262
1263 fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1264 if self.search_options.contains(SearchOptions::BACKWARDS) {
1265 direction.opposite()
1266 } else {
1267 direction
1268 }
1269 }
1270
1271 pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1272 let direction = self.reverse_direction_if_backwards(Direction::Next);
1273 let new_index = self
1274 .active_searchable_item
1275 .as_ref()
1276 .and_then(|searchable_item| {
1277 let matches = self
1278 .searchable_items_with_matches
1279 .get(&searchable_item.downgrade())?;
1280 searchable_item.active_match_index(direction, matches, window, cx)
1281 });
1282 if new_index != self.active_match_index {
1283 self.active_match_index = new_index;
1284 cx.notify();
1285 }
1286 }
1287
1288 fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1289 self.cycle_field(Direction::Next, window, cx);
1290 }
1291
1292 fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1293 self.cycle_field(Direction::Prev, window, cx);
1294 }
1295 fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1296 let mut handles = vec![self.query_editor.focus_handle(cx)];
1297 if self.replace_enabled {
1298 handles.push(self.replacement_editor.focus_handle(cx));
1299 }
1300 if let Some(item) = self.active_searchable_item.as_ref() {
1301 handles.push(item.item_focus_handle(cx));
1302 }
1303 let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1304 Some(index) => index,
1305 None => return,
1306 };
1307
1308 let new_index = match direction {
1309 Direction::Next => (current_index + 1) % handles.len(),
1310 Direction::Prev if current_index == 0 => handles.len() - 1,
1311 Direction::Prev => (current_index - 1) % handles.len(),
1312 };
1313 let next_focus_handle = &handles[new_index];
1314 self.focus(next_focus_handle, window);
1315 cx.stop_propagation();
1316 }
1317
1318 fn next_history_query(
1319 &mut self,
1320 _: &NextHistoryQuery,
1321 window: &mut Window,
1322 cx: &mut Context<Self>,
1323 ) {
1324 if let Some(new_query) = self
1325 .search_history
1326 .next(&mut self.search_history_cursor)
1327 .map(str::to_string)
1328 {
1329 drop(self.search(&new_query, Some(self.search_options), window, cx));
1330 } else {
1331 self.search_history_cursor.reset();
1332 drop(self.search("", Some(self.search_options), window, cx));
1333 }
1334 }
1335
1336 fn previous_history_query(
1337 &mut self,
1338 _: &PreviousHistoryQuery,
1339 window: &mut Window,
1340 cx: &mut Context<Self>,
1341 ) {
1342 if self.query(cx).is_empty()
1343 && let Some(new_query) = self
1344 .search_history
1345 .current(&self.search_history_cursor)
1346 .map(str::to_string)
1347 {
1348 drop(self.search(&new_query, Some(self.search_options), window, cx));
1349 return;
1350 }
1351
1352 if let Some(new_query) = self
1353 .search_history
1354 .previous(&mut self.search_history_cursor)
1355 .map(str::to_string)
1356 {
1357 drop(self.search(&new_query, Some(self.search_options), window, cx));
1358 }
1359 }
1360
1361 fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1362 window.invalidate_character_coordinates();
1363 window.focus(handle);
1364 }
1365
1366 fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1367 if self.active_searchable_item.is_some() {
1368 self.replace_enabled = !self.replace_enabled;
1369 let handle = if self.replace_enabled {
1370 self.replacement_editor.focus_handle(cx)
1371 } else {
1372 self.query_editor.focus_handle(cx)
1373 };
1374 self.focus(&handle, window);
1375 cx.notify();
1376 }
1377 }
1378
1379 fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1380 let mut should_propagate = true;
1381 if !self.dismissed
1382 && self.active_search.is_some()
1383 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1384 && let Some(query) = self.active_search.as_ref()
1385 && let Some(matches) = self
1386 .searchable_items_with_matches
1387 .get(&searchable_item.downgrade())
1388 {
1389 if let Some(active_index) = self.active_match_index {
1390 let query = query
1391 .as_ref()
1392 .clone()
1393 .with_replacement(self.replacement(cx));
1394 searchable_item.replace(matches.at(active_index), &query, window, cx);
1395 self.select_next_match(&SelectNextMatch, window, cx);
1396 }
1397 should_propagate = false;
1398 }
1399 if !should_propagate {
1400 cx.stop_propagation();
1401 }
1402 }
1403
1404 pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1405 if !self.dismissed
1406 && self.active_search.is_some()
1407 && let Some(searchable_item) = self.active_searchable_item.as_ref()
1408 && let Some(query) = self.active_search.as_ref()
1409 && let Some(matches) = self
1410 .searchable_items_with_matches
1411 .get(&searchable_item.downgrade())
1412 {
1413 let query = query
1414 .as_ref()
1415 .clone()
1416 .with_replacement(self.replacement(cx));
1417 searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1418 }
1419 }
1420
1421 pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1422 self.update_match_index(window, cx);
1423 self.active_match_index.is_some()
1424 }
1425
1426 pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1427 EditorSettings::get_global(cx).use_smartcase_search
1428 }
1429
1430 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1431 str.chars().any(|c| c.is_uppercase())
1432 }
1433
1434 fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1435 if self.should_use_smartcase_search(cx) {
1436 let query = self.query(cx);
1437 if !query.is_empty() {
1438 let is_case = self.is_contains_uppercase(&query);
1439 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1440 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1441 }
1442 }
1443 }
1444 }
1445
1446 fn adjust_query_regex_language(&self, cx: &mut App) {
1447 let enable = self.search_options.contains(SearchOptions::REGEX);
1448 let query_buffer = self
1449 .query_editor
1450 .read(cx)
1451 .buffer()
1452 .read(cx)
1453 .as_singleton()
1454 .expect("query editor should be backed by a singleton buffer");
1455 if enable {
1456 if let Some(regex_language) = self.regex_language.clone() {
1457 query_buffer.update(cx, |query_buffer, cx| {
1458 query_buffer.set_language(Some(regex_language), cx);
1459 })
1460 }
1461 } else {
1462 query_buffer.update(cx, |query_buffer, cx| {
1463 query_buffer.set_language(None, cx);
1464 })
1465 }
1466 }
1467}
1468
1469#[cfg(test)]
1470mod tests {
1471 use std::ops::Range;
1472
1473 use super::*;
1474 use editor::{
1475 DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1476 display_map::DisplayRow,
1477 };
1478 use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1479 use language::{Buffer, Point};
1480 use project::Project;
1481 use settings::SettingsStore;
1482 use smol::stream::StreamExt as _;
1483 use unindent::Unindent as _;
1484
1485 fn init_globals(cx: &mut TestAppContext) {
1486 cx.update(|cx| {
1487 let store = settings::SettingsStore::test(cx);
1488 cx.set_global(store);
1489 workspace::init_settings(cx);
1490 editor::init(cx);
1491
1492 language::init(cx);
1493 Project::init_settings(cx);
1494 theme::init(theme::LoadThemes::JustBase, cx);
1495 crate::init(cx);
1496 });
1497 }
1498
1499 fn init_test(
1500 cx: &mut TestAppContext,
1501 ) -> (
1502 Entity<Editor>,
1503 Entity<BufferSearchBar>,
1504 &mut VisualTestContext,
1505 ) {
1506 init_globals(cx);
1507 let buffer = cx.new(|cx| {
1508 Buffer::local(
1509 r#"
1510 A regular expression (shortened as regex or regexp;[1] also referred to as
1511 rational expression[2][3]) is a sequence of characters that specifies a search
1512 pattern in text. Usually such patterns are used by string-searching algorithms
1513 for "find" or "find and replace" operations on strings, or for input validation.
1514 "#
1515 .unindent(),
1516 cx,
1517 )
1518 });
1519 let cx = cx.add_empty_window();
1520 let editor =
1521 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1522
1523 let search_bar = cx.new_window_entity(|window, cx| {
1524 let mut search_bar = BufferSearchBar::new(None, window, cx);
1525 search_bar.set_active_pane_item(Some(&editor), window, cx);
1526 search_bar.show(window, cx);
1527 search_bar
1528 });
1529
1530 (editor, search_bar, cx)
1531 }
1532
1533 #[gpui::test]
1534 async fn test_search_simple(cx: &mut TestAppContext) {
1535 let (editor, search_bar, cx) = init_test(cx);
1536 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1537 background_highlights
1538 .into_iter()
1539 .map(|(range, _)| range)
1540 .collect::<Vec<_>>()
1541 };
1542 // Search for a string that appears with different casing.
1543 // By default, search is case-insensitive.
1544 search_bar
1545 .update_in(cx, |search_bar, window, cx| {
1546 search_bar.search("us", None, window, cx)
1547 })
1548 .await
1549 .unwrap();
1550 editor.update_in(cx, |editor, window, cx| {
1551 assert_eq!(
1552 display_points_of(editor.all_text_background_highlights(window, cx)),
1553 &[
1554 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1555 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1556 ]
1557 );
1558 });
1559
1560 // Switch to a case sensitive search.
1561 search_bar.update_in(cx, |search_bar, window, cx| {
1562 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1563 });
1564 let mut editor_notifications = cx.notifications(&editor);
1565 editor_notifications.next().await;
1566 editor.update_in(cx, |editor, window, cx| {
1567 assert_eq!(
1568 display_points_of(editor.all_text_background_highlights(window, cx)),
1569 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1570 );
1571 });
1572
1573 // Search for a string that appears both as a whole word and
1574 // within other words. By default, all results are found.
1575 search_bar
1576 .update_in(cx, |search_bar, window, cx| {
1577 search_bar.search("or", None, window, cx)
1578 })
1579 .await
1580 .unwrap();
1581 editor.update_in(cx, |editor, window, cx| {
1582 assert_eq!(
1583 display_points_of(editor.all_text_background_highlights(window, cx)),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1586 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1587 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1588 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1589 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1590 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1591 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1592 ]
1593 );
1594 });
1595
1596 // Switch to a whole word search.
1597 search_bar.update_in(cx, |search_bar, window, cx| {
1598 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1599 });
1600 let mut editor_notifications = cx.notifications(&editor);
1601 editor_notifications.next().await;
1602 editor.update_in(cx, |editor, window, cx| {
1603 assert_eq!(
1604 display_points_of(editor.all_text_background_highlights(window, cx)),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1607 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1608 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1609 ]
1610 );
1611 });
1612
1613 editor.update_in(cx, |editor, window, cx| {
1614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1615 s.select_display_ranges([
1616 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1617 ])
1618 });
1619 });
1620 search_bar.update_in(cx, |search_bar, window, cx| {
1621 assert_eq!(search_bar.active_match_index, Some(0));
1622 search_bar.select_next_match(&SelectNextMatch, window, cx);
1623 assert_eq!(
1624 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1625 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1626 );
1627 });
1628 search_bar.read_with(cx, |search_bar, _| {
1629 assert_eq!(search_bar.active_match_index, Some(0));
1630 });
1631
1632 search_bar.update_in(cx, |search_bar, window, cx| {
1633 search_bar.select_next_match(&SelectNextMatch, window, cx);
1634 assert_eq!(
1635 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1636 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1637 );
1638 });
1639 search_bar.read_with(cx, |search_bar, _| {
1640 assert_eq!(search_bar.active_match_index, Some(1));
1641 });
1642
1643 search_bar.update_in(cx, |search_bar, window, cx| {
1644 search_bar.select_next_match(&SelectNextMatch, window, cx);
1645 assert_eq!(
1646 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1647 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1648 );
1649 });
1650 search_bar.read_with(cx, |search_bar, _| {
1651 assert_eq!(search_bar.active_match_index, Some(2));
1652 });
1653
1654 search_bar.update_in(cx, |search_bar, window, cx| {
1655 search_bar.select_next_match(&SelectNextMatch, window, cx);
1656 assert_eq!(
1657 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1658 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1659 );
1660 });
1661 search_bar.read_with(cx, |search_bar, _| {
1662 assert_eq!(search_bar.active_match_index, Some(0));
1663 });
1664
1665 search_bar.update_in(cx, |search_bar, window, cx| {
1666 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1667 assert_eq!(
1668 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1669 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1670 );
1671 });
1672 search_bar.read_with(cx, |search_bar, _| {
1673 assert_eq!(search_bar.active_match_index, Some(2));
1674 });
1675
1676 search_bar.update_in(cx, |search_bar, window, cx| {
1677 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1678 assert_eq!(
1679 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1680 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1681 );
1682 });
1683 search_bar.read_with(cx, |search_bar, _| {
1684 assert_eq!(search_bar.active_match_index, Some(1));
1685 });
1686
1687 search_bar.update_in(cx, |search_bar, window, cx| {
1688 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1689 assert_eq!(
1690 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1691 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1692 );
1693 });
1694 search_bar.read_with(cx, |search_bar, _| {
1695 assert_eq!(search_bar.active_match_index, Some(0));
1696 });
1697
1698 // Park the cursor in between matches and ensure that going to the previous match selects
1699 // the closest match to the left.
1700 editor.update_in(cx, |editor, window, cx| {
1701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1702 s.select_display_ranges([
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1704 ])
1705 });
1706 });
1707 search_bar.update_in(cx, |search_bar, window, cx| {
1708 assert_eq!(search_bar.active_match_index, Some(1));
1709 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1710 assert_eq!(
1711 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1712 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1713 );
1714 });
1715 search_bar.read_with(cx, |search_bar, _| {
1716 assert_eq!(search_bar.active_match_index, Some(0));
1717 });
1718
1719 // Park the cursor in between matches and ensure that going to the next match selects the
1720 // closest match to the right.
1721 editor.update_in(cx, |editor, window, cx| {
1722 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1723 s.select_display_ranges([
1724 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1725 ])
1726 });
1727 });
1728 search_bar.update_in(cx, |search_bar, window, cx| {
1729 assert_eq!(search_bar.active_match_index, Some(1));
1730 search_bar.select_next_match(&SelectNextMatch, window, cx);
1731 assert_eq!(
1732 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1733 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1734 );
1735 });
1736 search_bar.read_with(cx, |search_bar, _| {
1737 assert_eq!(search_bar.active_match_index, Some(1));
1738 });
1739
1740 // Park the cursor after the last match and ensure that going to the previous match selects
1741 // the last match.
1742 editor.update_in(cx, |editor, window, cx| {
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1746 ])
1747 });
1748 });
1749 search_bar.update_in(cx, |search_bar, window, cx| {
1750 assert_eq!(search_bar.active_match_index, Some(2));
1751 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1752 assert_eq!(
1753 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1754 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1755 );
1756 });
1757 search_bar.read_with(cx, |search_bar, _| {
1758 assert_eq!(search_bar.active_match_index, Some(2));
1759 });
1760
1761 // Park the cursor after the last match and ensure that going to the next match selects the
1762 // first match.
1763 editor.update_in(cx, |editor, window, cx| {
1764 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1765 s.select_display_ranges([
1766 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1767 ])
1768 });
1769 });
1770 search_bar.update_in(cx, |search_bar, window, cx| {
1771 assert_eq!(search_bar.active_match_index, Some(2));
1772 search_bar.select_next_match(&SelectNextMatch, window, cx);
1773 assert_eq!(
1774 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1775 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1776 );
1777 });
1778 search_bar.read_with(cx, |search_bar, _| {
1779 assert_eq!(search_bar.active_match_index, Some(0));
1780 });
1781
1782 // Park the cursor before the first match and ensure that going to the previous match
1783 // selects the last match.
1784 editor.update_in(cx, |editor, window, cx| {
1785 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1786 s.select_display_ranges([
1787 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1788 ])
1789 });
1790 });
1791 search_bar.update_in(cx, |search_bar, window, cx| {
1792 assert_eq!(search_bar.active_match_index, Some(0));
1793 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1794 assert_eq!(
1795 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1796 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1797 );
1798 });
1799 search_bar.read_with(cx, |search_bar, _| {
1800 assert_eq!(search_bar.active_match_index, Some(2));
1801 });
1802 }
1803
1804 fn display_points_of(
1805 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1806 ) -> Vec<Range<DisplayPoint>> {
1807 background_highlights
1808 .into_iter()
1809 .map(|(range, _)| range)
1810 .collect::<Vec<_>>()
1811 }
1812
1813 #[gpui::test]
1814 async fn test_search_option_handling(cx: &mut TestAppContext) {
1815 let (editor, search_bar, cx) = init_test(cx);
1816
1817 // show with options should make current search case sensitive
1818 search_bar
1819 .update_in(cx, |search_bar, window, cx| {
1820 search_bar.show(window, cx);
1821 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1822 })
1823 .await
1824 .unwrap();
1825 editor.update_in(cx, |editor, window, cx| {
1826 assert_eq!(
1827 display_points_of(editor.all_text_background_highlights(window, cx)),
1828 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1829 );
1830 });
1831
1832 // search_suggested should restore default options
1833 search_bar.update_in(cx, |search_bar, window, cx| {
1834 search_bar.search_suggested(window, cx);
1835 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1836 });
1837
1838 // toggling a search option should update the defaults
1839 search_bar
1840 .update_in(cx, |search_bar, window, cx| {
1841 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1842 })
1843 .await
1844 .unwrap();
1845 search_bar.update_in(cx, |search_bar, window, cx| {
1846 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1847 });
1848 let mut editor_notifications = cx.notifications(&editor);
1849 editor_notifications.next().await;
1850 editor.update_in(cx, |editor, window, cx| {
1851 assert_eq!(
1852 display_points_of(editor.all_text_background_highlights(window, cx)),
1853 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1854 );
1855 });
1856
1857 // defaults should still include whole word
1858 search_bar.update_in(cx, |search_bar, window, cx| {
1859 search_bar.search_suggested(window, cx);
1860 assert_eq!(
1861 search_bar.search_options,
1862 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1863 )
1864 });
1865 }
1866
1867 #[gpui::test]
1868 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1869 init_globals(cx);
1870 let buffer_text = r#"
1871 A regular expression (shortened as regex or regexp;[1] also referred to as
1872 rational expression[2][3]) is a sequence of characters that specifies a search
1873 pattern in text. Usually such patterns are used by string-searching algorithms
1874 for "find" or "find and replace" operations on strings, or for input validation.
1875 "#
1876 .unindent();
1877 let expected_query_matches_count = buffer_text
1878 .chars()
1879 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1880 .count();
1881 assert!(
1882 expected_query_matches_count > 1,
1883 "Should pick a query with multiple results"
1884 );
1885 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1886 let window = cx.add_window(|_, _| gpui::Empty);
1887
1888 let editor = window.build_entity(cx, |window, cx| {
1889 Editor::for_buffer(buffer.clone(), None, window, cx)
1890 });
1891
1892 let search_bar = window.build_entity(cx, |window, cx| {
1893 let mut search_bar = BufferSearchBar::new(None, window, cx);
1894 search_bar.set_active_pane_item(Some(&editor), window, cx);
1895 search_bar.show(window, cx);
1896 search_bar
1897 });
1898
1899 window
1900 .update(cx, |_, window, cx| {
1901 search_bar.update(cx, |search_bar, cx| {
1902 search_bar.search("a", None, window, cx)
1903 })
1904 })
1905 .unwrap()
1906 .await
1907 .unwrap();
1908 let initial_selections = window
1909 .update(cx, |_, window, cx| {
1910 search_bar.update(cx, |search_bar, cx| {
1911 let handle = search_bar.query_editor.focus_handle(cx);
1912 window.focus(&handle);
1913 search_bar.activate_current_match(window, cx);
1914 });
1915 assert!(
1916 !editor.read(cx).is_focused(window),
1917 "Initially, the editor should not be focused"
1918 );
1919 let initial_selections = editor.update(cx, |editor, cx| {
1920 let initial_selections = editor.selections.display_ranges(cx);
1921 assert_eq!(
1922 initial_selections.len(), 1,
1923 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1924 );
1925 initial_selections
1926 });
1927 search_bar.update(cx, |search_bar, cx| {
1928 assert_eq!(search_bar.active_match_index, Some(0));
1929 let handle = search_bar.query_editor.focus_handle(cx);
1930 window.focus(&handle);
1931 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1932 });
1933 assert!(
1934 editor.read(cx).is_focused(window),
1935 "Should focus editor after successful SelectAllMatches"
1936 );
1937 search_bar.update(cx, |search_bar, cx| {
1938 let all_selections =
1939 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1940 assert_eq!(
1941 all_selections.len(),
1942 expected_query_matches_count,
1943 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1944 );
1945 assert_eq!(
1946 search_bar.active_match_index,
1947 Some(0),
1948 "Match index should not change after selecting all matches"
1949 );
1950 });
1951
1952 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1953 initial_selections
1954 }).unwrap();
1955
1956 window
1957 .update(cx, |_, window, cx| {
1958 assert!(
1959 editor.read(cx).is_focused(window),
1960 "Should still have editor focused after SelectNextMatch"
1961 );
1962 search_bar.update(cx, |search_bar, cx| {
1963 let all_selections =
1964 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1965 assert_eq!(
1966 all_selections.len(),
1967 1,
1968 "On next match, should deselect items and select the next match"
1969 );
1970 assert_ne!(
1971 all_selections, initial_selections,
1972 "Next match should be different from the first selection"
1973 );
1974 assert_eq!(
1975 search_bar.active_match_index,
1976 Some(1),
1977 "Match index should be updated to the next one"
1978 );
1979 let handle = search_bar.query_editor.focus_handle(cx);
1980 window.focus(&handle);
1981 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1982 });
1983 })
1984 .unwrap();
1985 window
1986 .update(cx, |_, window, cx| {
1987 assert!(
1988 editor.read(cx).is_focused(window),
1989 "Should focus editor after successful SelectAllMatches"
1990 );
1991 search_bar.update(cx, |search_bar, cx| {
1992 let all_selections =
1993 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1994 assert_eq!(
1995 all_selections.len(),
1996 expected_query_matches_count,
1997 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1998 );
1999 assert_eq!(
2000 search_bar.active_match_index,
2001 Some(1),
2002 "Match index should not change after selecting all matches"
2003 );
2004 });
2005 search_bar.update(cx, |search_bar, cx| {
2006 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2007 });
2008 })
2009 .unwrap();
2010 let last_match_selections = window
2011 .update(cx, |_, window, cx| {
2012 assert!(
2013 editor.read(cx).is_focused(window),
2014 "Should still have editor focused after SelectPreviousMatch"
2015 );
2016
2017 search_bar.update(cx, |search_bar, cx| {
2018 let all_selections =
2019 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2020 assert_eq!(
2021 all_selections.len(),
2022 1,
2023 "On previous match, should deselect items and select the previous item"
2024 );
2025 assert_eq!(
2026 all_selections, initial_selections,
2027 "Previous match should be the same as the first selection"
2028 );
2029 assert_eq!(
2030 search_bar.active_match_index,
2031 Some(0),
2032 "Match index should be updated to the previous one"
2033 );
2034 all_selections
2035 })
2036 })
2037 .unwrap();
2038
2039 window
2040 .update(cx, |_, window, cx| {
2041 search_bar.update(cx, |search_bar, cx| {
2042 let handle = search_bar.query_editor.focus_handle(cx);
2043 window.focus(&handle);
2044 search_bar.search("abas_nonexistent_match", None, window, cx)
2045 })
2046 })
2047 .unwrap()
2048 .await
2049 .unwrap();
2050 window
2051 .update(cx, |_, window, cx| {
2052 search_bar.update(cx, |search_bar, cx| {
2053 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2054 });
2055 assert!(
2056 editor.update(cx, |this, _cx| !this.is_focused(window)),
2057 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2058 );
2059 search_bar.update(cx, |search_bar, cx| {
2060 let all_selections =
2061 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2062 assert_eq!(
2063 all_selections, last_match_selections,
2064 "Should not select anything new if there are no matches"
2065 );
2066 assert!(
2067 search_bar.active_match_index.is_none(),
2068 "For no matches, there should be no active match index"
2069 );
2070 });
2071 })
2072 .unwrap();
2073 }
2074
2075 #[gpui::test]
2076 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2077 init_globals(cx);
2078 let buffer_text = r#"
2079 self.buffer.update(cx, |buffer, cx| {
2080 buffer.edit(
2081 edits,
2082 Some(AutoindentMode::Block {
2083 original_indent_columns,
2084 }),
2085 cx,
2086 )
2087 });
2088
2089 this.buffer.update(cx, |buffer, cx| {
2090 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2091 });
2092 "#
2093 .unindent();
2094 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2095 let cx = cx.add_empty_window();
2096
2097 let editor =
2098 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2099
2100 let search_bar = cx.new_window_entity(|window, cx| {
2101 let mut search_bar = BufferSearchBar::new(None, window, cx);
2102 search_bar.set_active_pane_item(Some(&editor), window, cx);
2103 search_bar.show(window, cx);
2104 search_bar
2105 });
2106
2107 search_bar
2108 .update_in(cx, |search_bar, window, cx| {
2109 search_bar.search(
2110 "edit\\(",
2111 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2112 window,
2113 cx,
2114 )
2115 })
2116 .await
2117 .unwrap();
2118
2119 search_bar.update_in(cx, |search_bar, window, cx| {
2120 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2121 });
2122 search_bar.update(cx, |_, cx| {
2123 let all_selections =
2124 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2125 assert_eq!(
2126 all_selections.len(),
2127 2,
2128 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2129 );
2130 });
2131
2132 search_bar
2133 .update_in(cx, |search_bar, window, cx| {
2134 search_bar.search(
2135 "edit(",
2136 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2137 window,
2138 cx,
2139 )
2140 })
2141 .await
2142 .unwrap();
2143
2144 search_bar.update_in(cx, |search_bar, window, cx| {
2145 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2146 });
2147 search_bar.update(cx, |_, cx| {
2148 let all_selections =
2149 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2150 assert_eq!(
2151 all_selections.len(),
2152 2,
2153 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2154 );
2155 });
2156 }
2157
2158 #[gpui::test]
2159 async fn test_search_query_history(cx: &mut TestAppContext) {
2160 init_globals(cx);
2161 let buffer_text = r#"
2162 A regular expression (shortened as regex or regexp;[1] also referred to as
2163 rational expression[2][3]) is a sequence of characters that specifies a search
2164 pattern in text. Usually such patterns are used by string-searching algorithms
2165 for "find" or "find and replace" operations on strings, or for input validation.
2166 "#
2167 .unindent();
2168 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2169 let cx = cx.add_empty_window();
2170
2171 let editor =
2172 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2173
2174 let search_bar = cx.new_window_entity(|window, cx| {
2175 let mut search_bar = BufferSearchBar::new(None, window, cx);
2176 search_bar.set_active_pane_item(Some(&editor), window, cx);
2177 search_bar.show(window, cx);
2178 search_bar
2179 });
2180
2181 // Add 3 search items into the history.
2182 search_bar
2183 .update_in(cx, |search_bar, window, cx| {
2184 search_bar.search("a", None, window, cx)
2185 })
2186 .await
2187 .unwrap();
2188 search_bar
2189 .update_in(cx, |search_bar, window, cx| {
2190 search_bar.search("b", None, window, cx)
2191 })
2192 .await
2193 .unwrap();
2194 search_bar
2195 .update_in(cx, |search_bar, window, cx| {
2196 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2197 })
2198 .await
2199 .unwrap();
2200 // Ensure that the latest search is active.
2201 search_bar.update(cx, |search_bar, cx| {
2202 assert_eq!(search_bar.query(cx), "c");
2203 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2204 });
2205
2206 // Next history query after the latest should set the query to the empty string.
2207 search_bar.update_in(cx, |search_bar, window, cx| {
2208 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2209 });
2210 search_bar.update(cx, |search_bar, cx| {
2211 assert_eq!(search_bar.query(cx), "");
2212 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2213 });
2214 search_bar.update_in(cx, |search_bar, window, cx| {
2215 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2216 });
2217 search_bar.update(cx, |search_bar, cx| {
2218 assert_eq!(search_bar.query(cx), "");
2219 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2220 });
2221
2222 // First previous query for empty current query should set the query to the latest.
2223 search_bar.update_in(cx, |search_bar, window, cx| {
2224 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2225 });
2226 search_bar.update(cx, |search_bar, cx| {
2227 assert_eq!(search_bar.query(cx), "c");
2228 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2229 });
2230
2231 // Further previous items should go over the history in reverse order.
2232 search_bar.update_in(cx, |search_bar, window, cx| {
2233 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2234 });
2235 search_bar.update(cx, |search_bar, cx| {
2236 assert_eq!(search_bar.query(cx), "b");
2237 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2238 });
2239
2240 // Previous items should never go behind the first history item.
2241 search_bar.update_in(cx, |search_bar, window, cx| {
2242 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2243 });
2244 search_bar.update(cx, |search_bar, cx| {
2245 assert_eq!(search_bar.query(cx), "a");
2246 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2247 });
2248 search_bar.update_in(cx, |search_bar, window, cx| {
2249 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2250 });
2251 search_bar.update(cx, |search_bar, cx| {
2252 assert_eq!(search_bar.query(cx), "a");
2253 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2254 });
2255
2256 // Next items should go over the history in the original order.
2257 search_bar.update_in(cx, |search_bar, window, cx| {
2258 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2259 });
2260 search_bar.update(cx, |search_bar, cx| {
2261 assert_eq!(search_bar.query(cx), "b");
2262 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2263 });
2264
2265 search_bar
2266 .update_in(cx, |search_bar, window, cx| {
2267 search_bar.search("ba", None, window, cx)
2268 })
2269 .await
2270 .unwrap();
2271 search_bar.update(cx, |search_bar, cx| {
2272 assert_eq!(search_bar.query(cx), "ba");
2273 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2274 });
2275
2276 // New search input should add another entry to history and move the selection to the end of the history.
2277 search_bar.update_in(cx, |search_bar, window, cx| {
2278 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2279 });
2280 search_bar.update(cx, |search_bar, cx| {
2281 assert_eq!(search_bar.query(cx), "c");
2282 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2283 });
2284 search_bar.update_in(cx, |search_bar, window, cx| {
2285 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2286 });
2287 search_bar.update(cx, |search_bar, cx| {
2288 assert_eq!(search_bar.query(cx), "b");
2289 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2290 });
2291 search_bar.update_in(cx, |search_bar, window, cx| {
2292 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2293 });
2294 search_bar.update(cx, |search_bar, cx| {
2295 assert_eq!(search_bar.query(cx), "c");
2296 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2297 });
2298 search_bar.update_in(cx, |search_bar, window, cx| {
2299 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2300 });
2301 search_bar.update(cx, |search_bar, cx| {
2302 assert_eq!(search_bar.query(cx), "ba");
2303 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2304 });
2305 search_bar.update_in(cx, |search_bar, window, cx| {
2306 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2307 });
2308 search_bar.update(cx, |search_bar, cx| {
2309 assert_eq!(search_bar.query(cx), "");
2310 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2311 });
2312 }
2313
2314 #[gpui::test]
2315 async fn test_replace_simple(cx: &mut TestAppContext) {
2316 let (editor, search_bar, cx) = init_test(cx);
2317
2318 search_bar
2319 .update_in(cx, |search_bar, window, cx| {
2320 search_bar.search("expression", None, window, cx)
2321 })
2322 .await
2323 .unwrap();
2324
2325 search_bar.update_in(cx, |search_bar, window, cx| {
2326 search_bar.replacement_editor.update(cx, |editor, cx| {
2327 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2328 editor.set_text("expr$1", window, cx);
2329 });
2330 search_bar.replace_all(&ReplaceAll, window, cx)
2331 });
2332 assert_eq!(
2333 editor.read_with(cx, |this, cx| { this.text(cx) }),
2334 r#"
2335 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2336 rational expr$1[2][3]) is a sequence of characters that specifies a search
2337 pattern in text. Usually such patterns are used by string-searching algorithms
2338 for "find" or "find and replace" operations on strings, or for input validation.
2339 "#
2340 .unindent()
2341 );
2342
2343 // Search for word boundaries and replace just a single one.
2344 search_bar
2345 .update_in(cx, |search_bar, window, cx| {
2346 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2347 })
2348 .await
2349 .unwrap();
2350
2351 search_bar.update_in(cx, |search_bar, window, cx| {
2352 search_bar.replacement_editor.update(cx, |editor, cx| {
2353 editor.set_text("banana", window, cx);
2354 });
2355 search_bar.replace_next(&ReplaceNext, window, cx)
2356 });
2357 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2358 assert_eq!(
2359 editor.read_with(cx, |this, cx| { this.text(cx) }),
2360 r#"
2361 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2362 rational expr$1[2][3]) is a sequence of characters that specifies a search
2363 pattern in text. Usually such patterns are used by string-searching algorithms
2364 for "find" or "find and replace" operations on strings, or for input validation.
2365 "#
2366 .unindent()
2367 );
2368 // Let's turn on regex mode.
2369 search_bar
2370 .update_in(cx, |search_bar, window, cx| {
2371 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2372 })
2373 .await
2374 .unwrap();
2375 search_bar.update_in(cx, |search_bar, window, cx| {
2376 search_bar.replacement_editor.update(cx, |editor, cx| {
2377 editor.set_text("${1}number", window, cx);
2378 });
2379 search_bar.replace_all(&ReplaceAll, window, cx)
2380 });
2381 assert_eq!(
2382 editor.read_with(cx, |this, cx| { this.text(cx) }),
2383 r#"
2384 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2385 rational expr$12number3number) is a sequence of characters that specifies a search
2386 pattern in text. Usually such patterns are used by string-searching algorithms
2387 for "find" or "find and replace" operations on strings, or for input validation.
2388 "#
2389 .unindent()
2390 );
2391 // Now with a whole-word twist.
2392 search_bar
2393 .update_in(cx, |search_bar, window, cx| {
2394 search_bar.search(
2395 "a\\w+s",
2396 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2397 window,
2398 cx,
2399 )
2400 })
2401 .await
2402 .unwrap();
2403 search_bar.update_in(cx, |search_bar, window, cx| {
2404 search_bar.replacement_editor.update(cx, |editor, cx| {
2405 editor.set_text("things", window, cx);
2406 });
2407 search_bar.replace_all(&ReplaceAll, window, cx)
2408 });
2409 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2410 // of words in this text that would match this regex if not for WHOLE_WORD.
2411 assert_eq!(
2412 editor.read_with(cx, |this, cx| { this.text(cx) }),
2413 r#"
2414 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2415 rational expr$12number3number) is a sequence of characters that specifies a search
2416 pattern in text. Usually such patterns are used by string-searching things
2417 for "find" or "find and replace" operations on strings, or for input validation.
2418 "#
2419 .unindent()
2420 );
2421 }
2422
2423 struct ReplacementTestParams<'a> {
2424 editor: &'a Entity<Editor>,
2425 search_bar: &'a Entity<BufferSearchBar>,
2426 cx: &'a mut VisualTestContext,
2427 search_text: &'static str,
2428 search_options: Option<SearchOptions>,
2429 replacement_text: &'static str,
2430 replace_all: bool,
2431 expected_text: String,
2432 }
2433
2434 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2435 options
2436 .search_bar
2437 .update_in(options.cx, |search_bar, window, cx| {
2438 if let Some(options) = options.search_options {
2439 search_bar.set_search_options(options, cx);
2440 }
2441 search_bar.search(options.search_text, options.search_options, window, cx)
2442 })
2443 .await
2444 .unwrap();
2445
2446 options
2447 .search_bar
2448 .update_in(options.cx, |search_bar, window, cx| {
2449 search_bar.replacement_editor.update(cx, |editor, cx| {
2450 editor.set_text(options.replacement_text, window, cx);
2451 });
2452
2453 if options.replace_all {
2454 search_bar.replace_all(&ReplaceAll, window, cx)
2455 } else {
2456 search_bar.replace_next(&ReplaceNext, window, cx)
2457 }
2458 });
2459
2460 assert_eq!(
2461 options
2462 .editor
2463 .read_with(options.cx, |this, cx| { this.text(cx) }),
2464 options.expected_text
2465 );
2466 }
2467
2468 #[gpui::test]
2469 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2470 let (editor, search_bar, cx) = init_test(cx);
2471
2472 run_replacement_test(ReplacementTestParams {
2473 editor: &editor,
2474 search_bar: &search_bar,
2475 cx,
2476 search_text: "expression",
2477 search_options: None,
2478 replacement_text: r"\n",
2479 replace_all: true,
2480 expected_text: r#"
2481 A regular \n (shortened as regex or regexp;[1] also referred to as
2482 rational \n[2][3]) is a sequence of characters that specifies a search
2483 pattern in text. Usually such patterns are used by string-searching algorithms
2484 for "find" or "find and replace" operations on strings, or for input validation.
2485 "#
2486 .unindent(),
2487 })
2488 .await;
2489
2490 run_replacement_test(ReplacementTestParams {
2491 editor: &editor,
2492 search_bar: &search_bar,
2493 cx,
2494 search_text: "or",
2495 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2496 replacement_text: r"\\\n\\\\",
2497 replace_all: false,
2498 expected_text: r#"
2499 A regular \n (shortened as regex \
2500 \\ regexp;[1] also referred to as
2501 rational \n[2][3]) is a sequence of characters that specifies a search
2502 pattern in text. Usually such patterns are used by string-searching algorithms
2503 for "find" or "find and replace" operations on strings, or for input validation.
2504 "#
2505 .unindent(),
2506 })
2507 .await;
2508
2509 run_replacement_test(ReplacementTestParams {
2510 editor: &editor,
2511 search_bar: &search_bar,
2512 cx,
2513 search_text: r"(that|used) ",
2514 search_options: Some(SearchOptions::REGEX),
2515 replacement_text: r"$1\n",
2516 replace_all: true,
2517 expected_text: r#"
2518 A regular \n (shortened as regex \
2519 \\ regexp;[1] also referred to as
2520 rational \n[2][3]) is a sequence of characters that
2521 specifies a search
2522 pattern in text. Usually such patterns are used
2523 by string-searching algorithms
2524 for "find" or "find and replace" operations on strings, or for input validation.
2525 "#
2526 .unindent(),
2527 })
2528 .await;
2529 }
2530
2531 #[gpui::test]
2532 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2533 cx: &mut TestAppContext,
2534 ) {
2535 init_globals(cx);
2536 let buffer = cx.new(|cx| {
2537 Buffer::local(
2538 r#"
2539 aaa bbb aaa ccc
2540 aaa bbb aaa ccc
2541 aaa bbb aaa ccc
2542 aaa bbb aaa ccc
2543 aaa bbb aaa ccc
2544 aaa bbb aaa ccc
2545 "#
2546 .unindent(),
2547 cx,
2548 )
2549 });
2550 let cx = cx.add_empty_window();
2551 let editor =
2552 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2553
2554 let search_bar = cx.new_window_entity(|window, cx| {
2555 let mut search_bar = BufferSearchBar::new(None, window, cx);
2556 search_bar.set_active_pane_item(Some(&editor), window, cx);
2557 search_bar.show(window, cx);
2558 search_bar
2559 });
2560
2561 editor.update_in(cx, |editor, window, cx| {
2562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2563 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2564 })
2565 });
2566
2567 search_bar.update_in(cx, |search_bar, window, cx| {
2568 let deploy = Deploy {
2569 focus: true,
2570 replace_enabled: false,
2571 selection_search_enabled: true,
2572 };
2573 search_bar.deploy(&deploy, window, cx);
2574 });
2575
2576 cx.run_until_parked();
2577
2578 search_bar
2579 .update_in(cx, |search_bar, window, cx| {
2580 search_bar.search("aaa", None, window, cx)
2581 })
2582 .await
2583 .unwrap();
2584
2585 editor.update(cx, |editor, cx| {
2586 assert_eq!(
2587 editor.search_background_highlights(cx),
2588 &[
2589 Point::new(1, 0)..Point::new(1, 3),
2590 Point::new(1, 8)..Point::new(1, 11),
2591 Point::new(2, 0)..Point::new(2, 3),
2592 ]
2593 );
2594 });
2595 }
2596
2597 #[gpui::test]
2598 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2599 cx: &mut TestAppContext,
2600 ) {
2601 init_globals(cx);
2602 let text = r#"
2603 aaa bbb aaa ccc
2604 aaa bbb aaa ccc
2605 aaa bbb aaa ccc
2606 aaa bbb aaa ccc
2607 aaa bbb aaa ccc
2608 aaa bbb aaa ccc
2609
2610 aaa bbb aaa ccc
2611 aaa bbb aaa ccc
2612 aaa bbb aaa ccc
2613 aaa bbb aaa ccc
2614 aaa bbb aaa ccc
2615 aaa bbb aaa ccc
2616 "#
2617 .unindent();
2618
2619 let cx = cx.add_empty_window();
2620 let editor = cx.new_window_entity(|window, cx| {
2621 let multibuffer = MultiBuffer::build_multi(
2622 [
2623 (
2624 &text,
2625 vec![
2626 Point::new(0, 0)..Point::new(2, 0),
2627 Point::new(4, 0)..Point::new(5, 0),
2628 ],
2629 ),
2630 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2631 ],
2632 cx,
2633 );
2634 Editor::for_multibuffer(multibuffer, None, window, cx)
2635 });
2636
2637 let search_bar = cx.new_window_entity(|window, cx| {
2638 let mut search_bar = BufferSearchBar::new(None, window, cx);
2639 search_bar.set_active_pane_item(Some(&editor), window, cx);
2640 search_bar.show(window, cx);
2641 search_bar
2642 });
2643
2644 editor.update_in(cx, |editor, window, cx| {
2645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2646 s.select_ranges(vec![
2647 Point::new(1, 0)..Point::new(1, 4),
2648 Point::new(5, 3)..Point::new(6, 4),
2649 ])
2650 })
2651 });
2652
2653 search_bar.update_in(cx, |search_bar, window, cx| {
2654 let deploy = Deploy {
2655 focus: true,
2656 replace_enabled: false,
2657 selection_search_enabled: true,
2658 };
2659 search_bar.deploy(&deploy, window, cx);
2660 });
2661
2662 cx.run_until_parked();
2663
2664 search_bar
2665 .update_in(cx, |search_bar, window, cx| {
2666 search_bar.search("aaa", None, window, cx)
2667 })
2668 .await
2669 .unwrap();
2670
2671 editor.update(cx, |editor, cx| {
2672 assert_eq!(
2673 editor.search_background_highlights(cx),
2674 &[
2675 Point::new(1, 0)..Point::new(1, 3),
2676 Point::new(5, 8)..Point::new(5, 11),
2677 Point::new(6, 0)..Point::new(6, 3),
2678 ]
2679 );
2680 });
2681 }
2682
2683 #[gpui::test]
2684 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2685 let (editor, search_bar, cx) = init_test(cx);
2686 // Search using valid regexp
2687 search_bar
2688 .update_in(cx, |search_bar, window, cx| {
2689 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2690 search_bar.search("expression", None, window, cx)
2691 })
2692 .await
2693 .unwrap();
2694 editor.update_in(cx, |editor, window, cx| {
2695 assert_eq!(
2696 display_points_of(editor.all_text_background_highlights(window, cx)),
2697 &[
2698 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2699 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2700 ],
2701 );
2702 });
2703
2704 // Now, the expression is invalid
2705 search_bar
2706 .update_in(cx, |search_bar, window, cx| {
2707 search_bar.search("expression (", None, window, cx)
2708 })
2709 .await
2710 .unwrap_err();
2711 editor.update_in(cx, |editor, window, cx| {
2712 assert!(
2713 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2714 );
2715 });
2716 }
2717
2718 #[gpui::test]
2719 async fn test_search_options_changes(cx: &mut TestAppContext) {
2720 let (_editor, search_bar, cx) = init_test(cx);
2721 update_search_settings(
2722 SearchSettings {
2723 button: true,
2724 whole_word: false,
2725 case_sensitive: false,
2726 include_ignored: false,
2727 regex: false,
2728 },
2729 cx,
2730 );
2731
2732 let deploy = Deploy {
2733 focus: true,
2734 replace_enabled: false,
2735 selection_search_enabled: true,
2736 };
2737
2738 search_bar.update_in(cx, |search_bar, window, cx| {
2739 assert_eq!(
2740 search_bar.search_options,
2741 SearchOptions::NONE,
2742 "Should have no search options enabled by default"
2743 );
2744 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2745 assert_eq!(
2746 search_bar.search_options,
2747 SearchOptions::WHOLE_WORD,
2748 "Should enable the option toggled"
2749 );
2750 assert!(
2751 !search_bar.dismissed,
2752 "Search bar should be present and visible"
2753 );
2754 search_bar.deploy(&deploy, window, cx);
2755 assert_eq!(
2756 search_bar.search_options,
2757 SearchOptions::WHOLE_WORD,
2758 "After (re)deploying, the option should still be enabled"
2759 );
2760
2761 search_bar.dismiss(&Dismiss, window, cx);
2762 search_bar.deploy(&deploy, window, cx);
2763 assert_eq!(
2764 search_bar.search_options,
2765 SearchOptions::WHOLE_WORD,
2766 "After hiding and showing the search bar, search options should be preserved"
2767 );
2768
2769 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2770 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2771 assert_eq!(
2772 search_bar.search_options,
2773 SearchOptions::REGEX,
2774 "Should enable the options toggled"
2775 );
2776 assert!(
2777 !search_bar.dismissed,
2778 "Search bar should be present and visible"
2779 );
2780 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2781 });
2782
2783 update_search_settings(
2784 SearchSettings {
2785 button: true,
2786 whole_word: false,
2787 case_sensitive: true,
2788 include_ignored: false,
2789 regex: false,
2790 },
2791 cx,
2792 );
2793 search_bar.update_in(cx, |search_bar, window, cx| {
2794 assert_eq!(
2795 search_bar.search_options,
2796 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2797 "Should have no search options enabled by default"
2798 );
2799
2800 search_bar.deploy(&deploy, window, cx);
2801 assert_eq!(
2802 search_bar.search_options,
2803 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2804 "Toggling a non-dismissed search bar with custom options should not change the default options"
2805 );
2806 search_bar.dismiss(&Dismiss, window, cx);
2807 search_bar.deploy(&deploy, window, cx);
2808 assert_eq!(
2809 search_bar.configured_options,
2810 SearchOptions::CASE_SENSITIVE,
2811 "After a settings update and toggling the search bar, configured options should be updated"
2812 );
2813 assert_eq!(
2814 search_bar.search_options,
2815 SearchOptions::CASE_SENSITIVE,
2816 "After a settings update and toggling the search bar, configured options should be used"
2817 );
2818 });
2819
2820 update_search_settings(
2821 SearchSettings {
2822 button: true,
2823 whole_word: true,
2824 case_sensitive: true,
2825 include_ignored: false,
2826 regex: false,
2827 },
2828 cx,
2829 );
2830
2831 search_bar.update_in(cx, |search_bar, window, cx| {
2832 search_bar.deploy(&deploy, window, cx);
2833 search_bar.dismiss(&Dismiss, window, cx);
2834 search_bar.show(window, cx);
2835 assert_eq!(
2836 search_bar.search_options,
2837 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2838 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2839 );
2840 });
2841 }
2842
2843 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2844 cx.update(|cx| {
2845 SettingsStore::update_global(cx, |store, cx| {
2846 store.update_user_settings::<EditorSettings>(cx, |settings| {
2847 settings.search = Some(search_settings);
2848 });
2849 });
2850 });
2851 }
2852}