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