1use bitflags::bitflags;
2pub use buffer_search::BufferSearchBar;
3use gpui::{actions, Action, AppContext, IntoElement};
4use project::search::SearchQuery;
5pub use project_search::ProjectSearchView;
6use ui::{prelude::*, Tooltip};
7use ui::{ButtonStyle, IconButton};
8use workspace::notifications::NotificationId;
9use workspace::{Toast, Workspace};
10
11pub mod buffer_search;
12pub mod project_search;
13pub(crate) mod search_bar;
14
15pub fn init(cx: &mut AppContext) {
16 menu::init();
17 buffer_search::init(cx);
18 project_search::init(cx);
19}
20
21actions!(
22 search,
23 [
24 FocusSearch,
25 ToggleWholeWord,
26 ToggleCaseSensitive,
27 ToggleIncludeIgnored,
28 ToggleRegex,
29 ToggleReplace,
30 ToggleSelection,
31 SelectNextMatch,
32 SelectPrevMatch,
33 SelectAllMatches,
34 NextHistoryQuery,
35 PreviousHistoryQuery,
36 ReplaceAll,
37 ReplaceNext,
38 ]
39);
40
41bitflags! {
42 #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
43 pub struct SearchOptions: u8 {
44 const NONE = 0b000;
45 const WHOLE_WORD = 0b001;
46 const CASE_SENSITIVE = 0b010;
47 const INCLUDE_IGNORED = 0b100;
48 const REGEX = 0b1000;
49 }
50}
51
52impl SearchOptions {
53 pub fn label(&self) -> &'static str {
54 match *self {
55 SearchOptions::WHOLE_WORD => "Match whole words",
56 SearchOptions::CASE_SENSITIVE => "Match case sensitively",
57 SearchOptions::INCLUDE_IGNORED => "Also search files ignored by configuration",
58 SearchOptions::REGEX => "Use regular expressions",
59 _ => panic!("{:?} is not a named SearchOption", self),
60 }
61 }
62
63 pub fn icon(&self) -> ui::IconName {
64 match *self {
65 SearchOptions::WHOLE_WORD => ui::IconName::WholeWord,
66 SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive,
67 SearchOptions::INCLUDE_IGNORED => ui::IconName::Sliders,
68 SearchOptions::REGEX => ui::IconName::Regex,
69 _ => panic!("{:?} is not a named SearchOption", self),
70 }
71 }
72
73 pub fn to_toggle_action(&self) -> Box<dyn Action + Sync + Send + 'static> {
74 match *self {
75 SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
76 SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
77 SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
78 SearchOptions::REGEX => Box::new(ToggleRegex),
79 _ => panic!("{:?} is not a named SearchOption", self),
80 }
81 }
82
83 pub fn none() -> SearchOptions {
84 SearchOptions::NONE
85 }
86
87 pub fn from_query(query: &SearchQuery) -> SearchOptions {
88 let mut options = SearchOptions::NONE;
89 options.set(SearchOptions::WHOLE_WORD, query.whole_word());
90 options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
91 options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
92 options.set(SearchOptions::REGEX, query.is_regex());
93 options
94 }
95
96 pub fn as_button(
97 &self,
98 active: bool,
99 action: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
100 ) -> impl IntoElement {
101 IconButton::new(self.label(), self.icon())
102 .on_click(action)
103 .style(ButtonStyle::Subtle)
104 .selected(active)
105 .tooltip({
106 let action = self.to_toggle_action();
107 let label = self.label();
108 move |cx| Tooltip::for_action(label, &*action, cx)
109 })
110 }
111}
112
113pub(crate) fn show_no_more_matches(cx: &mut WindowContext) {
114 cx.defer(|cx| {
115 struct NotifType();
116 let notification_id = NotificationId::unique::<NotifType>();
117 let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
118 return;
119 };
120 workspace
121 .update(cx, |workspace, cx| {
122 workspace.show_toast(
123 Toast::new(notification_id.clone(), "No more matches").autohide(),
124 cx,
125 );
126 })
127 .ok();
128 });
129}