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