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