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