Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:12:15.762Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

search.rs

248 lines · 8.5 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.