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