Skip to repository content248 lines · 8.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:15:51.454Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
search.rs
1use bitflags::bitflags;
2pub use buffer_search::BufferSearchBar;
3pub use editor::HighlightKey;
4use editor::SearchSettings;
5use gpui::{Action, App, ClickEvent, Entity, FocusHandle, IntoElement, actions};
6use project::search::SearchQuery;
7pub use project_search::ProjectSearchView;
8use ui::{IconButtonShape, Tooltip, prelude::*};
9use util::paths::PathMatcher;
10use workspace::notifications::NotificationId;
11use workspace::{Toast, Workspace};
12pub use zed_actions::search::ToggleIncludeIgnored;
13
14pub use search_status_button::SEARCH_ICON;
15
16use crate::project_search::ProjectSearchBar;
17
18pub mod buffer_search;
19pub mod project_search;
20pub(crate) mod search_bar;
21pub mod search_status_button;
22pub mod text_finder;
23
24pub fn init(cx: &mut App) {
25 menu::init();
26 buffer_search::init(cx);
27 project_search::init(cx);
28 text_finder::init(cx);
29}
30
31actions!(
32 search,
33 [
34 /// Focuses on the search input field.
35 FocusSearch,
36 /// Toggles whole word matching.
37 ToggleWholeWord,
38 /// Toggles case-sensitive search.
39 ToggleCaseSensitive,
40 /// Toggles regular expression mode.
41 ToggleRegex,
42 /// Toggles the replace interface.
43 ToggleReplace,
44 /// Toggles searching within selection only.
45 ToggleSelection,
46 /// Selects the next search match.
47 SelectNextMatch,
48 /// Selects the previous search match.
49 SelectPreviousMatch,
50 /// Selects all search matches.
51 SelectAllMatches,
52 /// Cycles through search modes.
53 CycleMode,
54 /// Navigates to the next query in search history.
55 NextHistoryQuery,
56 /// Navigates to the previous query in search history.
57 PreviousHistoryQuery,
58 /// Replaces all matches.
59 ReplaceAll,
60 /// Replaces the next match.
61 ReplaceNext,
62 ]
63);
64
65bitflags! {
66 #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
67 pub struct SearchOptions: u8 {
68 const NONE = 0;
69 const WHOLE_WORD = 1 << SearchOption::WholeWord as u8;
70 const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8;
71 const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8;
72 const REGEX = 1 << SearchOption::Regex as u8;
73 const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8;
74 /// If set, reverse direction when finding the active match
75 const BACKWARDS = 1 << SearchOption::Backwards as u8;
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[repr(u8)]
81pub enum SearchOption {
82 WholeWord = 0,
83 CaseSensitive,
84 IncludeIgnored,
85 Regex,
86 OneMatchPerLine,
87 Backwards,
88}
89
90const REPLACE_PLACEHOLDER: &str = "Replace in project…";
91const INCLUDE_PLACEHOLDER: &str = "Include: e.g. src/**/*.rs";
92const EXCLUDE_PLACEHOLDER: &str = "Exclude: e.g. vendor/*, *.lock";
93
94pub enum SearchSource<'a, 'b> {
95 Buffer,
96 Project(&'a Context<'b, ProjectSearchBar>),
97}
98
99impl SearchOption {
100 pub fn as_options(&self) -> SearchOptions {
101 SearchOptions::from_bits(1 << *self as u8).unwrap()
102 }
103
104 pub fn label(&self) -> &'static str {
105 match self {
106 SearchOption::WholeWord => "Match Whole Words",
107 SearchOption::CaseSensitive => "Match Case Sensitivity",
108 SearchOption::IncludeIgnored => "Also search files ignored by configuration",
109 SearchOption::Regex => "Use Regular Expressions",
110 SearchOption::OneMatchPerLine => "One Match Per Line",
111 SearchOption::Backwards => "Search Backwards",
112 }
113 }
114
115 pub fn icon(&self) -> ui::IconName {
116 match self {
117 SearchOption::WholeWord => IconName::WholeWord,
118 SearchOption::CaseSensitive => IconName::CaseSensitive,
119 SearchOption::IncludeIgnored => IconName::FileIgnored,
120 SearchOption::Regex => IconName::Regex,
121 _ => panic!("{self:?} is not a named SearchOption"),
122 }
123 }
124
125 pub fn to_toggle_action(self) -> &'static dyn Action {
126 match self {
127 SearchOption::WholeWord => &ToggleWholeWord,
128 SearchOption::CaseSensitive => &ToggleCaseSensitive,
129 SearchOption::IncludeIgnored => &ToggleIncludeIgnored,
130 SearchOption::Regex => &ToggleRegex,
131 _ => panic!("{self:?} is not a toggle action"),
132 }
133 }
134
135 pub fn as_button(
136 &self,
137 active: SearchOptions,
138 search_source: SearchSource,
139 focus_handle: FocusHandle,
140 ) -> impl IntoElement {
141 let action = self.to_toggle_action();
142 let label = self.label();
143
144 IconButton::new(
145 (label, matches!(search_source, SearchSource::Buffer) as u32),
146 self.icon(),
147 )
148 .map(|button| match search_source {
149 SearchSource::Buffer => {
150 let focus_handle = focus_handle.clone();
151 button.on_click(move |_: &ClickEvent, window, cx| {
152 if !focus_handle.is_focused(window) {
153 window.focus(&focus_handle, cx);
154 }
155 window.dispatch_action(action.boxed_clone(), cx);
156 })
157 }
158 SearchSource::Project(cx) => {
159 let options = self.as_options();
160 button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
161 this.toggle_search_option(options, window, cx);
162 }))
163 }
164 })
165 .shape(IconButtonShape::Square)
166 .toggle_state(active.contains(self.as_options()))
167 .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx))
168 }
169}
170
171impl SearchOptions {
172 pub fn none() -> SearchOptions {
173 SearchOptions::NONE
174 }
175
176 pub fn from_query(query: &SearchQuery) -> SearchOptions {
177 let mut options = SearchOptions::NONE;
178 options.set(SearchOptions::WHOLE_WORD, query.whole_word());
179 options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
180 options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
181 options.set(SearchOptions::REGEX, query.is_regex());
182 options
183 }
184
185 pub fn from_settings(settings: &SearchSettings) -> SearchOptions {
186 let mut options = SearchOptions::NONE;
187 options.set(SearchOptions::WHOLE_WORD, settings.whole_word);
188 options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive);
189 options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored);
190 options.set(SearchOptions::REGEX, settings.regex);
191 options
192 }
193
194 /// Build a [`SearchQuery`] from these options, selecting the regex or text
195 /// constructor based on [`SearchOptions::REGEX`]. Inverse of
196 /// [`SearchOptions::from_query`].
197 pub fn build_query(
198 &self,
199 query: impl ToString,
200 files_to_include: PathMatcher,
201 files_to_exclude: PathMatcher,
202 match_full_paths: bool,
203 buffers: Option<Vec<Entity<language::Buffer>>>,
204 ) -> anyhow::Result<SearchQuery> {
205 if self.contains(SearchOptions::REGEX) {
206 SearchQuery::regex(
207 query,
208 self.contains(SearchOptions::WHOLE_WORD),
209 self.contains(SearchOptions::CASE_SENSITIVE),
210 self.contains(SearchOptions::INCLUDE_IGNORED),
211 self.contains(SearchOptions::ONE_MATCH_PER_LINE),
212 files_to_include,
213 files_to_exclude,
214 match_full_paths,
215 buffers,
216 )
217 } else {
218 SearchQuery::text(
219 query,
220 self.contains(SearchOptions::WHOLE_WORD),
221 self.contains(SearchOptions::CASE_SENSITIVE),
222 self.contains(SearchOptions::INCLUDE_IGNORED),
223 files_to_include,
224 files_to_exclude,
225 match_full_paths,
226 buffers,
227 )
228 }
229 }
230}
231
232pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
233 window.defer(cx, |window, cx| {
234 struct NotifType();
235 let notification_id = NotificationId::unique::<NotifType>();
236
237 let Some(workspace) = Workspace::for_window(window, cx) else {
238 return;
239 };
240 workspace.update(cx, |workspace, cx| {
241 workspace.show_toast(
242 Toast::new(notification_id.clone(), "No more matches").autohide(),
243 cx,
244 );
245 })
246 });
247}
248