search.rs

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