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