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