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