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