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 mut editor = None;
1520 let window = cx.add_window(|window, cx| {
1521 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1522 "keymaps/default-macos.json",
1523 cx,
1524 )
1525 .unwrap();
1526 cx.bind_keys(default_key_bindings);
1527 editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1528 let mut search_bar = BufferSearchBar::new(None, window, cx);
1529 search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1530 search_bar.show(window, cx);
1531 search_bar
1532 });
1533 let search_bar = window.root(cx).unwrap();
1534
1535 let cx = VisualTestContext::from_window(*window, cx).into_mut();
1536
1537 (editor.unwrap(), search_bar, cx)
1538 }
1539
1540 #[gpui::test]
1541 async fn test_search_simple(cx: &mut TestAppContext) {
1542 let (editor, search_bar, cx) = init_test(cx);
1543 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1544 background_highlights
1545 .into_iter()
1546 .map(|(range, _)| range)
1547 .collect::<Vec<_>>()
1548 };
1549 // Search for a string that appears with different casing.
1550 // By default, search is case-insensitive.
1551 search_bar
1552 .update_in(cx, |search_bar, window, cx| {
1553 search_bar.search("us", None, window, cx)
1554 })
1555 .await
1556 .unwrap();
1557 editor.update_in(cx, |editor, window, cx| {
1558 assert_eq!(
1559 display_points_of(editor.all_text_background_highlights(window, cx)),
1560 &[
1561 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1562 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1563 ]
1564 );
1565 });
1566
1567 // Switch to a case sensitive search.
1568 search_bar.update_in(cx, |search_bar, window, cx| {
1569 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1570 });
1571 let mut editor_notifications = cx.notifications(&editor);
1572 editor_notifications.next().await;
1573 editor.update_in(cx, |editor, window, cx| {
1574 assert_eq!(
1575 display_points_of(editor.all_text_background_highlights(window, cx)),
1576 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1577 );
1578 });
1579
1580 // Search for a string that appears both as a whole word and
1581 // within other words. By default, all results are found.
1582 search_bar
1583 .update_in(cx, |search_bar, window, cx| {
1584 search_bar.search("or", None, window, cx)
1585 })
1586 .await
1587 .unwrap();
1588 editor.update_in(cx, |editor, window, cx| {
1589 assert_eq!(
1590 display_points_of(editor.all_text_background_highlights(window, cx)),
1591 &[
1592 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1593 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1594 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1595 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1596 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1597 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1598 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1599 ]
1600 );
1601 });
1602
1603 // Switch to a whole word search.
1604 search_bar.update_in(cx, |search_bar, window, cx| {
1605 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1606 });
1607 let mut editor_notifications = cx.notifications(&editor);
1608 editor_notifications.next().await;
1609 editor.update_in(cx, |editor, window, cx| {
1610 assert_eq!(
1611 display_points_of(editor.all_text_background_highlights(window, cx)),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1614 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1615 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1616 ]
1617 );
1618 });
1619
1620 editor.update_in(cx, |editor, window, cx| {
1621 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1622 s.select_display_ranges([
1623 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1624 ])
1625 });
1626 });
1627 search_bar.update_in(cx, |search_bar, window, cx| {
1628 assert_eq!(search_bar.active_match_index, Some(0));
1629 search_bar.select_next_match(&SelectNextMatch, window, cx);
1630 assert_eq!(
1631 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1632 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1633 );
1634 });
1635 search_bar.read_with(cx, |search_bar, _| {
1636 assert_eq!(search_bar.active_match_index, Some(0));
1637 });
1638
1639 search_bar.update_in(cx, |search_bar, window, cx| {
1640 search_bar.select_next_match(&SelectNextMatch, window, cx);
1641 assert_eq!(
1642 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1643 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1644 );
1645 });
1646 search_bar.read_with(cx, |search_bar, _| {
1647 assert_eq!(search_bar.active_match_index, Some(1));
1648 });
1649
1650 search_bar.update_in(cx, |search_bar, window, cx| {
1651 search_bar.select_next_match(&SelectNextMatch, window, cx);
1652 assert_eq!(
1653 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1654 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1655 );
1656 });
1657 search_bar.read_with(cx, |search_bar, _| {
1658 assert_eq!(search_bar.active_match_index, Some(2));
1659 });
1660
1661 search_bar.update_in(cx, |search_bar, window, cx| {
1662 search_bar.select_next_match(&SelectNextMatch, window, cx);
1663 assert_eq!(
1664 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1665 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1666 );
1667 });
1668 search_bar.read_with(cx, |search_bar, _| {
1669 assert_eq!(search_bar.active_match_index, Some(0));
1670 });
1671
1672 search_bar.update_in(cx, |search_bar, window, cx| {
1673 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1674 assert_eq!(
1675 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1676 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1677 );
1678 });
1679 search_bar.read_with(cx, |search_bar, _| {
1680 assert_eq!(search_bar.active_match_index, Some(2));
1681 });
1682
1683 search_bar.update_in(cx, |search_bar, window, cx| {
1684 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1685 assert_eq!(
1686 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1687 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1688 );
1689 });
1690 search_bar.read_with(cx, |search_bar, _| {
1691 assert_eq!(search_bar.active_match_index, Some(1));
1692 });
1693
1694 search_bar.update_in(cx, |search_bar, window, cx| {
1695 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1696 assert_eq!(
1697 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1698 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1699 );
1700 });
1701 search_bar.read_with(cx, |search_bar, _| {
1702 assert_eq!(search_bar.active_match_index, Some(0));
1703 });
1704
1705 // Park the cursor in between matches and ensure that going to the previous match selects
1706 // the closest match to the left.
1707 editor.update_in(cx, |editor, window, cx| {
1708 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1709 s.select_display_ranges([
1710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1711 ])
1712 });
1713 });
1714 search_bar.update_in(cx, |search_bar, window, cx| {
1715 assert_eq!(search_bar.active_match_index, Some(1));
1716 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1717 assert_eq!(
1718 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1719 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1720 );
1721 });
1722 search_bar.read_with(cx, |search_bar, _| {
1723 assert_eq!(search_bar.active_match_index, Some(0));
1724 });
1725
1726 // Park the cursor in between matches and ensure that going to the next match selects the
1727 // closest match to the right.
1728 editor.update_in(cx, |editor, window, cx| {
1729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1730 s.select_display_ranges([
1731 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1732 ])
1733 });
1734 });
1735 search_bar.update_in(cx, |search_bar, window, cx| {
1736 assert_eq!(search_bar.active_match_index, Some(1));
1737 search_bar.select_next_match(&SelectNextMatch, window, cx);
1738 assert_eq!(
1739 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1740 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1741 );
1742 });
1743 search_bar.read_with(cx, |search_bar, _| {
1744 assert_eq!(search_bar.active_match_index, Some(1));
1745 });
1746
1747 // Park the cursor after the last match and ensure that going to the previous match selects
1748 // the last match.
1749 editor.update_in(cx, |editor, window, cx| {
1750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1751 s.select_display_ranges([
1752 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1753 ])
1754 });
1755 });
1756 search_bar.update_in(cx, |search_bar, window, cx| {
1757 assert_eq!(search_bar.active_match_index, Some(2));
1758 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1759 assert_eq!(
1760 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1761 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1762 );
1763 });
1764 search_bar.read_with(cx, |search_bar, _| {
1765 assert_eq!(search_bar.active_match_index, Some(2));
1766 });
1767
1768 // Park the cursor after the last match and ensure that going to the next match selects the
1769 // first match.
1770 editor.update_in(cx, |editor, window, cx| {
1771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1772 s.select_display_ranges([
1773 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1774 ])
1775 });
1776 });
1777 search_bar.update_in(cx, |search_bar, window, cx| {
1778 assert_eq!(search_bar.active_match_index, Some(2));
1779 search_bar.select_next_match(&SelectNextMatch, window, cx);
1780 assert_eq!(
1781 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1782 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1783 );
1784 });
1785 search_bar.read_with(cx, |search_bar, _| {
1786 assert_eq!(search_bar.active_match_index, Some(0));
1787 });
1788
1789 // Park the cursor before the first match and ensure that going to the previous match
1790 // selects the last match.
1791 editor.update_in(cx, |editor, window, cx| {
1792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1793 s.select_display_ranges([
1794 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1795 ])
1796 });
1797 });
1798 search_bar.update_in(cx, |search_bar, window, cx| {
1799 assert_eq!(search_bar.active_match_index, Some(0));
1800 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1801 assert_eq!(
1802 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1803 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1804 );
1805 });
1806 search_bar.read_with(cx, |search_bar, _| {
1807 assert_eq!(search_bar.active_match_index, Some(2));
1808 });
1809 }
1810
1811 fn display_points_of(
1812 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1813 ) -> Vec<Range<DisplayPoint>> {
1814 background_highlights
1815 .into_iter()
1816 .map(|(range, _)| range)
1817 .collect::<Vec<_>>()
1818 }
1819
1820 #[gpui::test]
1821 async fn test_search_option_handling(cx: &mut TestAppContext) {
1822 let (editor, search_bar, cx) = init_test(cx);
1823
1824 // show with options should make current search case sensitive
1825 search_bar
1826 .update_in(cx, |search_bar, window, cx| {
1827 search_bar.show(window, cx);
1828 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1829 })
1830 .await
1831 .unwrap();
1832 editor.update_in(cx, |editor, window, cx| {
1833 assert_eq!(
1834 display_points_of(editor.all_text_background_highlights(window, cx)),
1835 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1836 );
1837 });
1838
1839 // search_suggested should restore default options
1840 search_bar.update_in(cx, |search_bar, window, cx| {
1841 search_bar.search_suggested(window, cx);
1842 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1843 });
1844
1845 // toggling a search option should update the defaults
1846 search_bar
1847 .update_in(cx, |search_bar, window, cx| {
1848 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1849 })
1850 .await
1851 .unwrap();
1852 search_bar.update_in(cx, |search_bar, window, cx| {
1853 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1854 });
1855 let mut editor_notifications = cx.notifications(&editor);
1856 editor_notifications.next().await;
1857 editor.update_in(cx, |editor, window, cx| {
1858 assert_eq!(
1859 display_points_of(editor.all_text_background_highlights(window, cx)),
1860 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1861 );
1862 });
1863
1864 // defaults should still include whole word
1865 search_bar.update_in(cx, |search_bar, window, cx| {
1866 search_bar.search_suggested(window, cx);
1867 assert_eq!(
1868 search_bar.search_options,
1869 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1870 )
1871 });
1872 }
1873
1874 #[gpui::test]
1875 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1876 init_globals(cx);
1877 let buffer_text = r#"
1878 A regular expression (shortened as regex or regexp;[1] also referred to as
1879 rational expression[2][3]) is a sequence of characters that specifies a search
1880 pattern in text. Usually such patterns are used by string-searching algorithms
1881 for "find" or "find and replace" operations on strings, or for input validation.
1882 "#
1883 .unindent();
1884 let expected_query_matches_count = buffer_text
1885 .chars()
1886 .filter(|c| c.eq_ignore_ascii_case(&'a'))
1887 .count();
1888 assert!(
1889 expected_query_matches_count > 1,
1890 "Should pick a query with multiple results"
1891 );
1892 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1893 let window = cx.add_window(|_, _| gpui::Empty);
1894
1895 let editor = window.build_entity(cx, |window, cx| {
1896 Editor::for_buffer(buffer.clone(), None, window, cx)
1897 });
1898
1899 let search_bar = window.build_entity(cx, |window, cx| {
1900 let mut search_bar = BufferSearchBar::new(None, window, cx);
1901 search_bar.set_active_pane_item(Some(&editor), window, cx);
1902 search_bar.show(window, cx);
1903 search_bar
1904 });
1905
1906 window
1907 .update(cx, |_, window, cx| {
1908 search_bar.update(cx, |search_bar, cx| {
1909 search_bar.search("a", None, window, cx)
1910 })
1911 })
1912 .unwrap()
1913 .await
1914 .unwrap();
1915 let initial_selections = window
1916 .update(cx, |_, window, cx| {
1917 search_bar.update(cx, |search_bar, cx| {
1918 let handle = search_bar.query_editor.focus_handle(cx);
1919 window.focus(&handle);
1920 search_bar.activate_current_match(window, cx);
1921 });
1922 assert!(
1923 !editor.read(cx).is_focused(window),
1924 "Initially, the editor should not be focused"
1925 );
1926 let initial_selections = editor.update(cx, |editor, cx| {
1927 let initial_selections = editor.selections.display_ranges(cx);
1928 assert_eq!(
1929 initial_selections.len(), 1,
1930 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1931 );
1932 initial_selections
1933 });
1934 search_bar.update(cx, |search_bar, cx| {
1935 assert_eq!(search_bar.active_match_index, Some(0));
1936 let handle = search_bar.query_editor.focus_handle(cx);
1937 window.focus(&handle);
1938 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1939 });
1940 assert!(
1941 editor.read(cx).is_focused(window),
1942 "Should focus editor after successful SelectAllMatches"
1943 );
1944 search_bar.update(cx, |search_bar, cx| {
1945 let all_selections =
1946 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1947 assert_eq!(
1948 all_selections.len(),
1949 expected_query_matches_count,
1950 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1951 );
1952 assert_eq!(
1953 search_bar.active_match_index,
1954 Some(0),
1955 "Match index should not change after selecting all matches"
1956 );
1957 });
1958
1959 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1960 initial_selections
1961 }).unwrap();
1962
1963 window
1964 .update(cx, |_, window, cx| {
1965 assert!(
1966 editor.read(cx).is_focused(window),
1967 "Should still have editor focused after SelectNextMatch"
1968 );
1969 search_bar.update(cx, |search_bar, cx| {
1970 let all_selections =
1971 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1972 assert_eq!(
1973 all_selections.len(),
1974 1,
1975 "On next match, should deselect items and select the next match"
1976 );
1977 assert_ne!(
1978 all_selections, initial_selections,
1979 "Next match should be different from the first selection"
1980 );
1981 assert_eq!(
1982 search_bar.active_match_index,
1983 Some(1),
1984 "Match index should be updated to the next one"
1985 );
1986 let handle = search_bar.query_editor.focus_handle(cx);
1987 window.focus(&handle);
1988 search_bar.select_all_matches(&SelectAllMatches, window, cx);
1989 });
1990 })
1991 .unwrap();
1992 window
1993 .update(cx, |_, window, cx| {
1994 assert!(
1995 editor.read(cx).is_focused(window),
1996 "Should focus editor after successful SelectAllMatches"
1997 );
1998 search_bar.update(cx, |search_bar, cx| {
1999 let all_selections =
2000 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2001 assert_eq!(
2002 all_selections.len(),
2003 expected_query_matches_count,
2004 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2005 );
2006 assert_eq!(
2007 search_bar.active_match_index,
2008 Some(1),
2009 "Match index should not change after selecting all matches"
2010 );
2011 });
2012 search_bar.update(cx, |search_bar, cx| {
2013 search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2014 });
2015 })
2016 .unwrap();
2017 let last_match_selections = window
2018 .update(cx, |_, window, cx| {
2019 assert!(
2020 editor.read(cx).is_focused(window),
2021 "Should still have editor focused after SelectPreviousMatch"
2022 );
2023
2024 search_bar.update(cx, |search_bar, cx| {
2025 let all_selections =
2026 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2027 assert_eq!(
2028 all_selections.len(),
2029 1,
2030 "On previous match, should deselect items and select the previous item"
2031 );
2032 assert_eq!(
2033 all_selections, initial_selections,
2034 "Previous match should be the same as the first selection"
2035 );
2036 assert_eq!(
2037 search_bar.active_match_index,
2038 Some(0),
2039 "Match index should be updated to the previous one"
2040 );
2041 all_selections
2042 })
2043 })
2044 .unwrap();
2045
2046 window
2047 .update(cx, |_, window, cx| {
2048 search_bar.update(cx, |search_bar, cx| {
2049 let handle = search_bar.query_editor.focus_handle(cx);
2050 window.focus(&handle);
2051 search_bar.search("abas_nonexistent_match", None, window, cx)
2052 })
2053 })
2054 .unwrap()
2055 .await
2056 .unwrap();
2057 window
2058 .update(cx, |_, window, cx| {
2059 search_bar.update(cx, |search_bar, cx| {
2060 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2061 });
2062 assert!(
2063 editor.update(cx, |this, _cx| !this.is_focused(window)),
2064 "Should not switch focus to editor if SelectAllMatches does not find any matches"
2065 );
2066 search_bar.update(cx, |search_bar, cx| {
2067 let all_selections =
2068 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2069 assert_eq!(
2070 all_selections, last_match_selections,
2071 "Should not select anything new if there are no matches"
2072 );
2073 assert!(
2074 search_bar.active_match_index.is_none(),
2075 "For no matches, there should be no active match index"
2076 );
2077 });
2078 })
2079 .unwrap();
2080 }
2081
2082 #[gpui::test]
2083 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2084 init_globals(cx);
2085 let buffer_text = r#"
2086 self.buffer.update(cx, |buffer, cx| {
2087 buffer.edit(
2088 edits,
2089 Some(AutoindentMode::Block {
2090 original_indent_columns,
2091 }),
2092 cx,
2093 )
2094 });
2095
2096 this.buffer.update(cx, |buffer, cx| {
2097 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2098 });
2099 "#
2100 .unindent();
2101 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2102 let cx = cx.add_empty_window();
2103
2104 let editor =
2105 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2106
2107 let search_bar = cx.new_window_entity(|window, cx| {
2108 let mut search_bar = BufferSearchBar::new(None, window, cx);
2109 search_bar.set_active_pane_item(Some(&editor), window, cx);
2110 search_bar.show(window, cx);
2111 search_bar
2112 });
2113
2114 search_bar
2115 .update_in(cx, |search_bar, window, cx| {
2116 search_bar.search(
2117 "edit\\(",
2118 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2119 window,
2120 cx,
2121 )
2122 })
2123 .await
2124 .unwrap();
2125
2126 search_bar.update_in(cx, |search_bar, window, cx| {
2127 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2128 });
2129 search_bar.update(cx, |_, cx| {
2130 let all_selections =
2131 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2132 assert_eq!(
2133 all_selections.len(),
2134 2,
2135 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2136 );
2137 });
2138
2139 search_bar
2140 .update_in(cx, |search_bar, window, cx| {
2141 search_bar.search(
2142 "edit(",
2143 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2144 window,
2145 cx,
2146 )
2147 })
2148 .await
2149 .unwrap();
2150
2151 search_bar.update_in(cx, |search_bar, window, cx| {
2152 search_bar.select_all_matches(&SelectAllMatches, window, cx);
2153 });
2154 search_bar.update(cx, |_, cx| {
2155 let all_selections =
2156 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2157 assert_eq!(
2158 all_selections.len(),
2159 2,
2160 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2161 );
2162 });
2163 }
2164
2165 #[gpui::test]
2166 async fn test_search_query_history(cx: &mut TestAppContext) {
2167 init_globals(cx);
2168 let buffer_text = r#"
2169 A regular expression (shortened as regex or regexp;[1] also referred to as
2170 rational expression[2][3]) is a sequence of characters that specifies a search
2171 pattern in text. Usually such patterns are used by string-searching algorithms
2172 for "find" or "find and replace" operations on strings, or for input validation.
2173 "#
2174 .unindent();
2175 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2176 let cx = cx.add_empty_window();
2177
2178 let editor =
2179 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2180
2181 let search_bar = cx.new_window_entity(|window, cx| {
2182 let mut search_bar = BufferSearchBar::new(None, window, cx);
2183 search_bar.set_active_pane_item(Some(&editor), window, cx);
2184 search_bar.show(window, cx);
2185 search_bar
2186 });
2187
2188 // Add 3 search items into the history.
2189 search_bar
2190 .update_in(cx, |search_bar, window, cx| {
2191 search_bar.search("a", None, window, cx)
2192 })
2193 .await
2194 .unwrap();
2195 search_bar
2196 .update_in(cx, |search_bar, window, cx| {
2197 search_bar.search("b", None, window, cx)
2198 })
2199 .await
2200 .unwrap();
2201 search_bar
2202 .update_in(cx, |search_bar, window, cx| {
2203 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2204 })
2205 .await
2206 .unwrap();
2207 // Ensure that the latest search is active.
2208 search_bar.update(cx, |search_bar, cx| {
2209 assert_eq!(search_bar.query(cx), "c");
2210 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2211 });
2212
2213 // Next history query after the latest should set the query to the empty string.
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 search_bar.update_in(cx, |search_bar, window, cx| {
2222 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2223 });
2224 search_bar.update(cx, |search_bar, cx| {
2225 assert_eq!(search_bar.query(cx), "");
2226 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2227 });
2228
2229 // First previous query for empty current query should set the query to the latest.
2230 search_bar.update_in(cx, |search_bar, window, cx| {
2231 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2232 });
2233 search_bar.update(cx, |search_bar, cx| {
2234 assert_eq!(search_bar.query(cx), "c");
2235 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2236 });
2237
2238 // Further previous items should go over the history in reverse order.
2239 search_bar.update_in(cx, |search_bar, window, cx| {
2240 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2241 });
2242 search_bar.update(cx, |search_bar, cx| {
2243 assert_eq!(search_bar.query(cx), "b");
2244 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2245 });
2246
2247 // Previous items should never go behind the first history item.
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 search_bar.update_in(cx, |search_bar, window, cx| {
2256 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2257 });
2258 search_bar.update(cx, |search_bar, cx| {
2259 assert_eq!(search_bar.query(cx), "a");
2260 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2261 });
2262
2263 // Next items should go over the history in the original order.
2264 search_bar.update_in(cx, |search_bar, window, cx| {
2265 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2266 });
2267 search_bar.update(cx, |search_bar, cx| {
2268 assert_eq!(search_bar.query(cx), "b");
2269 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2270 });
2271
2272 search_bar
2273 .update_in(cx, |search_bar, window, cx| {
2274 search_bar.search("ba", None, window, cx)
2275 })
2276 .await
2277 .unwrap();
2278 search_bar.update(cx, |search_bar, cx| {
2279 assert_eq!(search_bar.query(cx), "ba");
2280 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2281 });
2282
2283 // New search input should add another entry to history and move the selection to the end of the history.
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), "c");
2289 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2290 });
2291 search_bar.update_in(cx, |search_bar, window, cx| {
2292 search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2293 });
2294 search_bar.update(cx, |search_bar, cx| {
2295 assert_eq!(search_bar.query(cx), "b");
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), "c");
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), "ba");
2310 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2311 });
2312 search_bar.update_in(cx, |search_bar, window, cx| {
2313 search_bar.next_history_query(&NextHistoryQuery, window, cx);
2314 });
2315 search_bar.update(cx, |search_bar, cx| {
2316 assert_eq!(search_bar.query(cx), "");
2317 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2318 });
2319 }
2320
2321 #[gpui::test]
2322 async fn test_replace_simple(cx: &mut TestAppContext) {
2323 let (editor, search_bar, cx) = init_test(cx);
2324
2325 search_bar
2326 .update_in(cx, |search_bar, window, cx| {
2327 search_bar.search("expression", None, window, cx)
2328 })
2329 .await
2330 .unwrap();
2331
2332 search_bar.update_in(cx, |search_bar, window, cx| {
2333 search_bar.replacement_editor.update(cx, |editor, cx| {
2334 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2335 editor.set_text("expr$1", window, cx);
2336 });
2337 search_bar.replace_all(&ReplaceAll, window, cx)
2338 });
2339 assert_eq!(
2340 editor.read_with(cx, |this, cx| { this.text(cx) }),
2341 r#"
2342 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2343 rational expr$1[2][3]) is a sequence of characters that specifies a search
2344 pattern in text. Usually such patterns are used by string-searching algorithms
2345 for "find" or "find and replace" operations on strings, or for input validation.
2346 "#
2347 .unindent()
2348 );
2349
2350 // Search for word boundaries and replace just a single one.
2351 search_bar
2352 .update_in(cx, |search_bar, window, cx| {
2353 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2354 })
2355 .await
2356 .unwrap();
2357
2358 search_bar.update_in(cx, |search_bar, window, cx| {
2359 search_bar.replacement_editor.update(cx, |editor, cx| {
2360 editor.set_text("banana", window, cx);
2361 });
2362 search_bar.replace_next(&ReplaceNext, window, cx)
2363 });
2364 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2365 assert_eq!(
2366 editor.read_with(cx, |this, cx| { this.text(cx) }),
2367 r#"
2368 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2369 rational expr$1[2][3]) is a sequence of characters that specifies a search
2370 pattern in text. Usually such patterns are used by string-searching algorithms
2371 for "find" or "find and replace" operations on strings, or for input validation.
2372 "#
2373 .unindent()
2374 );
2375 // Let's turn on regex mode.
2376 search_bar
2377 .update_in(cx, |search_bar, window, cx| {
2378 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2379 })
2380 .await
2381 .unwrap();
2382 search_bar.update_in(cx, |search_bar, window, cx| {
2383 search_bar.replacement_editor.update(cx, |editor, cx| {
2384 editor.set_text("${1}number", window, cx);
2385 });
2386 search_bar.replace_all(&ReplaceAll, window, cx)
2387 });
2388 assert_eq!(
2389 editor.read_with(cx, |this, cx| { this.text(cx) }),
2390 r#"
2391 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2392 rational expr$12number3number) is a sequence of characters that specifies a search
2393 pattern in text. Usually such patterns are used by string-searching algorithms
2394 for "find" or "find and replace" operations on strings, or for input validation.
2395 "#
2396 .unindent()
2397 );
2398 // Now with a whole-word twist.
2399 search_bar
2400 .update_in(cx, |search_bar, window, cx| {
2401 search_bar.search(
2402 "a\\w+s",
2403 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2404 window,
2405 cx,
2406 )
2407 })
2408 .await
2409 .unwrap();
2410 search_bar.update_in(cx, |search_bar, window, cx| {
2411 search_bar.replacement_editor.update(cx, |editor, cx| {
2412 editor.set_text("things", window, cx);
2413 });
2414 search_bar.replace_all(&ReplaceAll, window, cx)
2415 });
2416 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2417 // of words in this text that would match this regex if not for WHOLE_WORD.
2418 assert_eq!(
2419 editor.read_with(cx, |this, cx| { this.text(cx) }),
2420 r#"
2421 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2422 rational expr$12number3number) is a sequence of characters that specifies a search
2423 pattern in text. Usually such patterns are used by string-searching things
2424 for "find" or "find and replace" operations on strings, or for input validation.
2425 "#
2426 .unindent()
2427 );
2428 }
2429
2430 struct ReplacementTestParams<'a> {
2431 editor: &'a Entity<Editor>,
2432 search_bar: &'a Entity<BufferSearchBar>,
2433 cx: &'a mut VisualTestContext,
2434 search_text: &'static str,
2435 search_options: Option<SearchOptions>,
2436 replacement_text: &'static str,
2437 replace_all: bool,
2438 expected_text: String,
2439 }
2440
2441 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2442 options
2443 .search_bar
2444 .update_in(options.cx, |search_bar, window, cx| {
2445 if let Some(options) = options.search_options {
2446 search_bar.set_search_options(options, cx);
2447 }
2448 search_bar.search(options.search_text, options.search_options, window, cx)
2449 })
2450 .await
2451 .unwrap();
2452
2453 options
2454 .search_bar
2455 .update_in(options.cx, |search_bar, window, cx| {
2456 search_bar.replacement_editor.update(cx, |editor, cx| {
2457 editor.set_text(options.replacement_text, window, cx);
2458 });
2459
2460 if options.replace_all {
2461 search_bar.replace_all(&ReplaceAll, window, cx)
2462 } else {
2463 search_bar.replace_next(&ReplaceNext, window, cx)
2464 }
2465 });
2466
2467 assert_eq!(
2468 options
2469 .editor
2470 .read_with(options.cx, |this, cx| { this.text(cx) }),
2471 options.expected_text
2472 );
2473 }
2474
2475 #[gpui::test]
2476 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2477 let (editor, search_bar, cx) = init_test(cx);
2478
2479 run_replacement_test(ReplacementTestParams {
2480 editor: &editor,
2481 search_bar: &search_bar,
2482 cx,
2483 search_text: "expression",
2484 search_options: None,
2485 replacement_text: r"\n",
2486 replace_all: true,
2487 expected_text: r#"
2488 A regular \n (shortened as regex or regexp;[1] also referred to as
2489 rational \n[2][3]) is a sequence of characters that specifies a search
2490 pattern in text. Usually such patterns are used by string-searching algorithms
2491 for "find" or "find and replace" operations on strings, or for input validation.
2492 "#
2493 .unindent(),
2494 })
2495 .await;
2496
2497 run_replacement_test(ReplacementTestParams {
2498 editor: &editor,
2499 search_bar: &search_bar,
2500 cx,
2501 search_text: "or",
2502 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2503 replacement_text: r"\\\n\\\\",
2504 replace_all: false,
2505 expected_text: r#"
2506 A regular \n (shortened as regex \
2507 \\ regexp;[1] also referred to as
2508 rational \n[2][3]) is a sequence of characters that specifies a search
2509 pattern in text. Usually such patterns are used by string-searching algorithms
2510 for "find" or "find and replace" operations on strings, or for input validation.
2511 "#
2512 .unindent(),
2513 })
2514 .await;
2515
2516 run_replacement_test(ReplacementTestParams {
2517 editor: &editor,
2518 search_bar: &search_bar,
2519 cx,
2520 search_text: r"(that|used) ",
2521 search_options: Some(SearchOptions::REGEX),
2522 replacement_text: r"$1\n",
2523 replace_all: true,
2524 expected_text: r#"
2525 A regular \n (shortened as regex \
2526 \\ regexp;[1] also referred to as
2527 rational \n[2][3]) is a sequence of characters that
2528 specifies a search
2529 pattern in text. Usually such patterns are used
2530 by string-searching algorithms
2531 for "find" or "find and replace" operations on strings, or for input validation.
2532 "#
2533 .unindent(),
2534 })
2535 .await;
2536 }
2537
2538 #[gpui::test]
2539 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2540 cx: &mut TestAppContext,
2541 ) {
2542 init_globals(cx);
2543 let buffer = cx.new(|cx| {
2544 Buffer::local(
2545 r#"
2546 aaa bbb aaa ccc
2547 aaa bbb aaa ccc
2548 aaa bbb aaa ccc
2549 aaa bbb aaa ccc
2550 aaa bbb aaa ccc
2551 aaa bbb aaa ccc
2552 "#
2553 .unindent(),
2554 cx,
2555 )
2556 });
2557 let cx = cx.add_empty_window();
2558 let editor =
2559 cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2560
2561 let search_bar = cx.new_window_entity(|window, cx| {
2562 let mut search_bar = BufferSearchBar::new(None, window, cx);
2563 search_bar.set_active_pane_item(Some(&editor), window, cx);
2564 search_bar.show(window, cx);
2565 search_bar
2566 });
2567
2568 editor.update_in(cx, |editor, window, cx| {
2569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2570 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2571 })
2572 });
2573
2574 search_bar.update_in(cx, |search_bar, window, cx| {
2575 let deploy = Deploy {
2576 focus: true,
2577 replace_enabled: false,
2578 selection_search_enabled: true,
2579 };
2580 search_bar.deploy(&deploy, window, cx);
2581 });
2582
2583 cx.run_until_parked();
2584
2585 search_bar
2586 .update_in(cx, |search_bar, window, cx| {
2587 search_bar.search("aaa", None, window, cx)
2588 })
2589 .await
2590 .unwrap();
2591
2592 editor.update(cx, |editor, cx| {
2593 assert_eq!(
2594 editor.search_background_highlights(cx),
2595 &[
2596 Point::new(1, 0)..Point::new(1, 3),
2597 Point::new(1, 8)..Point::new(1, 11),
2598 Point::new(2, 0)..Point::new(2, 3),
2599 ]
2600 );
2601 });
2602 }
2603
2604 #[gpui::test]
2605 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2606 cx: &mut TestAppContext,
2607 ) {
2608 init_globals(cx);
2609 let text = r#"
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 aaa bbb aaa ccc
2618 aaa bbb aaa ccc
2619 aaa bbb aaa ccc
2620 aaa bbb aaa ccc
2621 aaa bbb aaa ccc
2622 aaa bbb aaa ccc
2623 "#
2624 .unindent();
2625
2626 let cx = cx.add_empty_window();
2627 let editor = cx.new_window_entity(|window, cx| {
2628 let multibuffer = MultiBuffer::build_multi(
2629 [
2630 (
2631 &text,
2632 vec![
2633 Point::new(0, 0)..Point::new(2, 0),
2634 Point::new(4, 0)..Point::new(5, 0),
2635 ],
2636 ),
2637 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2638 ],
2639 cx,
2640 );
2641 Editor::for_multibuffer(multibuffer, None, window, cx)
2642 });
2643
2644 let search_bar = cx.new_window_entity(|window, cx| {
2645 let mut search_bar = BufferSearchBar::new(None, window, cx);
2646 search_bar.set_active_pane_item(Some(&editor), window, cx);
2647 search_bar.show(window, cx);
2648 search_bar
2649 });
2650
2651 editor.update_in(cx, |editor, window, cx| {
2652 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2653 s.select_ranges(vec![
2654 Point::new(1, 0)..Point::new(1, 4),
2655 Point::new(5, 3)..Point::new(6, 4),
2656 ])
2657 })
2658 });
2659
2660 search_bar.update_in(cx, |search_bar, window, cx| {
2661 let deploy = Deploy {
2662 focus: true,
2663 replace_enabled: false,
2664 selection_search_enabled: true,
2665 };
2666 search_bar.deploy(&deploy, window, cx);
2667 });
2668
2669 cx.run_until_parked();
2670
2671 search_bar
2672 .update_in(cx, |search_bar, window, cx| {
2673 search_bar.search("aaa", None, window, cx)
2674 })
2675 .await
2676 .unwrap();
2677
2678 editor.update(cx, |editor, cx| {
2679 assert_eq!(
2680 editor.search_background_highlights(cx),
2681 &[
2682 Point::new(1, 0)..Point::new(1, 3),
2683 Point::new(5, 8)..Point::new(5, 11),
2684 Point::new(6, 0)..Point::new(6, 3),
2685 ]
2686 );
2687 });
2688 }
2689
2690 #[gpui::test]
2691 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2692 let (editor, search_bar, cx) = init_test(cx);
2693 // Search using valid regexp
2694 search_bar
2695 .update_in(cx, |search_bar, window, cx| {
2696 search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2697 search_bar.search("expression", None, window, cx)
2698 })
2699 .await
2700 .unwrap();
2701 editor.update_in(cx, |editor, window, cx| {
2702 assert_eq!(
2703 display_points_of(editor.all_text_background_highlights(window, cx)),
2704 &[
2705 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2706 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2707 ],
2708 );
2709 });
2710
2711 // Now, the expression is invalid
2712 search_bar
2713 .update_in(cx, |search_bar, window, cx| {
2714 search_bar.search("expression (", None, window, cx)
2715 })
2716 .await
2717 .unwrap_err();
2718 editor.update_in(cx, |editor, window, cx| {
2719 assert!(
2720 display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2721 );
2722 });
2723 }
2724
2725 #[gpui::test]
2726 async fn test_search_options_changes(cx: &mut TestAppContext) {
2727 let (_editor, search_bar, cx) = init_test(cx);
2728 update_search_settings(
2729 SearchSettings {
2730 button: true,
2731 whole_word: false,
2732 case_sensitive: false,
2733 include_ignored: false,
2734 regex: false,
2735 },
2736 cx,
2737 );
2738
2739 let deploy = Deploy {
2740 focus: true,
2741 replace_enabled: false,
2742 selection_search_enabled: true,
2743 };
2744
2745 search_bar.update_in(cx, |search_bar, window, cx| {
2746 assert_eq!(
2747 search_bar.search_options,
2748 SearchOptions::NONE,
2749 "Should have no search options enabled by default"
2750 );
2751 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2752 assert_eq!(
2753 search_bar.search_options,
2754 SearchOptions::WHOLE_WORD,
2755 "Should enable the option toggled"
2756 );
2757 assert!(
2758 !search_bar.dismissed,
2759 "Search bar should be present and visible"
2760 );
2761 search_bar.deploy(&deploy, window, cx);
2762 assert_eq!(
2763 search_bar.search_options,
2764 SearchOptions::WHOLE_WORD,
2765 "After (re)deploying, the option should still be enabled"
2766 );
2767
2768 search_bar.dismiss(&Dismiss, window, cx);
2769 search_bar.deploy(&deploy, window, cx);
2770 assert_eq!(
2771 search_bar.search_options,
2772 SearchOptions::WHOLE_WORD,
2773 "After hiding and showing the search bar, search options should be preserved"
2774 );
2775
2776 search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2777 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2778 assert_eq!(
2779 search_bar.search_options,
2780 SearchOptions::REGEX,
2781 "Should enable the options toggled"
2782 );
2783 assert!(
2784 !search_bar.dismissed,
2785 "Search bar should be present and visible"
2786 );
2787 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2788 });
2789
2790 update_search_settings(
2791 SearchSettings {
2792 button: true,
2793 whole_word: false,
2794 case_sensitive: true,
2795 include_ignored: false,
2796 regex: false,
2797 },
2798 cx,
2799 );
2800 search_bar.update_in(cx, |search_bar, window, cx| {
2801 assert_eq!(
2802 search_bar.search_options,
2803 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2804 "Should have no search options enabled by default"
2805 );
2806
2807 search_bar.deploy(&deploy, window, cx);
2808 assert_eq!(
2809 search_bar.search_options,
2810 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2811 "Toggling a non-dismissed search bar with custom options should not change the default options"
2812 );
2813 search_bar.dismiss(&Dismiss, window, cx);
2814 search_bar.deploy(&deploy, window, cx);
2815 assert_eq!(
2816 search_bar.configured_options,
2817 SearchOptions::CASE_SENSITIVE,
2818 "After a settings update and toggling the search bar, configured options should be updated"
2819 );
2820 assert_eq!(
2821 search_bar.search_options,
2822 SearchOptions::CASE_SENSITIVE,
2823 "After a settings update and toggling the search bar, configured options should be used"
2824 );
2825 });
2826
2827 update_search_settings(
2828 SearchSettings {
2829 button: true,
2830 whole_word: true,
2831 case_sensitive: true,
2832 include_ignored: false,
2833 regex: false,
2834 },
2835 cx,
2836 );
2837
2838 search_bar.update_in(cx, |search_bar, window, cx| {
2839 search_bar.deploy(&deploy, window, cx);
2840 search_bar.dismiss(&Dismiss, window, cx);
2841 search_bar.show(window, cx);
2842 assert_eq!(
2843 search_bar.search_options,
2844 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2845 "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2846 );
2847 });
2848 }
2849
2850 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2851 cx.update(|cx| {
2852 SettingsStore::update_global(cx, |store, cx| {
2853 store.update_user_settings::<EditorSettings>(cx, |settings| {
2854 settings.search = Some(search_settings);
2855 });
2856 });
2857 });
2858 }
2859}