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