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