1use bitflags::bitflags;
2pub use buffer_search::BufferSearchBar;
3use editor::SearchSettings;
4use gpui::{Action, App, ClickEvent, FocusHandle, IntoElement, actions};
5use project::search::SearchQuery;
6pub use project_search::ProjectSearchView;
7use ui::{ButtonStyle, IconButton, IconButtonShape};
8use ui::{Tooltip, prelude::*};
9use workspace::notifications::NotificationId;
10use workspace::{Toast, Workspace};
11
12pub use search_status_button::SEARCH_ICON;
13
14use crate::project_search::ProjectSearchBar;
15
16pub mod buffer_search;
17pub mod project_search;
18pub(crate) mod search_bar;
19pub mod search_status_button;
20
21pub fn init(cx: &mut App) {
22 menu::init();
23 buffer_search::init(cx);
24 project_search::init(cx);
25}
26
27actions!(
28 search,
29 [
30 /// Focuses on the search input field.
31 FocusSearch,
32 /// Toggles whole word matching.
33 ToggleWholeWord,
34 /// Toggles case-sensitive search.
35 ToggleCaseSensitive,
36 /// Toggles searching in ignored files.
37 ToggleIncludeIgnored,
38 /// Toggles regular expression mode.
39 ToggleRegex,
40 /// Toggles the replace interface.
41 ToggleReplace,
42 /// Toggles searching within selection only.
43 ToggleSelection,
44 /// Selects the next search match.
45 SelectNextMatch,
46 /// Selects the previous search match.
47 SelectPreviousMatch,
48 /// Selects all search matches.
49 SelectAllMatches,
50 /// Cycles through search modes.
51 CycleMode,
52 /// Navigates to the next query in search history.
53 NextHistoryQuery,
54 /// Navigates to the previous query in search history.
55 PreviousHistoryQuery,
56 /// Replaces all matches.
57 ReplaceAll,
58 /// Replaces the next match.
59 ReplaceNext,
60 ]
61);
62
63bitflags! {
64 #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
65 pub struct SearchOptions: u8 {
66 const NONE = 0;
67 const WHOLE_WORD = 1 << SearchOption::WholeWord as u8;
68 const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8;
69 const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8;
70 const REGEX = 1 << SearchOption::Regex as u8;
71 const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8;
72 /// If set, reverse direction when finding the active match
73 const BACKWARDS = 1 << SearchOption::Backwards as u8;
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[repr(u8)]
79pub enum SearchOption {
80 WholeWord = 0,
81 CaseSensitive,
82 IncludeIgnored,
83 Regex,
84 OneMatchPerLine,
85 Backwards,
86}
87
88pub(crate) enum SearchSource<'a, 'b> {
89 Buffer,
90 Project(&'a Context<'b, ProjectSearchBar>),
91}
92
93impl SearchOption {
94 pub fn as_options(&self) -> SearchOptions {
95 SearchOptions::from_bits(1 << *self as u8).unwrap()
96 }
97
98 pub fn label(&self) -> &'static str {
99 match self {
100 SearchOption::WholeWord => "Match Whole Words",
101 SearchOption::CaseSensitive => "Match Case Sensitivity",
102 SearchOption::IncludeIgnored => "Also search files ignored by configuration",
103 SearchOption::Regex => "Use Regular Expressions",
104 SearchOption::OneMatchPerLine => "One Match Per Line",
105 SearchOption::Backwards => "Search Backwards",
106 }
107 }
108
109 pub fn icon(&self) -> ui::IconName {
110 match self {
111 SearchOption::WholeWord => ui::IconName::WholeWord,
112 SearchOption::CaseSensitive => ui::IconName::CaseSensitive,
113 SearchOption::IncludeIgnored => ui::IconName::Sliders,
114 SearchOption::Regex => ui::IconName::Regex,
115 _ => panic!("{self:?} is not a named SearchOption"),
116 }
117 }
118
119 pub fn to_toggle_action(self) -> &'static dyn Action {
120 match self {
121 SearchOption::WholeWord => &ToggleWholeWord,
122 SearchOption::CaseSensitive => &ToggleCaseSensitive,
123 SearchOption::IncludeIgnored => &ToggleIncludeIgnored,
124 SearchOption::Regex => &ToggleRegex,
125 _ => panic!("{self:?} is not a toggle action"),
126 }
127 }
128
129 pub(crate) fn as_button(
130 &self,
131 active: SearchOptions,
132 search_source: SearchSource,
133 focus_handle: FocusHandle,
134 ) -> impl IntoElement {
135 let action = self.to_toggle_action();
136 let label = self.label();
137 IconButton::new(
138 (label, matches!(search_source, SearchSource::Buffer) as u32),
139 self.icon(),
140 )
141 .map(|button| match search_source {
142 SearchSource::Buffer => {
143 let focus_handle = focus_handle.clone();
144 button.on_click(move |_: &ClickEvent, window, cx| {
145 if !focus_handle.is_focused(window) {
146 window.focus(&focus_handle);
147 }
148 window.dispatch_action(action.boxed_clone(), cx);
149 })
150 }
151 SearchSource::Project(cx) => {
152 let options = self.as_options();
153 button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
154 this.toggle_search_option(options, window, cx);
155 }))
156 }
157 })
158 .style(ButtonStyle::Subtle)
159 .shape(IconButtonShape::Square)
160 .toggle_state(active.contains(self.as_options()))
161 .tooltip({
162 move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx)
163 })
164 }
165}
166
167impl SearchOptions {
168 pub fn none() -> SearchOptions {
169 SearchOptions::NONE
170 }
171
172 pub fn from_query(query: &SearchQuery) -> SearchOptions {
173 let mut options = SearchOptions::NONE;
174 options.set(SearchOptions::WHOLE_WORD, query.whole_word());
175 options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
176 options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
177 options.set(SearchOptions::REGEX, query.is_regex());
178 options
179 }
180
181 pub fn from_settings(settings: &SearchSettings) -> SearchOptions {
182 let mut options = SearchOptions::NONE;
183 options.set(SearchOptions::WHOLE_WORD, settings.whole_word);
184 options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive);
185 options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored);
186 options.set(SearchOptions::REGEX, settings.regex);
187 options
188 }
189}
190
191pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
192 window.defer(cx, |window, cx| {
193 struct NotifType();
194 let notification_id = NotificationId::unique::<NotifType>();
195
196 let Some(workspace) = window.root::<Workspace>().flatten() else {
197 return;
198 };
199 workspace.update(cx, |workspace, cx| {
200 workspace.show_toast(
201 Toast::new(notification_id.clone(), "No more matches").autohide(),
202 cx,
203 );
204 })
205 });
206}