mod registrar; use crate::{ search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,};use any_vec::AnyVec;use collections::HashMap;use editor::{ actions::{Tab, TabPrev}, DisplayPoint, Editor, EditorElement, EditorStyle,};use futures::channel::oneshot;use gpui::{ actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, WhiteSpace, WindowContext,};use project::{ search::SearchQuery, search_history::{SearchHistory, SearchHistoryCursor},};use serde::Deserialize;use settings::Settings;use std::sync::Arc;use theme::ThemeSettings; use ui::{h_flex, prelude::*, IconButton, IconName, Tooltip, BASE_REM_SIZE_IN_PX};use util::ResultExt;use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,}; pub use registrar::DivRegistrar;use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MIN_INPUT_WIDTH_REMS: f32 = 10.;const MAX_INPUT_WIDTH_REMS: f32 = 30.;const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; const fn true_value() -> bool { true} #[derive(PartialEq, Clone, Deserialize)]pub struct Deploy { #[serde(default = "true_value")] pub focus: bool, #[serde(default)] pub replace_enabled: bool,} impl_actions!(buffer_search, [Deploy]); actions!(buffer_search, [Dismiss, FocusEditor]); impl Deploy { pub fn find() -> Self { Self { focus: true, replace_enabled: false, } }} pub enum Event { UpdateLocation,} pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) .detach();} pub struct BufferSearchBar { query_editor: View<Editor>, query_editor_focused: bool, replacement_editor: View<Editor>, replacement_editor_focused: bool, active_searchable_item: Option<Box<dyn SearchableItemHandle>>, active_match_index: Option<usize>, active_searchable_item_subscription: Option<Subscription>, active_search: Option<Arc<SearchQuery>>, searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>, pending_search: Option<Task<()>>, search_options: SearchOptions, default_options: SearchOptions, query_contains_error: bool, dismissed: bool, search_history: SearchHistory, search_history_cursor: SearchHistoryCursor, replace_enabled: bool, scroll_handle: ScrollHandle, editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels,} impl BufferSearchBar { fn render_text_input( &self, editor: &View<Editor>, color: Hsla, cx: &ViewContext<Self>, ) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if editor.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { color }, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, white_space: WhiteSpace::Normal, }; EditorElement::new( &editor, EditorStyle { background: cx.theme().colors().editor_background, local_player: cx.theme().players().local(), text: text_style, ..Default::default() }, ) }} impl EventEmitter<Event> for BufferSearchBar {}impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}impl Render for BufferSearchBar { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { if self.dismissed { return div().id("search_bar"); } let narrow_mode = self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX; let hide_inline_icons = self.editor_needed_width > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.; let supported_options = self.supported_options(); if self.query_editor.update(cx, |query_editor, cx| { query_editor.placeholder_text(cx).is_none() }) { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text("Search", cx); }); } self.replacement_editor.update(cx, |editor, cx| { editor.set_placeholder_text("Replace with...", cx); }); let mut text_color = Color::Default; let match_text = self .active_searchable_item .as_ref() .and_then(|searchable_item| { if self.query(cx).is_empty() { return None; } let matches_count = self .searchable_items_with_matches .get(&searchable_item.downgrade()) .map(AnyVec::len) .unwrap_or(0); if let Some(match_ix) = self.active_match_index { Some(format!("{}/{}", match_ix + 1, matches_count)) } else { text_color = Color::Error; // No matches found None } }) .unwrap_or_else(|| "0/0".to_string()); let should_show_replace_input = self.replace_enabled && supported_options.replacement; let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); let mut key_context = KeyContext::new_with_defaults(); key_context.add("BufferSearchBar"); if in_replace { key_context.add("in_replace"); } let editor_border = if self.query_contains_error { Color::Error.color(cx) } else { cx.theme().colors().border }; let search_line = h_flex() .child( h_flex() .id("editor-scroll") .track_scroll(&self.editor_scroll_handle) .flex_1() .h_8() .px_2() .mr_2() .py_1() .border_1() .border_color(editor_border) .min_w(rems(MIN_INPUT_WIDTH_REMS)) .max_w(rems(MAX_INPUT_WIDTH_REMS)) .rounded_lg() .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx)) .when(!hide_inline_icons, |div| { div.children(supported_options.case.then(|| { self.render_search_option_button( SearchOptions::CASE_SENSITIVE, cx.listener(|this, _, cx| { this.toggle_case_sensitive(&ToggleCaseSensitive, cx) }), ) })) .children(supported_options.word.then(|| { self.render_search_option_button( SearchOptions::WHOLE_WORD, cx.listener(|this, _, cx| { this.toggle_whole_word(&ToggleWholeWord, cx) }), ) })) .children(supported_options.word.then(|| { self.render_search_option_button( SearchOptions::REGEX, cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)), ) })) }), ) .when(supported_options.replacement, |this| { this.child( IconButton::new("buffer-search-bar-toggle-replace-button", IconName::Replace) .style(ButtonStyle::Subtle) .when(self.replace_enabled, |button| { button.style(ButtonStyle::Filled) }) .on_click(cx.listener(|this, _: &ClickEvent, cx| { this.toggle_replace(&ToggleReplace, cx); })) .selected(self.replace_enabled) .size(ButtonSize::Compact) .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)), ) }) .child( h_flex() .flex_none() .child( IconButton::new("select-all", ui::IconName::SelectAll) .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone())) .size(ButtonSize::Compact) .tooltip(|cx| { Tooltip::for_action("Select all matches", &SelectAllMatches, cx) }), ) .child(render_nav_button( ui::IconName::ChevronLeft, self.active_match_index.is_some(), "Select previous match", &SelectPrevMatch, )) .child(render_nav_button( ui::IconName::ChevronRight, self.active_match_index.is_some(), "Select next match", &SelectNextMatch, )) .when(!narrow_mode, |this| { this.child(h_flex().min_w(rems_from_px(40.)).child( Label::new(match_text).color(if self.active_match_index.is_some() { Color::Default } else { Color::Disabled }), )) }), ); let replace_line = should_show_replace_input.then(|| { h_flex() .gap_2() .flex_1() .child( h_flex() .flex_1() // We're giving this a fixed height to match the height of the search input, // which has an icon inside that is increasing its height. .h_8() .px_2() .py_1() .border_1() .border_color(cx.theme().colors().border) .rounded_lg() .min_w(rems(MIN_INPUT_WIDTH_REMS)) .max_w(rems(MAX_INPUT_WIDTH_REMS)) .child(self.render_text_input( &self.replacement_editor, cx.theme().colors().text, cx, )), ) .child( h_flex() .flex_none() .child( IconButton::new("search-replace-next", ui::IconName::ReplaceNext) .tooltip(move |cx| { Tooltip::for_action("Replace next", &ReplaceNext, cx) }) .on_click( cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)), ), ) .child( IconButton::new("search-replace-all", ui::IconName::ReplaceAll) .tooltip(move |cx| { Tooltip::for_action("Replace all", &ReplaceAll, cx) }) .on_click( cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)), ), ), ) }); v_flex() .id("buffer_search") .track_scroll(&self.scroll_handle) .key_context(key_context) .capture_action(cx.listener(Self::tab)) .capture_action(cx.listener(Self::tab_prev)) .on_action(cx.listener(Self::previous_history_query)) .on_action(cx.listener(Self::next_history_query)) .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_prev_match)) .when(self.supported_options().replacement, |this| { this.on_action(cx.listener(Self::toggle_replace)) .when(in_replace, |this| { this.on_action(cx.listener(Self::replace_next)) .on_action(cx.listener(Self::replace_all)) }) }) .when(self.supported_options().case, |this| { this.on_action(cx.listener(Self::toggle_case_sensitive)) }) .when(self.supported_options().word, |this| { this.on_action(cx.listener(Self::toggle_whole_word)) }) .when(self.supported_options().regex, |this| { this.on_action(cx.listener(Self::toggle_regex)) }) .gap_2() .child( h_flex() .child(search_line.w_full()) .when(!narrow_mode, |div| { div.child( IconButton::new(SharedString::from("Close"), IconName::Close) .tooltip(move |cx| { Tooltip::for_action("Close search bar", &Dismiss, cx) }) .on_click(cx.listener(|this, _: &ClickEvent, cx| { this.dismiss(&Dismiss, cx) })), ) }), ) .children(replace_line) }} impl FocusableView for BufferSearchBar { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { self.query_editor.focus_handle(cx) }} impl ToolbarItemView for BufferSearchBar { fn set_active_pane_item( &mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>, ) -> ToolbarItemLocation { cx.notify(); self.active_searchable_item_subscription.take(); self.active_searchable_item.take(); self.pending_search.take(); if let Some(searchable_item_handle) = item.and_then(|item| item.to_searchable_item_handle(cx)) { let this = cx.view().downgrade(); self.active_searchable_item_subscription = Some(searchable_item_handle.subscribe_to_search_events( cx, Box::new(move |search_event, cx| { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.on_active_searchable_item_event(search_event, cx) }); } }), )); self.active_searchable_item = Some(searchable_item_handle); let _ = self.update_matches(cx); if !self.dismissed { return ToolbarItemLocation::Secondary; } } ToolbarItemLocation::Hidden }} impl BufferSearchBar { pub fn register(registrar: &mut impl SearchActionsRegistrar) { registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| { this.query_editor.focus_handle(cx).focus(cx); this.select_query(cx); })); registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| { if this.supported_options().case { this.toggle_case_sensitive(action, cx); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| { if this.supported_options().word { this.toggle_whole_word(action, cx); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| { if this.supported_options().replacement { this.toggle_replace(action, cx); } })); registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| { this.select_next_match(action, cx); })); registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| { this.select_prev_match(action, cx); })); registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| { this.select_all_matches(action, cx); })); registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| { this.dismiss(&Dismiss, cx); })); // register deploy buffer search for both search bar states, since we want to focus into the search bar // when the deploy action is triggered in the buffer. registrar.register_handler(ForDeployed(|this, deploy, cx| { this.deploy(deploy, cx); })); registrar.register_handler(ForDismissed(|this, deploy, cx| { this.deploy(deploy, cx); })) } pub fn new(cx: &mut ViewContext<Self>) -> Self { let query_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); let replacement_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&replacement_editor, Self::on_replacement_editor_event) .detach(); Self { query_editor, query_editor_focused: false, replacement_editor, replacement_editor_focused: false, active_searchable_item: None, active_searchable_item_subscription: None, active_match_index: None, searchable_items_with_matches: Default::default(), default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, query_contains_error: false, dismissed: true, search_history: SearchHistory::new( Some(MAX_BUFFER_SEARCH_HISTORY_SIZE), project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains, ),Lines 1–500 of 2077. Load more
mod registrar; use crate::{ search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord, }; use any_vec::AnyVec; use collections::HashMap; use editor::{ actions::{Tab, TabPrev}, DisplayPoint, Editor, EditorElement, EditorStyle, }; use futures::channel::oneshot; use gpui::{ actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, WhiteSpace, WindowContext, }; use project::{ search::SearchQuery, search_history::{SearchHistory, SearchHistoryCursor}, }; use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::ThemeSettings; use ui::{h_flex, prelude::*, IconButton, IconName, Tooltip, BASE_REM_SIZE_IN_PX}; use util::ResultExt; use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; pub use registrar::DivRegistrar; use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MIN_INPUT_WIDTH_REMS: f32 = 10.; const MAX_INPUT_WIDTH_REMS: f32 = 30.; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; const fn true_value() -> bool { true } #[derive(PartialEq, Clone, Deserialize)] pub struct Deploy { #[serde(default = "true_value")] pub focus: bool, #[serde(default)] pub replace_enabled: bool, } impl_actions!(buffer_search, [Deploy]); actions!(buffer_search, [Dismiss, FocusEditor]); impl Deploy { pub fn find() -> Self { Self { focus: true, replace_enabled: false, } } } pub enum Event { UpdateLocation, } pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) .detach(); } pub struct BufferSearchBar { query_editor: View<Editor>, query_editor_focused: bool, replacement_editor: View<Editor>, replacement_editor_focused: bool, active_searchable_item: Option<Box<dyn SearchableItemHandle>>, active_match_index: Option<usize>, active_searchable_item_subscription: Option<Subscription>, active_search: Option<Arc<SearchQuery>>, searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>, pending_search: Option<Task<()>>, search_options: SearchOptions, default_options: SearchOptions, query_contains_error: bool, dismissed: bool, search_history: SearchHistory, search_history_cursor: SearchHistoryCursor, replace_enabled: bool, scroll_handle: ScrollHandle, editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels, } impl BufferSearchBar { fn render_text_input( &self, editor: &View<Editor>, color: Hsla, cx: &ViewContext<Self>, ) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if editor.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { color }, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, white_space: WhiteSpace::Normal, }; EditorElement::new( &editor, EditorStyle { background: cx.theme().colors().editor_background, local_player: cx.theme().players().local(), text: text_style, ..Default::default() }, ) } } impl EventEmitter<Event> for BufferSearchBar {} impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {} impl Render for BufferSearchBar { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { if self.dismissed { return div().id("search_bar"); } let narrow_mode = self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX; let hide_inline_icons = self.editor_needed_width > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.; let supported_options = self.supported_options(); if self.query_editor.update(cx, |query_editor, cx| { query_editor.placeholder_text(cx).is_none() }) { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text("Search", cx); }); } self.replacement_editor.update(cx, |editor, cx| { editor.set_placeholder_text("Replace with...", cx); }); let mut text_color = Color::Default; let match_text = self .active_searchable_item .as_ref() .and_then(|searchable_item| { if self.query(cx).is_empty() { return None; } let matches_count = self .searchable_items_with_matches .get(&searchable_item.downgrade()) .map(AnyVec::len) .unwrap_or(0); if let Some(match_ix) = self.active_match_index { Some(format!("{}/{}", match_ix + 1, matches_count)) } else { text_color = Color::Error; // No matches found None } }) .unwrap_or_else(|| "0/0".to_string()); let should_show_replace_input = self.replace_enabled && supported_options.replacement; let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); let mut key_context = KeyContext::new_with_defaults(); key_context.add("BufferSearchBar"); if in_replace { key_context.add("in_replace"); } let editor_border = if self.query_contains_error { Color::Error.color(cx) } else { cx.theme().colors().border }; let search_line = h_flex() .child( h_flex() .id("editor-scroll") .track_scroll(&self.editor_scroll_handle) .flex_1() .h_8() .px_2() .mr_2() .py_1() .border_1() .border_color(editor_border) .min_w(rems(MIN_INPUT_WIDTH_REMS)) .max_w(rems(MAX_INPUT_WIDTH_REMS)) .rounded_lg() .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx)) .when(!hide_inline_icons, |div| { div.children(supported_options.case.then(|| { self.render_search_option_button( SearchOptions::CASE_SENSITIVE, cx.listener(|this, _, cx| { this.toggle_case_sensitive(&ToggleCaseSensitive, cx) }), ) })) .children(supported_options.word.then(|| { self.render_search_option_button( SearchOptions::WHOLE_WORD, cx.listener(|this, _, cx| { this.toggle_whole_word(&ToggleWholeWord, cx) }), ) })) .children(supported_options.word.then(|| { self.render_search_option_button( SearchOptions::REGEX, cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)), ) })) }), ) .when(supported_options.replacement, |this| { this.child( IconButton::new("buffer-search-bar-toggle-replace-button", IconName::Replace) .style(ButtonStyle::Subtle) .when(self.replace_enabled, |button| { button.style(ButtonStyle::Filled) }) .on_click(cx.listener(|this, _: &ClickEvent, cx| { this.toggle_replace(&ToggleReplace, cx); })) .selected(self.replace_enabled) .size(ButtonSize::Compact) .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)), ) }) .child( h_flex() .flex_none() .child( IconButton::new("select-all", ui::IconName::SelectAll) .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone())) .size(ButtonSize::Compact) .tooltip(|cx| { Tooltip::for_action("Select all matches", &SelectAllMatches, cx) }), ) .child(render_nav_button( ui::IconName::ChevronLeft, self.active_match_index.is_some(), "Select previous match", &SelectPrevMatch, )) .child(render_nav_button( ui::IconName::ChevronRight, self.active_match_index.is_some(), "Select next match", &SelectNextMatch, )) .when(!narrow_mode, |this| { this.child(h_flex().min_w(rems_from_px(40.)).child( Label::new(match_text).color(if self.active_match_index.is_some() { Color::Default } else { Color::Disabled }), )) }), ); let replace_line = should_show_replace_input.then(|| { h_flex() .gap_2() .flex_1() .child( h_flex() .flex_1() // We're giving this a fixed height to match the height of the search input, // which has an icon inside that is increasing its height. .h_8() .px_2() .py_1() .border_1() .border_color(cx.theme().colors().border) .rounded_lg() .min_w(rems(MIN_INPUT_WIDTH_REMS)) .max_w(rems(MAX_INPUT_WIDTH_REMS)) .child(self.render_text_input( &self.replacement_editor, cx.theme().colors().text, cx, )), ) .child( h_flex() .flex_none() .child( IconButton::new("search-replace-next", ui::IconName::ReplaceNext) .tooltip(move |cx| { Tooltip::for_action("Replace next", &ReplaceNext, cx) }) .on_click( cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)), ), ) .child( IconButton::new("search-replace-all", ui::IconName::ReplaceAll) .tooltip(move |cx| { Tooltip::for_action("Replace all", &ReplaceAll, cx) }) .on_click( cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)), ), ), ) }); v_flex() .id("buffer_search") .track_scroll(&self.scroll_handle) .key_context(key_context) .capture_action(cx.listener(Self::tab)) .capture_action(cx.listener(Self::tab_prev)) .on_action(cx.listener(Self::previous_history_query)) .on_action(cx.listener(Self::next_history_query)) .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_prev_match)) .when(self.supported_options().replacement, |this| { this.on_action(cx.listener(Self::toggle_replace)) .when(in_replace, |this| { this.on_action(cx.listener(Self::replace_next)) .on_action(cx.listener(Self::replace_all)) }) }) .when(self.supported_options().case, |this| { this.on_action(cx.listener(Self::toggle_case_sensitive)) }) .when(self.supported_options().word, |this| { this.on_action(cx.listener(Self::toggle_whole_word)) }) .when(self.supported_options().regex, |this| { this.on_action(cx.listener(Self::toggle_regex)) }) .gap_2() .child( h_flex() .child(search_line.w_full()) .when(!narrow_mode, |div| { div.child( IconButton::new(SharedString::from("Close"), IconName::Close) .tooltip(move |cx| { Tooltip::for_action("Close search bar", &Dismiss, cx) }) .on_click(cx.listener(|this, _: &ClickEvent, cx| { this.dismiss(&Dismiss, cx) })), ) }), ) .children(replace_line) } } impl FocusableView for BufferSearchBar { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { self.query_editor.focus_handle(cx) } } impl ToolbarItemView for BufferSearchBar { fn set_active_pane_item( &mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>, ) -> ToolbarItemLocation { cx.notify(); self.active_searchable_item_subscription.take(); self.active_searchable_item.take(); self.pending_search.take(); if let Some(searchable_item_handle) = item.and_then(|item| item.to_searchable_item_handle(cx)) { let this = cx.view().downgrade(); self.active_searchable_item_subscription = Some(searchable_item_handle.subscribe_to_search_events( cx, Box::new(move |search_event, cx| { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.on_active_searchable_item_event(search_event, cx) }); } }), )); self.active_searchable_item = Some(searchable_item_handle); let _ = self.update_matches(cx); if !self.dismissed { return ToolbarItemLocation::Secondary; } } ToolbarItemLocation::Hidden } } impl BufferSearchBar { pub fn register(registrar: &mut impl SearchActionsRegistrar) { registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| { this.query_editor.focus_handle(cx).focus(cx); this.select_query(cx); })); registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| { if this.supported_options().case { this.toggle_case_sensitive(action, cx); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| { if this.supported_options().word { this.toggle_whole_word(action, cx); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| { if this.supported_options().replacement { this.toggle_replace(action, cx); } })); registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| { this.select_next_match(action, cx); })); registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| { this.select_prev_match(action, cx); })); registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| { this.select_all_matches(action, cx); })); registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| { this.dismiss(&Dismiss, cx); })); // register deploy buffer search for both search bar states, since we want to focus into the search bar // when the deploy action is triggered in the buffer. registrar.register_handler(ForDeployed(|this, deploy, cx| { this.deploy(deploy, cx); })); registrar.register_handler(ForDismissed(|this, deploy, cx| { this.deploy(deploy, cx); })) } pub fn new(cx: &mut ViewContext<Self>) -> Self { let query_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); let replacement_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&replacement_editor, Self::on_replacement_editor_event) .detach(); Self { query_editor, query_editor_focused: false, replacement_editor, replacement_editor_focused: false, active_searchable_item: None, active_searchable_item_subscription: None, active_match_index: None, searchable_items_with_matches: Default::default(), default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, query_contains_error: false, dismissed: true, search_history: SearchHistory::new( Some(MAX_BUFFER_SEARCH_HISTORY_SIZE), project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains, ),