use bitflags::bitflags; pub use buffer_search::BufferSearchBar; pub use editor::HighlightKey; use editor::SearchSettings; use gpui::{Action, App, ClickEvent, Entity, FocusHandle, IntoElement, actions}; use project::search::SearchQuery; pub use project_search::ProjectSearchView; use ui::{IconButtonShape, Tooltip, prelude::*}; use util::paths::PathMatcher; use workspace::notifications::NotificationId; use workspace::{Toast, Workspace}; pub use zed_actions::search::ToggleIncludeIgnored; pub use search_status_button::SEARCH_ICON; use crate::project_search::ProjectSearchBar; pub mod buffer_search; pub mod project_search; pub(crate) mod search_bar; pub mod search_status_button; pub mod text_finder; pub fn init(cx: &mut App) { menu::init(); buffer_search::init(cx); project_search::init(cx); text_finder::init(cx); } actions!( search, [ /// Focuses on the search input field. FocusSearch, /// Toggles whole word matching. ToggleWholeWord, /// Toggles case-sensitive search. ToggleCaseSensitive, /// Toggles regular expression mode. ToggleRegex, /// Toggles the replace interface. ToggleReplace, /// Toggles searching within selection only. ToggleSelection, /// Selects the next search match. SelectNextMatch, /// Selects the previous search match. SelectPreviousMatch, /// Selects all search matches. SelectAllMatches, /// Cycles through search modes. CycleMode, /// Navigates to the next query in search history. NextHistoryQuery, /// Navigates to the previous query in search history. PreviousHistoryQuery, /// Replaces all matches. ReplaceAll, /// Replaces the next match. ReplaceNext, ] ); bitflags! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub struct SearchOptions: u8 { const NONE = 0; const WHOLE_WORD = 1 << SearchOption::WholeWord as u8; const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8; const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8; const REGEX = 1 << SearchOption::Regex as u8; const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8; /// If set, reverse direction when finding the active match const BACKWARDS = 1 << SearchOption::Backwards as u8; } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum SearchOption { WholeWord = 0, CaseSensitive, IncludeIgnored, Regex, OneMatchPerLine, Backwards, } const REPLACE_PLACEHOLDER: &str = "Replace in project…"; const INCLUDE_PLACEHOLDER: &str = "Include: e.g. src/**/*.rs"; const EXCLUDE_PLACEHOLDER: &str = "Exclude: e.g. vendor/*, *.lock"; pub enum SearchSource<'a, 'b> { Buffer, Project(&'a Context<'b, ProjectSearchBar>), } impl SearchOption { pub fn as_options(&self) -> SearchOptions { SearchOptions::from_bits(1 << *self as u8).unwrap() } pub fn label(&self) -> &'static str { match self { SearchOption::WholeWord => "Match Whole Words", SearchOption::CaseSensitive => "Match Case Sensitivity", SearchOption::IncludeIgnored => "Also search files ignored by configuration", SearchOption::Regex => "Use Regular Expressions", SearchOption::OneMatchPerLine => "One Match Per Line", SearchOption::Backwards => "Search Backwards", } } pub fn icon(&self) -> ui::IconName { match self { SearchOption::WholeWord => IconName::WholeWord, SearchOption::CaseSensitive => IconName::CaseSensitive, SearchOption::IncludeIgnored => IconName::FileIgnored, SearchOption::Regex => IconName::Regex, _ => panic!("{self:?} is not a named SearchOption"), } } pub fn to_toggle_action(self) -> &'static dyn Action { match self { SearchOption::WholeWord => &ToggleWholeWord, SearchOption::CaseSensitive => &ToggleCaseSensitive, SearchOption::IncludeIgnored => &ToggleIncludeIgnored, SearchOption::Regex => &ToggleRegex, _ => panic!("{self:?} is not a toggle action"), } } pub fn as_button( &self, active: SearchOptions, search_source: SearchSource, focus_handle: FocusHandle, ) -> impl IntoElement { let action = self.to_toggle_action(); let label = self.label(); IconButton::new( (label, matches!(search_source, SearchSource::Buffer) as u32), self.icon(), ) .map(|button| match search_source { SearchSource::Buffer => { let focus_handle = focus_handle.clone(); button.on_click(move |_: &ClickEvent, window, cx| { if !focus_handle.is_focused(window) { window.focus(&focus_handle, cx); } window.dispatch_action(action.boxed_clone(), cx); }) } SearchSource::Project(cx) => { let options = self.as_options(); button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { this.toggle_search_option(options, window, cx); })) } }) .shape(IconButtonShape::Square) .toggle_state(active.contains(self.as_options())) .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx)) } } impl SearchOptions { pub fn none() -> SearchOptions { SearchOptions::NONE } pub fn from_query(query: &SearchQuery) -> SearchOptions { let mut options = SearchOptions::NONE; options.set(SearchOptions::WHOLE_WORD, query.whole_word()); options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored()); options.set(SearchOptions::REGEX, query.is_regex()); options } pub fn from_settings(settings: &SearchSettings) -> SearchOptions { let mut options = SearchOptions::NONE; options.set(SearchOptions::WHOLE_WORD, settings.whole_word); options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive); options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored); options.set(SearchOptions::REGEX, settings.regex); options } /// Build a [`SearchQuery`] from these options, selecting the regex or text /// constructor based on [`SearchOptions::REGEX`]. Inverse of /// [`SearchOptions::from_query`]. pub fn build_query( &self, query: impl ToString, files_to_include: PathMatcher, files_to_exclude: PathMatcher, match_full_paths: bool, buffers: Option>>, ) -> anyhow::Result { if self.contains(SearchOptions::REGEX) { SearchQuery::regex( query, self.contains(SearchOptions::WHOLE_WORD), self.contains(SearchOptions::CASE_SENSITIVE), self.contains(SearchOptions::INCLUDE_IGNORED), self.contains(SearchOptions::ONE_MATCH_PER_LINE), files_to_include, files_to_exclude, match_full_paths, buffers, ) } else { SearchQuery::text( query, self.contains(SearchOptions::WHOLE_WORD), self.contains(SearchOptions::CASE_SENSITIVE), self.contains(SearchOptions::INCLUDE_IGNORED), files_to_include, files_to_exclude, match_full_paths, buffers, ) } } } pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { window.defer(cx, |window, cx| { struct NotifType(); let notification_id = NotificationId::unique::(); let Some(workspace) = Workspace::for_window(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { workspace.show_toast( Toast::new(notification_id.clone(), "No more matches").autohide(), cx, ); }) }); }