diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index cbbeeb0f1260a57a9907e854c9aeb666c078fd90..798277893608c2772812b7100166c3b2a09cdb62 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -165,7 +165,7 @@ impl Render for BufferSearchBar { let replace_all = should_show_replace_input .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll)); let replace_next = should_show_replace_input - .then(|| super::render_replace_button(ReplaceNext, ui::Icon::Replace)); + .then(|| super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext)); let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); h_stack() diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index e75b4f26b1519fbe8b781f9c41258630a6368fdc..c0147bad1ef7fe19f9934479172476b1e9957bac 100644 --- a/crates/search2/src/project_search.rs +++ b/crates/search2/src/project_search.rs @@ -13,9 +13,10 @@ use editor::{ MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, }; use gpui::{ - actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Div, Element, Entity, - EntityId, EventEmitter, FocusableView, Model, ModelContext, PromptLevel, Render, SharedString, - Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, + actions, div, white, Action, AnyElement, AnyView, AppContext, Context as _, Div, Element, + Entity, EntityId, EventEmitter, FocusableView, InteractiveElement, IntoElement, Model, + ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, }; use menu::Confirm; use project::{ @@ -35,6 +36,11 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use theme::ActiveTheme; +use ui::{ + h_stack, v_stack, Button, Clickable, Color, Disableable, Icon, IconButton, IconElement, Label, + Selectable, +}; use util::{paths::PathMatcher, ResultExt as _}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, @@ -318,7 +324,7 @@ impl EventEmitter for ProjectSearchView {} impl Render for ProjectSearchView { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() + div().child(Label::new("xd")) } } // impl Entity for ProjectSearchView { @@ -569,36 +575,23 @@ impl Item for ProjectSearchView { } fn tab_content(&self, _: Option, cx: &WindowContext<'_>) -> AnyElement { - // Flex::row() - // .with_child( - // Svg::new("icons/magnifying_glass.svg") - // .with_color(tab_theme.label.text.color) - // .constrained() - // .with_width(tab_theme.type_icon_width) - // .aligned() - // .contained() - // .with_margin_right(tab_theme.spacing), - // ) - // .with_child({ - // let tab_name: Option> = self - // .model - // .read(cx) - // .search_history - // .current() - // .as_ref() - // .map(|query| { - // let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); - // query_text.into() - // }); - // Label::new( - // tab_name - // .filter(|name| !name.is_empty()) - // .unwrap_or("Project search".into()), - // tab_theme.label.clone(), - // ) - // .aligned() - // }) - div().into_any() + let last_query: Option = self + .model + .read(cx) + .search_history + .current() + .as_ref() + .map(|query| { + let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); + query_text.into() + }); + let tab_name = last_query + .filter(|query| !query.is_empty()) + .unwrap_or_else(|| "Project search".into()); + h_stack() + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(Label::new(tab_name)) + .into_any() } fn for_each_project_item( @@ -1686,7 +1679,127 @@ impl Render for ProjectSearchBar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() + let Some(search) = self.active_project_search.clone() else { + return div(); + }; + let search = search.read(cx); + let query_column = v_stack() + .flex_1() + .child( + h_stack() + .min_w_80() + .on_action(cx.listener(|this, _: &ToggleFilters, cx| { + this.toggle_filters(cx); + })) + .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })) + .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + })) + .on_action(cx.listener(|this, action: &ToggleReplace, cx| { + this.toggle_replace(action, cx); + })) + .on_action(cx.listener(|this, action: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx) + })) + .on_action(cx.listener(|this, action: &ActivateRegexMode, cx| { + this.activate_search_mode(SearchMode::Regex, cx) + })) + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(search.query_editor.clone()) + .child( + h_stack() + .child( + IconButton::new("project-search-filter-button", Icon::Filter) + .on_click(|_, cx| { + cx.dispatch_action(ToggleFilters.boxed_clone()) + }), + ) + .child(IconButton::new( + "project-search-case-sensitive", + Icon::CaseSensitive, + )) + .child(IconButton::new( + "project-search-whole-word", + Icon::WholeWord, + )), + ) + .border_2() + .bg(white()) + .rounded_lg(), + ) + .when(search.filters_enabled, |this| { + this.child( + h_stack() + .child(search.included_files_editor.clone()) + .child(search.excluded_files_editor.clone()), + ) + }); + let mode_column = h_stack() + .child( + h_stack() + .child( + Button::new("project-search-text-button", "Text") + .selected(search.current_mode == SearchMode::Text) + .on_click(|_, cx| cx.dispatch_action(ActivateTextMode.boxed_clone())), + ) + .child( + Button::new("project-search-regex-button", "Regex") + .selected(search.current_mode == SearchMode::Regex) + .on_click(|_, cx| cx.dispatch_action(ActivateRegexMode.boxed_clone())), + ), + ) + .child( + IconButton::new("project-search-toggle-replace", Icon::Replace).on_click( + |_, cx| { + cx.dispatch_action(ToggleReplace.boxed_clone()); + }, + ), + ); + let replace_column = if search.replace_enabled { + h_stack() + .bg(white()) + .flex_1() + .border_2() + .rounded_lg() + .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small)) + .child(search.replacement_editor.clone()) + } else { + // Fill out the space if we don't have a replacement editor. + h_stack().size_full() + }; + let actions_column = h_stack() + .when(search.replace_enabled, |this| { + this.children([ + IconButton::new("project-search-replace-next", Icon::ReplaceNext), + IconButton::new("project-search-replace-all", Icon::ReplaceAll), + ]) + }) + .when_some(search.active_match_index, |this, index| { + let match_quantity = search.model.read(cx).match_ranges.len(); + debug_assert!(match_quantity > index); + this.child(IconButton::new( + "project-search-select-all", + Icon::SelectAll, + )) + .child(Label::new(format!("{index}/{match_quantity}"))) + }) + .children([ + IconButton::new("project-search-prev-match", Icon::ChevronLeft) + .disabled(search.active_match_index.is_none()), + IconButton::new("project-search-next-match", Icon::ChevronRight) + .disabled(search.active_match_index.is_none()), + ]); + h_stack() + .size_full() + .p_1() + .m_2() + .justify_between() + .child(query_column) + .child(mode_column) + .child(replace_column) + .child(actions_column) } } // impl Entity for ProjectSearchBar { diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a5b09782f569c46b1b7d26c61fe09deef4d7e90e..78df9569697718bef1cf5b456b7d54c850c377bd 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -61,6 +61,7 @@ pub enum Icon { FileRust, FileToml, FileTree, + Filter, Folder, FolderOpen, FolderX, @@ -80,6 +81,7 @@ pub enum Icon { Quote, Replace, ReplaceAll, + ReplaceNext, Screen, SelectAll, Split, @@ -138,6 +140,7 @@ impl Icon { Icon::FileRust => "icons/file_icons/rust.svg", Icon::FileToml => "icons/file_icons/toml.svg", Icon::FileTree => "icons/project.svg", + Icon::Filter => "icons/filter.svg", Icon::Folder => "icons/file_icons/folder.svg", Icon::FolderOpen => "icons/file_icons/folder_open.svg", Icon::FolderX => "icons/stop_sharing.svg", @@ -157,6 +160,7 @@ impl Icon { Icon::Quote => "icons/quote.svg", Icon::Replace => "icons/replace.svg", Icon::ReplaceAll => "icons/replace_all.svg", + Icon::ReplaceNext => "icons/replace_next.svg", Icon::Screen => "icons/desktop.svg", Icon::SelectAll => "icons/select-all.svg", Icon::Split => "icons/split.svg", diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index d36d16a654791ddf8cc6ad885bf62408124e4ad3..3fab03aa93034a29cb53688d1ab191d532f2cc89 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -24,6 +24,7 @@ use anyhow::{anyhow, Context as _}; use futures::{channel::mpsc, StreamExt}; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; +use search::project_search::ProjectSearchBar; use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; @@ -426,8 +427,8 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewCo toolbar.add_item(quick_action_bar, cx); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); - // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - // toolbar.add_item(project_search_bar, cx); + let project_search_bar = cx.build_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); // let lsp_log_item = // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); // toolbar.add_item(lsp_log_item, cx);