diff --git a/README.md b/README.md index 961c8f9ff3..2ee426a2a6 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,12 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea ``` sudo xcodebuild -license ``` - -* Install rustup (rust, cargo, etc.) - ``` - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` -* Install homebrew and node +* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.) ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install node + brew install node rustup-init + rustup-init # follow the installation steps ``` * Install postgres and configure the database diff --git a/assets/icons/select-all.svg b/assets/icons/select-all.svg new file mode 100644 index 0000000000..45a10bba42 --- /dev/null +++ b/assets/icons/select-all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d999872592..b31c9dcd1b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -16,7 +16,7 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, SelectionGoal, }; -use project::{FormatTrigger, Item as _, Project, ProjectPath}; +use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; use smallvec::SmallVec; use std::{ @@ -26,6 +26,7 @@ use std::{ iter, ops::Range, path::{Path, PathBuf}, + sync::Arc, }; use text::Selection; use util::{ @@ -978,7 +979,26 @@ impl SearchableItem for Editor { } self.change_selections(None, cx, |s| s.select_ranges(ranges)); } + fn replace( + &mut self, + identifier: &Self::Match, + query: &SearchQuery, + cx: &mut ViewContext, + ) { + let text = self.buffer.read(cx); + let text = text.snapshot(cx); + let text = text.text_for_range(identifier.clone()).collect::>(); + let text: Cow<_> = if text.len() == 1 { + text.first().cloned().unwrap().into() + } else { + let joined_chunks = text.join(""); + joined_chunks.into() + }; + if let Some(replacement) = query.replacement(&text) { + self.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + } + } fn match_index_for_direction( &mut self, matches: &Vec>, @@ -1030,7 +1050,7 @@ impl SearchableItem for Editor { fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>> { let buffer = self.buffer().read(cx).snapshot(cx); diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index a717223f6d..0b8a29e114 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -13,7 +13,7 @@ use gpui::{ use isahc::Request; use language::Buffer; use postage::prelude::Stream; -use project::Project; +use project::{search::SearchQuery, Project}; use regex::Regex; use serde::Serialize; use smallvec::SmallVec; @@ -418,10 +418,13 @@ impl SearchableItem for FeedbackEditor { self.editor .update(cx, |e, cx| e.select_matches(matches, cx)) } - + fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.replace(matches, query, cx)); + } fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task> { self.editor diff --git a/crates/gpui2_macros/src/styleable_helpers.rs b/crates/gpui2_macros/src/styleable_helpers.rs index ae7b2d3b1b..d5d76ff33b 100644 --- a/crates/gpui2_macros/src/styleable_helpers.rs +++ b/crates/gpui2_macros/src/styleable_helpers.rs @@ -28,24 +28,24 @@ fn generate_methods() -> Vec { let mut methods = Vec::new(); for (prefix, auto_allowed, fields) in box_prefixes() { - for (suffix, length_tokens) in box_suffixes() { + for (suffix, length_tokens, doc_string) in box_suffixes() { if auto_allowed || suffix != "auto" { - let method = generate_method(prefix, suffix, &fields, length_tokens); + let method = generate_method(prefix, suffix, &fields, length_tokens, doc_string); methods.push(method); } } } for (prefix, fields) in corner_prefixes() { - for (suffix, radius_tokens) in corner_suffixes() { - let method = generate_method(prefix, suffix, &fields, radius_tokens); + for (suffix, radius_tokens, doc_string) in corner_suffixes() { + let method = generate_method(prefix, suffix, &fields, radius_tokens, doc_string); methods.push(method); } } for (prefix, fields) in border_prefixes() { - for (suffix, width_tokens) in border_suffixes() { - let method = generate_method(prefix, suffix, &fields, width_tokens); + for (suffix, width_tokens, doc_string) in border_suffixes() { + let method = generate_method(prefix, suffix, &fields, width_tokens, doc_string); methods.push(method); } } @@ -58,6 +58,7 @@ fn generate_method( suffix: &'static str, fields: &Vec, length_tokens: TokenStream2, + doc_string: &'static str, ) -> TokenStream2 { let method_name = if suffix.is_empty() { format_ident!("{}", prefix) @@ -75,6 +76,7 @@ fn generate_method( .collect::>(); let method = quote! { + #[doc = #doc_string] fn #method_name(mut self) -> Self where Self: std::marker::Sized { let mut style = self.declared_style(); #(#field_assignments)* @@ -160,55 +162,52 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec)> { ] } -fn box_suffixes() -> Vec<(&'static str, TokenStream2)> { +fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { vec![ - ("0", quote! { pixels(0.) }), - ("0p5", quote! { rems(0.125) }), - ("1", quote! { rems(0.25) }), - ("1p5", quote! { rems(0.375) }), - ("2", quote! { rems(0.5) }), - ("2p5", quote! { rems(0.625) }), - ("3", quote! { rems(0.75) }), - ("3p5", quote! { rems(0.875) }), - ("4", quote! { rems(1.) }), - ("5", quote! { rems(1.25) }), - ("6", quote! { rems(1.5) }), - ("7", quote! { rems(1.75) }), - ("8", quote! { rems(2.0) }), - ("9", quote! { rems(2.25) }), - ("10", quote! { rems(2.5) }), - ("11", quote! { rems(2.75) }), - ("12", quote! { rems(3.) }), - ("16", quote! { rems(4.) }), - ("20", quote! { rems(5.) }), - ("24", quote! { rems(6.) }), - ("32", quote! { rems(8.) }), - ("40", quote! { rems(10.) }), - ("48", quote! { rems(12.) }), - ("56", quote! { rems(14.) }), - ("64", quote! { rems(16.) }), - ("72", quote! { rems(18.) }), - ("80", quote! { rems(20.) }), - ("96", quote! { rems(24.) }), - ("auto", quote! { auto() }), - ("px", quote! { pixels(1.) }), - ("full", quote! { relative(1.) }), - ("1_2", quote! { relative(0.5) }), - ("1_3", quote! { relative(1./3.) }), - ("2_3", quote! { relative(2./3.) }), - ("1_4", quote! { relative(0.25) }), - ("2_4", quote! { relative(0.5) }), - ("3_4", quote! { relative(0.75) }), - ("1_5", quote! { relative(0.2) }), - ("2_5", quote! { relative(0.4) }), - ("3_5", quote! { relative(0.6) }), - ("4_5", quote! { relative(0.8) }), - ("1_6", quote! { relative(1./6.) }), - ("5_6", quote! { relative(5./6.) }), - ("1_12", quote! { relative(1./12.) }), - // ("screen_50", quote! { DefiniteLength::Vh(50.0) }), - // ("screen_75", quote! { DefiniteLength::Vh(75.0) }), - // ("screen", quote! { DefiniteLength::Vh(100.0) }), + ("0", quote! { pixels(0.) }, "0px"), + ("0p5", quote! { rems(0.125) }, "2px (0.125rem)"), + ("1", quote! { rems(0.25) }, "4px (0.25rem)"), + ("1p5", quote! { rems(0.375) }, "6px (0.375rem)"), + ("2", quote! { rems(0.5) }, "8px (0.5rem)"), + ("2p5", quote! { rems(0.625) }, "10px (0.625rem)"), + ("3", quote! { rems(0.75) }, "12px (0.75rem)"), + ("3p5", quote! { rems(0.875) }, "14px (0.875rem)"), + ("4", quote! { rems(1.) }, "16px (1rem)"), + ("5", quote! { rems(1.25) }, "20px (1.25rem)"), + ("6", quote! { rems(1.5) }, "24px (1.5rem)"), + ("7", quote! { rems(1.75) }, "28px (1.75rem)"), + ("8", quote! { rems(2.0) }, "32px (2rem)"), + ("9", quote! { rems(2.25) }, "36px (2.25rem)"), + ("10", quote! { rems(2.5) }, "40px (2.5rem)"), + ("11", quote! { rems(2.75) }, "44px (2.75rem)"), + ("12", quote! { rems(3.) }, "48px (3rem)"), + ("16", quote! { rems(4.) }, "64px (4rem)"), + ("20", quote! { rems(5.) }, "80px (5rem)"), + ("24", quote! { rems(6.) }, "96px (6rem)"), + ("32", quote! { rems(8.) }, "128px (8rem)"), + ("40", quote! { rems(10.) }, "160px (10rem)"), + ("48", quote! { rems(12.) }, "192px (12rem)"), + ("56", quote! { rems(14.) }, "224px (14rem)"), + ("64", quote! { rems(16.) }, "256px (16rem)"), + ("72", quote! { rems(18.) }, "288px (18rem)"), + ("80", quote! { rems(20.) }, "320px (20rem)"), + ("96", quote! { rems(24.) }, "384px (24rem)"), + ("auto", quote! { auto() }, "Auto"), + ("px", quote! { pixels(1.) }, "1px"), + ("full", quote! { relative(1.) }, "100%"), + ("1_2", quote! { relative(0.5) }, "50% (1/2)"), + ("1_3", quote! { relative(1./3.) }, "33% (1/3)"), + ("2_3", quote! { relative(2./3.) }, "66% (2/3)"), + ("1_4", quote! { relative(0.25) }, "25% (1/4)"), + ("2_4", quote! { relative(0.5) }, "50% (2/4)"), + ("3_4", quote! { relative(0.75) }, "75% (3/4)"), + ("1_5", quote! { relative(0.2) }, "20% (1/5)"), + ("2_5", quote! { relative(0.4) }, "40% (2/5)"), + ("3_5", quote! { relative(0.6) }, "60% (3/5)"), + ("4_5", quote! { relative(0.8) }, "80% (4/5)"), + ("1_6", quote! { relative(1./6.) }, "16% (1/6)"), + ("5_6", quote! { relative(5./6.) }, "80% (5/6)"), + ("1_12", quote! { relative(1./12.) }, "8% (1/12)"), ] } @@ -258,16 +257,16 @@ fn corner_prefixes() -> Vec<(&'static str, Vec)> { ] } -fn corner_suffixes() -> Vec<(&'static str, TokenStream2)> { +fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { vec![ - ("none", quote! { pixels(0.) }), - ("sm", quote! { rems(0.125) }), - ("md", quote! { rems(0.25) }), - ("lg", quote! { rems(0.5) }), - ("xl", quote! { rems(0.75) }), - ("2xl", quote! { rems(1.) }), - ("3xl", quote! { rems(1.5) }), - ("full", quote! { pixels(9999.) }), + ("none", quote! { pixels(0.) }, "0px"), + ("sm", quote! { rems(0.125) }, "2px (0.125rem)"), + ("md", quote! { rems(0.25) }, "4px (0.25rem)"), + ("lg", quote! { rems(0.5) }, "8px (0.5rem)"), + ("xl", quote! { rems(0.75) }, "12px (0.75rem)"), + ("2xl", quote! { rems(1.) }, "16px (1rem)"), + ("3xl", quote! { rems(1.5) }, "24px (1.5rem)"), + ("full", quote! { pixels(9999.) }, "9999px"), ] } @@ -303,25 +302,25 @@ fn border_prefixes() -> Vec<(&'static str, Vec)> { ] } -fn border_suffixes() -> Vec<(&'static str, TokenStream2)> { +fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { vec![ - ("", quote! { pixels(1.) }), - ("0", quote! { pixels(0.) }), - ("1", quote! { pixels(1.) }), - ("2", quote! { pixels(2.) }), - ("3", quote! { pixels(3.) }), - ("4", quote! { pixels(4.) }), - ("5", quote! { pixels(5.) }), - ("6", quote! { pixels(6.) }), - ("7", quote! { pixels(7.) }), - ("8", quote! { pixels(8.) }), - ("9", quote! { pixels(9.) }), - ("10", quote! { pixels(10.) }), - ("11", quote! { pixels(11.) }), - ("12", quote! { pixels(12.) }), - ("16", quote! { pixels(16.) }), - ("20", quote! { pixels(20.) }), - ("24", quote! { pixels(24.) }), - ("32", quote! { pixels(32.) }), + ("", quote! { pixels(1.)}, "1px"), + ("0", quote! { pixels(0.)}, "0px"), + ("1", quote! { pixels(1.) }, "1px"), + ("2", quote! { pixels(2.) }, "2px"), + ("3", quote! { pixels(3.) }, "3px"), + ("4", quote! { pixels(4.) }, "4px"), + ("5", quote! { pixels(5.) }, "5px"), + ("6", quote! { pixels(6.) }, "6px"), + ("7", quote! { pixels(7.) }, "7px"), + ("8", quote! { pixels(8.) }, "8px"), + ("9", quote! { pixels(9.) }, "9px"), + ("10", quote! { pixels(10.) }, "10px"), + ("11", quote! { pixels(11.) }, "11px"), + ("12", quote! { pixels(12.) }, "12px"), + ("16", quote! { pixels(16.) }, "16px"), + ("20", quote! { pixels(20.) }, "20px"), + ("24", quote! { pixels(24.) }, "24px"), + ("32", quote! { pixels(32.) }, "32px"), ] } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index a918e3d151..587e6ed25a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Buffer, LanguageServerId, LanguageServerName}; use lsp::IoKind; -use project::{Project, Worktree}; +use project::{search::SearchQuery, Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; use workspace::{ @@ -524,12 +524,24 @@ impl SearchableItem for LspLogView { fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> gpui::Task> { self.editor.update(cx, |e, cx| e.find_matches(query, cx)) } + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Since LSP Log is read-only, it doesn't make sense to support replace operation. + } + fn supported_options() -> workspace::searchable::SearchOptions { + workspace::searchable::SearchOptions { + case: true, + word: true, + regex: true, + // LSP log is read-only. + replacement: false, + } + } fn active_match_index( &mut self, matches: Vec, diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 6c53d2e934..bf81158701 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -7,6 +7,7 @@ use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; use std::{ + borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, path::{Path, PathBuf}, @@ -35,6 +36,7 @@ impl SearchInputs { pub enum SearchQuery { Text { search: Arc>, + replacement: Option, whole_word: bool, case_sensitive: bool, inner: SearchInputs, @@ -42,7 +44,7 @@ pub enum SearchQuery { Regex { regex: Regex, - + replacement: Option, multiline: bool, whole_word: bool, case_sensitive: bool, @@ -95,6 +97,7 @@ impl SearchQuery { }; Self::Text { search: Arc::new(search), + replacement: None, whole_word, case_sensitive, inner, @@ -130,6 +133,7 @@ impl SearchQuery { }; Ok(Self::Regex { regex, + replacement: None, multiline, whole_word, case_sensitive, @@ -156,7 +160,21 @@ impl SearchQuery { )) } } - + pub fn with_replacement(mut self, new_replacement: Option) -> Self { + match self { + Self::Text { + ref mut replacement, + .. + } + | Self::Regex { + ref mut replacement, + .. + } => { + *replacement = new_replacement; + self + } + } + } pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { proto::SearchProject { project_id, @@ -214,7 +232,20 @@ impl SearchQuery { } } } - + pub fn replacement<'a>(&self, text: &'a str) -> Option> { + match self { + SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from), + SearchQuery::Regex { + regex, replacement, .. + } => { + if let Some(replacement) = replacement { + Some(regex.replace(text, replacement)) + } else { + None + } + } + } + } pub async fn search( &self, buffer: &BufferSnapshot, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 78729df936..6a227812d1 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2,19 +2,16 @@ use crate::{ history::SearchHistory, mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, + SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, + ToggleWholeWord, }; use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{ - actions, - elements::*, - impl_actions, - platform::{CursorStyle, MouseButton}, - Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle, - WindowContext, + actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription, + Task, View, ViewContext, ViewHandle, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -54,6 +51,11 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::previous_history_query); cx.add_action(BufferSearchBar::cycle_mode); cx.add_action(BufferSearchBar::cycle_mode_on_pane); + cx.add_action(BufferSearchBar::replace_all); + cx.add_action(BufferSearchBar::replace_next); + cx.add_action(BufferSearchBar::replace_all_on_pane); + cx.add_action(BufferSearchBar::replace_next_on_pane); + cx.add_action(BufferSearchBar::toggle_replace); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); } @@ -73,9 +75,11 @@ fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContex pub struct BufferSearchBar { query_editor: ViewHandle, + replacement_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, + active_search: Option>, searchable_items_with_matches: HashMap, Vec>>, pending_search: Option>, @@ -85,6 +89,7 @@ pub struct BufferSearchBar { dismissed: bool, search_history: SearchHistory, current_mode: SearchMode, + replace_is_active: bool, } impl Entity for BufferSearchBar { @@ -156,6 +161,9 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); + self.replacement_editor.update(cx, |editor, cx| { + editor.set_placeholder_text("Replace with...", cx); + }); let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; @@ -212,7 +220,6 @@ impl View for BufferSearchBar { cx, ) }; - let query_column = Flex::row() .with_child( Svg::for_style(theme.search.editor_icon.clone().icon) @@ -243,7 +250,57 @@ impl View for BufferSearchBar { .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let should_show_replace_input = self.replace_is_active && supported_options.replacement; + let replacement = should_show_replace_input.then(|| { + Flex::row() + .with_child( + Svg::for_style(theme.search.replace_icon.clone().icon) + .contained() + .with_style(theme.search.replace_icon.clone().container), + ) + .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true)) + .align_children_center() + .flex(1., true) + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false) + }); + let replace_all = should_show_replace_input.then(|| { + super::replace_action( + ReplaceAll, + "Replace all", + "icons/replace_all.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let replace_next = should_show_replace_input.then(|| { + super::replace_action( + ReplaceNext, + "Replace next", + "icons/replace_next.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let switches_column = supported_options.replacement.then(|| { + Flex::row() + .align_children_center() + .with_child(super::toggle_replace_button( + self.replace_is_active, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .contained() + .with_style(theme.search.option_button_group) + }); let mode_column = Flex::row() .with_child(search_button_for_mode( SearchMode::Text, @@ -261,7 +318,10 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height); let nav_column = Flex::row() - .with_child(self.render_action_button("all", cx)) + .align_children_center() + .with_children(replace_next) + .with_children(replace_all) + .with_child(self.render_action_button("icons/select-all.svg", cx)) .with_child(Flex::row().with_children(match_count)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) @@ -271,6 +331,8 @@ impl View for BufferSearchBar { Flex::row() .with_child(query_column) + .with_children(switches_column) + .with_children(replacement) .with_child(mode_column) .with_child(nav_column) .contained() @@ -345,9 +407,18 @@ impl BufferSearchBar { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - + let replacement_editor = cx.add_view(|cx| { + Editor::auto_height( + 2, + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ) + }); + // cx.subscribe(&replacement_editor, Self::on_query_editor_event) + // .detach(); Self { query_editor, + replacement_editor, active_searchable_item: None, active_searchable_item_subscription: None, active_match_index: None, @@ -359,6 +430,8 @@ impl BufferSearchBar { dismissed: true, search_history: SearchHistory::default(), current_mode: SearchMode::default(), + active_search: None, + replace_is_active: false, } } @@ -441,7 +514,9 @@ impl BufferSearchBar { pub fn query(&self, cx: &WindowContext) -> String { self.query_editor.read(cx).text(cx) } - + pub fn replacement(&self, cx: &WindowContext) -> String { + self.replacement_editor.read(cx).text(cx) + } pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { self.active_searchable_item .as_ref() @@ -477,37 +552,16 @@ impl BufferSearchBar { ) -> AnyElement { let tooltip = "Select All Matches"; let tooltip_style = theme::current(cx).tooltip.clone(); - let action_type_id = 0_usize; - let has_matches = self.active_match_index.is_some(); - let cursor_style = if has_matches { - CursorStyle::PointingHand - } else { - CursorStyle::default() - }; - enum ActionButton {} - MouseEventHandler::new::(action_type_id, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .action_button - .in_state(has_matches) - .style_for(state); - Label::new(icon, style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.select_all_matches(&SelectAllMatches, cx) - }) - .with_cursor_style(cursor_style) - .with_tooltip::( - action_type_id, - tooltip.to_string(), - Some(Box::new(SelectAllMatches)), - tooltip_style, - cx, - ) + + let theme = theme::current(cx); + let style = theme.search.action_button.clone(); + + gpui::elements::Component::element(SafeStylable::with_style( + theme::components::action_button::Button::action(SelectAllMatches) + .with_tooltip(tooltip, tooltip_style) + .with_contents(theme::components::svg::Svg::new(icon)), + style, + )) .into_any() } @@ -688,6 +742,7 @@ impl BufferSearchBar { let (done_tx, done_rx) = oneshot::channel(); let query = self.query(cx); self.pending_search.take(); + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); @@ -695,7 +750,7 @@ impl BufferSearchBar { let _ = done_tx.send(()); cx.notify(); } else { - let query = if self.current_mode == SearchMode::Regex { + let query: Arc<_> = if self.current_mode == SearchMode::Regex { match SearchQuery::regex( query, self.search_options.contains(SearchOptions::WHOLE_WORD), @@ -703,7 +758,8 @@ impl BufferSearchBar { Vec::new(), Vec::new(), ) { - Ok(query) => query, + Ok(query) => query + .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())), Err(_) => { self.query_contains_error = true; cx.notify(); @@ -718,8 +774,10 @@ impl BufferSearchBar { Vec::new(), Vec::new(), ) - }; - + .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())) + } + .into(); + self.active_search = Some(query.clone()); let query_text = query.as_str().to_string(); let matches = active_searchable_item.find_matches(query, cx); @@ -810,6 +868,63 @@ impl BufferSearchBar { cx.propagate_action(); } } + fn toggle_replace(&mut self, _: &ToggleReplace, _: &mut ViewContext) { + if let Some(_) = &self.active_searchable_item { + self.replace_is_active = !self.replace_is_active; + } + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query.as_ref().clone().with_replacement( + Some(self.replacement(cx)).filter(|rep| !rep.is_empty()), + ); + searchable_item.replace(&matches[active_index], &query, cx); + } + + self.focus_editor(&FocusEditor, cx); + } + } + } + } + } + fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query.as_ref().clone().with_replacement( + Some(self.replacement(cx)).filter(|rep| !rep.is_empty()), + ); + for m in matches { + searchable_item.replace(m, &query, cx); + } + + self.focus_editor(&FocusEditor, cx); + } + } + } + } + } + fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_next(action, cx)); + } + } + fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_all(action, cx)); + } + } } #[cfg(test)] @@ -1539,4 +1654,109 @@ mod tests { assert_eq!(search_bar.search_options, SearchOptions::NONE); }); } + #[gpui::test] + async fn test_replace_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("expression", None, cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. + editor.set_text("expr$1", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex or regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + + // Search for word boundaries and replace just a single one. + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("banana", cx); + }); + search_bar.replace_next(&ReplaceNext, cx) + }); + // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Let's turn on regex mode. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("\\[([^\\]]+)\\]", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("${1}number", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Now with a whole-word twist. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("things", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + // The only word affected by this edit should be `algorithms`, even though there's a bunch + // of words in this text that would match this regex if not for WHOLE_WORD. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching things + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 47f7f485c4..0135ed4eed 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -27,6 +29,7 @@ actions!( CycleMode, ToggleWholeWord, ToggleCaseSensitive, + ToggleReplace, SelectNextMatch, SelectPrevMatch, SelectAllMatches, @@ -34,7 +37,9 @@ actions!( PreviousHistoryQuery, ActivateTextMode, ActivateSemanticMode, - ActivateRegexMode + ActivateRegexMode, + ReplaceAll, + ReplaceNext ] ); @@ -98,3 +103,32 @@ impl SearchOptions { .into_any() } } + +fn toggle_replace_button( + active: bool, + tooltip_style: TooltipStyle, + button_style: ToggleIconButtonStyle, +) -> AnyElement { + Button::dynamic_action(Box::new(ToggleReplace)) + .with_tooltip("Toggle replace", tooltip_style) + .with_contents(theme::components::svg::Svg::new("icons/replace.svg")) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() +} + +fn replace_action( + action: impl Action, + name: &'static str, + icon_path: &'static str, + tooltip_style: TooltipStyle, + button_style: IconButtonStyle, +) -> AnyElement { + Button::dynamic_action(Box::new(action)) + .with_tooltip(name, tooltip_style) + .with_contents(theme::components::svg::Svg::new(icon_path)) + .with_style(button_style) + .element() + .into_any() +} diff --git a/crates/storybook/src/components.rs b/crates/storybook/src/components.rs index 1aafefc1a6..d07c2651a0 100644 --- a/crates/storybook/src/components.rs +++ b/crates/storybook/src/components.rs @@ -4,6 +4,12 @@ use gpui2::{ }; use std::{marker::PhantomData, rc::Rc}; +mod icon_button; +mod tab; + +pub(crate) use icon_button::{icon_button, ButtonVariant}; +pub(crate) use tab::tab; + struct ButtonHandlers { click: Option)>>, } diff --git a/crates/storybook/src/components/icon_button.rs b/crates/storybook/src/components/icon_button.rs new file mode 100644 index 0000000000..0a9b2ca285 --- /dev/null +++ b/crates/storybook/src/components/icon_button.rs @@ -0,0 +1,50 @@ +use crate::theme::theme; +use gpui2::elements::svg; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub(crate) struct IconButton { + path: &'static str, + variant: ButtonVariant, +} + +#[derive(PartialEq)] +pub enum ButtonVariant { + Ghost, + Filled, +} + +pub fn icon_button(path: &'static str, variant: ButtonVariant) -> impl Element { + IconButton { path, variant } +} + +impl IconButton { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + let mut div = div(); + if self.variant == ButtonVariant::Filled { + div = div.fill(theme.highest.on.default.background); + } + + div.w_7() + .h_6() + .flex() + .items_center() + .justify_center() + .rounded_md() + .hover() + .fill(theme.highest.base.hovered.background) + .active() + .fill(theme.highest.base.pressed.background) + .child( + svg() + .path(self.path) + .w_4() + .h_4() + .fill(theme.highest.variant.default.foreground), + ) + } +} diff --git a/crates/storybook/src/components/tab.rs b/crates/storybook/src/components/tab.rs new file mode 100644 index 0000000000..b945e113e5 --- /dev/null +++ b/crates/storybook/src/components/tab.rs @@ -0,0 +1,55 @@ +use crate::theme::theme; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub(crate) struct Tab { + title: &'static str, + enabled: bool, +} + +pub fn tab(title: &'static str, enabled: bool) -> impl Element { + Tab { title, enabled } +} + +impl Tab { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .px_2() + .py_0p5() + .flex() + .items_center() + .justify_center() + .rounded_lg() + .fill(if self.enabled { + theme.highest.on.default.background + } else { + theme.highest.base.default.background + }) + .hover() + .fill(if self.enabled { + theme.highest.on.hovered.background + } else { + theme.highest.base.hovered.background + }) + .active() + .fill(if self.enabled { + theme.highest.on.pressed.background + } else { + theme.highest.base.pressed.background + }) + .child( + div() + .text_sm() + .text_color(if self.enabled { + theme.highest.base.default.foreground + } else { + theme.highest.variant.default.foreground + }) + .child(self.title), + ) + } +} diff --git a/crates/storybook/src/modules.rs b/crates/storybook/src/modules.rs new file mode 100644 index 0000000000..bc8ba73b08 --- /dev/null +++ b/crates/storybook/src/modules.rs @@ -0,0 +1,3 @@ +mod tab_bar; + +pub(crate) use tab_bar::tab_bar; diff --git a/crates/storybook/src/modules/tab_bar.rs b/crates/storybook/src/modules/tab_bar.rs new file mode 100644 index 0000000000..06029c5dc2 --- /dev/null +++ b/crates/storybook/src/modules/tab_bar.rs @@ -0,0 +1,82 @@ +use std::marker::PhantomData; + +use crate::components::{icon_button, tab, ButtonVariant}; +use crate::theme::theme; +use gpui2::elements::div::ScrollState; +use gpui2::style::StyleHelpers; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct TabBar { + view_type: PhantomData, + scroll_state: ScrollState, +} + +pub fn tab_bar(scroll_state: ScrollState) -> TabBar { + TabBar { + view_type: PhantomData, + scroll_state, + } +} + +impl TabBar { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .w_full() + .flex() + // Left Side + .child( + div() + .px_1() + .flex() + .flex_none() + .gap_2() + // Nav Buttons + .child( + div() + .flex() + .items_center() + .gap_px() + .child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled)) + .child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)), + ), + ) + .child( + div().w_0().flex_1().h_full().child( + div() + .flex() + .gap_px() + .overflow_x_scroll(self.scroll_state.clone()) + .child(tab("Cargo.toml", false)) + .child(tab("Channels Panel", true)) + .child(tab("channels_panel.rs", false)) + .child(tab("workspace.rs", false)) + .child(tab("icon_button.rs", false)) + .child(tab("storybook.rs", false)) + .child(tab("theme.rs", false)) + .child(tab("theme_registry.rs", false)) + .child(tab("styleable_helpers.rs", false)), + ), + ) + // Right Side + .child( + div() + .px_1() + .flex() + .flex_none() + .gap_2() + // Nav Buttons + .child( + div() + .flex() + .items_center() + .gap_px() + .child(icon_button("icons/plus.svg", ButtonVariant::Ghost)) + .child(icon_button("icons/split.svg", ButtonVariant::Ghost)), + ), + ) + } +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 04e1038988..1b40bc2dc4 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -12,6 +12,7 @@ use simplelog::SimpleLogger; mod collab_panel; mod components; mod element_ext; +mod modules; mod theme; mod workspace; @@ -34,13 +35,13 @@ fn main() { cx.add_window( gpui2::WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1400., 900.))), + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1600., 900.))), center: true, ..Default::default() }, |cx| { view(|cx| { - cx.enable_inspector(); + // cx.enable_inspector(); storybook(&mut ViewContext::new(cx)) }) }, diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs index d9f9c22fcb..c37b3f16ea 100644 --- a/crates/storybook/src/workspace.rs +++ b/crates/storybook/src/workspace.rs @@ -1,4 +1,4 @@ -use crate::{collab_panel::collab_panel, theme::theme}; +use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme}; use gpui2::{ elements::{div, div::ScrollState, img, svg}, style::{StyleHelpers, Styleable}, @@ -9,6 +9,7 @@ use gpui2::{ struct WorkspaceElement { left_scroll_state: ScrollState, right_scroll_state: ScrollState, + tab_bar_scroll_state: ScrollState, } pub fn workspace() -> impl Element { @@ -38,7 +39,19 @@ impl WorkspaceElement { .flex_row() .overflow_hidden() .child(collab_panel(self.left_scroll_state.clone())) - .child(div().h_full().flex_1()) + .child( + div() + .h_full() + .flex_1() + .fill(theme.highest.base.default.background) + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(tab_bar(self.tab_bar_scroll_state.clone())), + ), + ) .child(collab_panel(self.right_scroll_state.clone())), ) .child(statusbar()) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a12f9d3c3c..b79f655f81 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -18,7 +18,7 @@ use gpui::{ ViewHandle, WeakViewHandle, }; use language::Bias; -use project::{LocalWorktree, Project}; +use project::{search::SearchQuery, LocalWorktree, Project}; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; use smol::Timer; @@ -26,6 +26,7 @@ use std::{ borrow::Cow, ops::RangeInclusive, path::{Path, PathBuf}, + sync::Arc, time::Duration, }; use terminal::{ @@ -380,10 +381,10 @@ impl TerminalView { pub fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>> { - let searcher = regex_search_for_query(query); + let searcher = regex_search_for_query(&query); if let Some(searcher) = searcher { self.terminal @@ -486,7 +487,7 @@ fn possible_open_targets( .collect() } -pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { +pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let query = query.as_str(); let searcher = RegexSearch::new(&query); searcher.ok() @@ -798,6 +799,7 @@ impl SearchableItem for TerminalView { case: false, word: false, regex: false, + replacement: false, } } @@ -851,10 +853,10 @@ impl SearchableItem for TerminalView { /// Get all of the matches for this query, should be done on the background fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task> { - if let Some(searcher) = regex_search_for_query(query) { + if let Some(searcher) = regex_search_for_query(&query) { self.terminal() .update(cx, |term, cx| term.find_matches(searcher, cx)) } else { @@ -898,6 +900,9 @@ impl SearchableItem for TerminalView { res } + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Replacement is not supported in terminal view, so this is a no-op. + } } ///Get's the working directory for the given workspace, respecting the user's settings. diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e924fe2124..a845db3ba4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,9 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{ + action_button::ButtonStyle, disclosure::DisclosureStyle, IconButtonStyle, ToggleIconButtonStyle, +}; use gpui::{ color::Color, elements::{Border, ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -440,9 +442,7 @@ pub struct Search { pub include_exclude_editor: FindEditor, pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, - pub option_button: Toggleable>, pub option_button_component: ToggleIconButtonStyle, - pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, pub major_results_status: TextStyle, @@ -454,6 +454,10 @@ pub struct Search { pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, + pub replace_icon: IconStyle, + // Used for filters and replace + pub option_button: Toggleable>, + pub action_button: IconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 7a470db7c9..ddde5c3554 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -1,4 +1,4 @@ -use std::any::Any; +use std::{any::Any, sync::Arc}; use gpui::{ AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle, @@ -25,6 +25,8 @@ pub struct SearchOptions { pub case: bool, pub word: bool, pub regex: bool, + /// Specifies whether the item supports search & replace. + pub replacement: bool, } pub trait SearchableItem: Item { @@ -35,6 +37,7 @@ pub trait SearchableItem: Item { case: true, word: true, regex: true, + replacement: true, } } fn to_search_event( @@ -52,6 +55,7 @@ pub trait SearchableItem: Item { cx: &mut ViewContext, ); fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); fn match_index_for_direction( &mut self, matches: &Vec, @@ -74,7 +78,7 @@ pub trait SearchableItem: Item { } fn find_matches( &mut self, - query: SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>; fn active_match_index( @@ -103,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle { cx: &mut WindowContext, ); fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); fn match_index_for_direction( &self, matches: &Vec>, @@ -113,7 +118,7 @@ pub trait SearchableItemHandle: ItemHandle { ) -> usize; fn find_matches( &self, - query: SearchQuery, + query: Arc, cx: &mut WindowContext, ) -> Task>>; fn active_match_index( @@ -189,7 +194,7 @@ impl SearchableItemHandle for ViewHandle { } fn find_matches( &self, - query: SearchQuery, + query: Arc, cx: &mut WindowContext, ) -> Task>> { let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); @@ -209,6 +214,11 @@ impl SearchableItemHandle for ViewHandle { let matches = downcast_matches(matches); self.update(cx, |this, cx| this.active_match_index(matches, cx)) } + + fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { + let matches = matches.downcast_ref().unwrap(); + self.update(cx, |this, cx| this.replace(matches, query, cx)) + } } fn downcast_matches(matches: &Vec>) -> Vec { diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 8174690fde..bc95b91819 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -30,9 +30,6 @@ export default function search(): any { selection: theme.players[0], text: text(theme.highest, "mono", "default"), border: border(theme.highest), - margin: { - right: SEARCH_ROW_SPACING, - }, padding: { top: 4, bottom: 4, @@ -125,7 +122,7 @@ export default function search(): any { button_width: 32, background: background(theme.highest, "on"), - corner_radius: 2, + corner_radius: 6, margin: { right: 2 }, border: { width: 1, @@ -185,26 +182,6 @@ export default function search(): any { }, }, }), - // Search tool buttons - // HACK: This is not how disabled elements should be created - // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled - action_button: toggleable({ - state: { - inactive: text_button({ - variant: "ghost", - layer: theme.highest, - disabled: true, - margin: { right: SEARCH_ROW_SPACING }, - text_properties: { size: "sm" }, - }), - active: text_button({ - variant: "ghost", - layer: theme.highest, - margin: { right: SEARCH_ROW_SPACING }, - text_properties: { size: "sm" }, - }), - }, - }), editor, invalid_editor: { ...editor, @@ -218,6 +195,7 @@ export default function search(): any { match_index: { ...text(theme.highest, "mono", { size: "sm" }), padding: { + left: SEARCH_ROW_SPACING, right: SEARCH_ROW_SPACING, }, }, @@ -398,6 +376,59 @@ export default function search(): any { search_row_spacing: 8, option_button_height: 22, modes_container: {}, + replace_icon: { + icon: { + color: foreground(theme.highest, "disabled"), + asset: "icons/replace.svg", + dimensions: { + width: 14, + height: 14, + }, + }, + container: { + margin: { right: 4 }, + padding: { left: 1, right: 1 }, + }, + }, + action_button: interactive({ + base: { + icon_size: 14, + color: foreground(theme.highest, "variant"), + + button_width: 32, + background: background(theme.highest, "on"), + corner_radius: 6, + margin: { right: 2 }, + border: { + width: 1, + color: background(theme.highest, "on"), + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1, + color: background(theme.highest, "on", "hovered"), + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1, + color: background(theme.highest, "on", "pressed"), + }, + }, + }, + }), ...search_results(), } }