2077 lines · 81.1 KB · Rust
RawDownload
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.