diff --git a/assets/icons/case_insensitive_12.svg b/assets/icons/case_insensitive_12.svg new file mode 100644 index 0000000000..8c943e7509 --- /dev/null +++ b/assets/icons/case_insensitive_12.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/filter_12.svg b/assets/icons/filter_12.svg new file mode 100644 index 0000000000..9c1ad5ba5c --- /dev/null +++ b/assets/icons/filter_12.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/filter_14.svg b/assets/icons/filter_14.svg new file mode 100644 index 0000000000..379be15b51 --- /dev/null +++ b/assets/icons/filter_14.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/word_search_12.svg b/assets/icons/word_search_12.svg new file mode 100644 index 0000000000..4cf6401fd2 --- /dev/null +++ b/assets/icons/word_search_12.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/word_search_14.svg b/assets/icons/word_search_14.svg new file mode 100644 index 0000000000..adb4976bcc --- /dev/null +++ b/assets/icons/word_search_14.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index f4d36ee95b..3ec994335e 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -225,7 +225,8 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", - "alt-enter": "search::SelectAllMatches" + "alt-enter": "search::SelectAllMatches", + "alt-tab": "search::CycleMode" } }, { @@ -238,7 +239,8 @@ { "context": "ProjectSearchBar", "bindings": { - "escape": "project_search::ToggleFocus" + "escape": "project_search::ToggleFocus", + "alt-tab": "search::CycleMode" } }, { @@ -251,7 +253,8 @@ { "context": "ProjectSearchView", "bindings": { - "escape": "project_search::ToggleFocus" + "escape": "project_search::ToggleFocus", + "alt-tab": "search::CycleMode" } }, { @@ -263,7 +266,8 @@ "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex" + "alt-tab": "search::CycleMode", + "alt-cmd-f": "project_search::ToggleFilters" } }, // Bindings from VS Code diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 70473cbc7f..e5026182ed 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -158,7 +158,7 @@ impl AssistantPanel { }); let toolbar = cx.add_view(|cx| { - let mut toolbar = Toolbar::new(None); + let mut toolbar = Toolbar::new(); toolbar.set_can_navigate(false, cx); toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); toolbar diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b08d9501f6..ca5c2fb8b5 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3313,11 +3313,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { &mut self, element_id: usize, initial: T, + ) -> ElementStateHandle { + self.element_state_dynamic(TypeTag::new::(), element_id, initial) + } + + pub fn element_state_dynamic( + &mut self, + tag: TypeTag, + element_id: usize, + initial: T, ) -> ElementStateHandle { let id = ElementStateId { view_id: self.view_id(), element_id, - tag: TypeId::of::(), + tag, }; self.element_states .entry(id) @@ -3331,11 +3340,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { ) -> ElementStateHandle { self.element_state::(element_id, T::default()) } + + pub fn default_element_state_dynamic( + &mut self, + tag: TypeTag, + element_id: usize, + ) -> ElementStateHandle { + self.element_state_dynamic::(tag, element_id, T::default()) + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct TypeTag { tag: TypeId, + composed: Option, #[cfg(debug_assertions)] tag_type_name: &'static str, } @@ -3344,6 +3362,7 @@ impl TypeTag { pub fn new() -> Self { Self { tag: TypeId::of::(), + composed: None, #[cfg(debug_assertions)] tag_type_name: std::any::type_name::(), } @@ -3352,11 +3371,17 @@ impl TypeTag { pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self { Self { tag, + composed: None, #[cfg(debug_assertions)] tag_type_name: type_name, } } + pub fn compose(mut self, other: TypeTag) -> Self { + self.composed = Some(other.tag); + self + } + #[cfg(debug_assertions)] pub(crate) fn type_name(&self) -> &'static str { self.tag_type_name @@ -4751,7 +4776,7 @@ impl Hash for AnyWeakViewHandle { pub struct ElementStateId { view_id: usize, element_id: usize, - tag: TypeId, + tag: TypeTag, } pub struct ElementStateHandle { diff --git a/crates/gpui/src/app/action.rs b/crates/gpui/src/app/action.rs index c6b43e489b..23eb4da730 100644 --- a/crates/gpui/src/app/action.rs +++ b/crates/gpui/src/app/action.rs @@ -1,10 +1,13 @@ use std::any::{Any, TypeId}; +use crate::TypeTag; + pub trait Action: 'static { fn id(&self) -> TypeId; fn namespace(&self) -> &'static str; fn name(&self) -> &'static str; fn as_any(&self) -> &dyn Any; + fn type_tag(&self) -> TypeTag; fn boxed_clone(&self) -> Box; fn eq(&self, other: &dyn Action) -> bool; @@ -107,6 +110,10 @@ macro_rules! __impl_action { } } + fn type_tag(&self) -> $crate::TypeTag { + $crate::TypeTag::new::() + } + $from_json_fn } }; diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index f1be9b34ae..03caae8dd9 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -34,8 +34,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext, - WeakViewHandle, WindowContext, + json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View, + ViewContext, WeakViewHandle, WindowContext, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -172,6 +172,20 @@ pub trait Element: 'static { FlexItem::new(self.into_any()).float() } + fn with_dynamic_tooltip( + self, + tag: TypeTag, + id: usize, + text: impl Into>, + action: Option>, + style: TooltipStyle, + cx: &mut ViewContext, + ) -> Tooltip + where + Self: 'static + Sized, + { + Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx) + } fn with_tooltip( self, id: usize, diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 2f9cc6cce6..a26355a539 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use pathfinder_geometry::{rect::RectF, vector::Vector2F}; use crate::{ @@ -7,6 +5,34 @@ use crate::{ ViewContext, }; +use super::Empty; + +pub trait GeneralComponent { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; +} + +pub trait StyleableComponent { + type Style: Clone; + type Output: GeneralComponent; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +impl GeneralComponent for () { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl StyleableComponent for () { + type Style = (); + type Output = (); + + fn with_style(self, _: Self::Style) -> Self::Output { + () + } +} + pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; @@ -18,22 +44,32 @@ pub trait Component { } } -pub struct ComponentAdapter { - component: Option, - phantom: PhantomData, +impl Component for C { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(v, cx) + } } -impl ComponentAdapter { +pub struct ComponentAdapter { + component: Option, + element: Option>, + #[cfg(debug_assertions)] + _component_name: &'static str, +} + +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), - phantom: PhantomData, + element: None, + #[cfg(debug_assertions)] + _component_name: std::any::type_name::(), } } } impl + 'static> Element for ComponentAdapter { - type LayoutState = AnyElement; + type LayoutState = (); type PaintState = (); @@ -43,10 +79,12 @@ impl + 'static> Element for ComponentAdapter { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let component = self.component.take().unwrap(); - let mut element = component.render(view, cx.view_context()); - let constraint = element.layout(constraint, view, cx); - (constraint, element) + if self.element.is_none() { + let component = self.component.take().unwrap(); + self.element = Some(component.render(view, cx.view_context())); + } + let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx); + (constraint, ()) } fn paint( @@ -54,11 +92,14 @@ impl + 'static> Element for ComponentAdapter { scene: &mut SceneBuilder, bounds: RectF, visible_bounds: RectF, - layout: &mut Self::LayoutState, + _: &mut Self::LayoutState, view: &mut V, cx: &mut PaintContext, ) -> Self::PaintState { - layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + self.element + .as_mut() + .unwrap() + .paint(scene, bounds.origin(), visible_bounds, view, cx) } fn rect_for_text_range( @@ -66,25 +107,35 @@ impl + 'static> Element for ComponentAdapter { range_utf16: std::ops::Range, _: RectF, _: RectF, - element: &Self::LayoutState, + _: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> Option { - element.rect_for_text_range(range_utf16, view, cx) + self.element + .as_ref() + .unwrap() + .rect_for_text_range(range_utf16, view, cx) } fn debug( &self, _: RectF, - element: &Self::LayoutState, + _: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> serde_json::Value { + #[cfg(debug_assertions)] + let component_name = self._component_name; + + #[cfg(not(debug_assertions))] + let component_name = "Unknown"; + serde_json::json!({ "type": "ComponentAdapter", - "child": element.debug(view, cx), + "child": self.element.as_ref().unwrap().debug(view, cx), + "component_name": component_name }) } } diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 0ba0110303..0ce34fcc14 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -7,7 +7,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::json, Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, - Task, View, ViewContext, + Task, TypeTag, View, ViewContext, }; use schemars::JsonSchema; use serde::Deserialize; @@ -61,11 +61,23 @@ impl Tooltip { child: AnyElement, cx: &mut ViewContext, ) -> Self { - struct ElementState(Tag); - struct MouseEventHandlerState(Tag); + Self::new_dynamic(TypeTag::new::(), id, text, action, style, child, cx) + } + + pub fn new_dynamic( + mut tag: TypeTag, + id: usize, + text: impl Into>, + action: Option>, + style: TooltipStyle, + child: AnyElement, + cx: &mut ViewContext, + ) -> Self { + tag = tag.compose(TypeTag::new::()); + let focused_view_id = cx.focused_view_id(); - let state_handle = cx.default_element_state::, Rc>(id); + let state_handle = cx.default_element_state_dynamic::>(tag, id); let state = state_handle.read(cx).clone(); let text = text.into(); @@ -95,7 +107,7 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::new::, _>(id, cx, |_, _| child) + let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child) .on_hover(move |e, _, cx| { let position = e.position; if e.started { diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 71a0b70b81..08ff803598 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -13,24 +13,39 @@ use std::{ sync::Arc, }; +#[derive(Clone, Debug)] +pub struct SearchInputs { + query: Arc, + files_to_include: Vec, + files_to_exclude: Vec, +} + +impl SearchInputs { + pub fn as_str(&self) -> &str { + self.query.as_ref() + } + pub fn files_to_include(&self) -> &[PathMatcher] { + &self.files_to_include + } + pub fn files_to_exclude(&self) -> &[PathMatcher] { + &self.files_to_exclude + } +} #[derive(Clone, Debug)] pub enum SearchQuery { Text { search: Arc>, - query: Arc, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + inner: SearchInputs, }, Regex { regex: Regex, - query: Arc, + multiline: bool, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + inner: SearchInputs, }, } @@ -72,13 +87,16 @@ impl SearchQuery { .auto_configure(&[&query]) .ascii_case_insensitive(!case_sensitive) .build(&[&query]); + let inner = SearchInputs { + query: query.into(), + files_to_exclude, + files_to_include, + }; Self::Text { search: Arc::new(search), - query: Arc::from(query), whole_word, case_sensitive, - files_to_include, - files_to_exclude, + inner, } } @@ -104,14 +122,17 @@ impl SearchQuery { .case_insensitive(!case_sensitive) .multi_line(multiline) .build()?; + let inner = SearchInputs { + query: initial_query, + files_to_exclude, + files_to_include, + }; Ok(Self::Regex { regex, - query: initial_query, multiline, whole_word, case_sensitive, - files_to_include, - files_to_exclude, + inner, }) } @@ -267,10 +288,7 @@ impl SearchQuery { } pub fn as_str(&self) -> &str { - match self { - Self::Text { query, .. } => query.as_ref(), - Self::Regex { query, .. } => query.as_ref(), - } + self.as_inner().as_str() } pub fn whole_word(&self) -> bool { @@ -292,25 +310,11 @@ impl SearchQuery { } pub fn files_to_include(&self) -> &[PathMatcher] { - match self { - Self::Text { - files_to_include, .. - } => files_to_include, - Self::Regex { - files_to_include, .. - } => files_to_include, - } + self.as_inner().files_to_include() } pub fn files_to_exclude(&self) -> &[PathMatcher] { - match self { - Self::Text { - files_to_exclude, .. - } => files_to_exclude, - Self::Regex { - files_to_exclude, .. - } => files_to_exclude, - } + self.as_inner().files_to_exclude() } pub fn file_matches(&self, file_path: Option<&Path>) -> bool { @@ -329,6 +333,11 @@ impl SearchQuery { None => self.files_to_include().is_empty(), } } + pub fn as_inner(&self) -> &SearchInputs { + match self { + Self::Regex { inner, .. } | Self::Text { inner, .. } => inner, + } + } } fn deserialize_path_matches(glob_set: &str) -> anyhow::Result> { diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 3a59f9a5cd..64421f5431 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -30,11 +30,11 @@ serde_derive.workspace = true smallvec.workspace = true smol.workspace = true globset.workspace = true - +serde_json.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -serde_json.workspace = true + workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d85d311b8f..b65c7222a4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,9 @@ use crate::{ - NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, + history::SearchHistory, + mode::{next_mode, SearchMode}, + search_bar::{render_nav_button, render_search_mode_button}, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -16,6 +19,7 @@ use gpui::{ use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; + use util::ResultExt; use workspace::{ item::ItemHandle, @@ -48,9 +52,10 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::handle_editor_cancel); cx.add_action(BufferSearchBar::next_history_query); cx.add_action(BufferSearchBar::previous_history_query); + cx.add_action(BufferSearchBar::cycle_mode); + cx.add_action(BufferSearchBar::cycle_mode_on_pane); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); - add_toggle_option_action::(SearchOptions::REGEX, cx); } fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { @@ -79,6 +84,7 @@ pub struct BufferSearchBar { query_contains_error: bool, dismissed: bool, search_history: SearchHistory, + current_mode: SearchMode, } impl Entity for BufferSearchBar { @@ -98,7 +104,7 @@ impl View for BufferSearchBar { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); - let editor_container = if self.query_contains_error { + let query_container_style = if self.query_contains_error { theme.search.invalid_editor } else { theme.search.editor.input.container @@ -150,81 +156,137 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); + let search_button_for_mode = |mode, cx: &mut ViewContext| { + let is_active = self.current_mode == mode; - Flex::row() + render_search_mode_button( + mode, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, + ) + }; + let search_option_button = |option| { + let is_active = self.search_options.contains(option); + option.as_button( + is_active, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + ) + }; + let match_count = self + .active_searchable_item + .as_ref() + .and_then(|searchable_item| { + if self.query(cx).is_empty() { + return None; + } + let matches = self + .searchable_items_with_matches + .get(&searchable_item.downgrade())?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, matches.len()) + } else { + "No matches".to_string() + }; + + Some( + Label::new(message, theme.search.match_index.text.clone()) + .contained() + .with_style(theme.search.match_index.container) + .aligned(), + ) + }); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + self.active_match_index.is_some(), + move |_, this, cx| match direction { + Direction::Prev => this.select_prev_match(&Default::default(), cx), + Direction::Next => this.select_next_match(&Default::default(), cx), + }, + cx, + ) + }; + + let icon_style = theme.search.editor_icon.clone(); + let nav_column = Flex::row() + .with_child(self.render_action_button("Select All", cx)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .with_child(Flex::row().with_children(match_count)) + .constrained() + .with_height(theme.search.search_bar_row_height); + + let query = Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( Flex::row() - .with_child( - Flex::row() - .with_child( - ChildView::new(&self.query_editor, cx) - .aligned() - .left() - .flex(1., true), - ) - .with_children(self.active_searchable_item.as_ref().and_then( - |searchable_item| { - let matches = self - .searchable_items_with_matches - .get(&searchable_item.downgrade())?; - let message = if let Some(match_ix) = self.active_match_index { - format!("{}/{}", match_ix + 1, matches.len()) - } else { - "No matches".to_string() - }; - - Some( - Label::new(message, theme.search.match_index.text.clone()) - .contained() - .with_style(theme.search.match_index.container) - .aligned(), - ) - }, - )) - .contained() - .with_style(editor_container) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .flex(1., false), + .with_children( + supported_options + .case + .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .with_child(self.render_action_button("Select All", cx)) - .aligned(), + .with_children( + supported_options + .word + .then(|| search_option_button(SearchOptions::WHOLE_WORD)), ) - .with_child( - Flex::row() - .with_children(self.render_search_option( - supported_options.case, - "Case", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_children(self.render_search_option( - supported_options.word, - "Word", - SearchOptions::WHOLE_WORD, - cx, - )) - .with_children(self.render_search_option( - supported_options.regex, - "Regex", - SearchOptions::REGEX, - cx, - )) - .contained() - .with_style(theme.search.option_button_group) - .aligned(), - ) - .flex(1., true), + .flex_float() + .contained(), ) - .with_child(self.render_close_button(&theme.search, cx)) + .align_children_center() + .flex(1., true); + let editor_column = Flex::row() + .with_child( + query + .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), + ) + .contained() + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., false); + let mode_column = Flex::row() + .with_child( + Flex::row() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .contained() + .with_style(theme.search.modes_container), + ) + .with_child(super::search_bar::render_close_button( + "Dismiss Buffer Search", + &theme.search, + cx, + |_, this, cx| this.dismiss(&Default::default(), cx), + Some(Box::new(Dismiss)), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .aligned() + .right() + .flex_float(); + Flex::row() + .with_child(editor_column) + .with_child(nav_column) + .with_child(mode_column) .contained() .with_style(theme.search.container) + .aligned() .into_any_named("search bar") } } @@ -278,6 +340,9 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } } + fn row_count(&self, _: &ViewContext) -> usize { + 2 + } } impl BufferSearchBar { @@ -304,6 +369,7 @@ impl BufferSearchBar { query_contains_error: false, dismissed: true, search_history: SearchHistory::default(), + current_mode: SearchMode::default(), } } @@ -415,91 +481,6 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_search_option( - &self, - option_supported: bool, - icon: &'static str, - option: SearchOptions, - cx: &mut ViewContext, - ) -> Option> { - if !option_supported { - return None; - } - - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.search_options.contains(option); - Some( - MouseEventHandler::new::(option.bits as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_search_option(option, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - option.bits as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, - ) - .into_any(), - ) - } - - fn render_nav_button( - &self, - icon: &'static str, - direction: Direction, - cx: &mut ViewContext, - ) -> AnyElement { - let action: Box; - let tooltip; - match direction { - Direction::Prev => { - action = Box::new(SelectPrevMatch); - tooltip = "Select Previous Match"; - } - Direction::Next => { - action = Box::new(SelectNextMatch); - tooltip = "Select Next Match"; - } - }; - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum NavButton {} - MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme.search.option_button.inactive_state().style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, { - move |_, this, cx| match direction { - Direction::Prev => this.select_prev_match(&Default::default(), cx), - Direction::Next => this.select_next_match(&Default::default(), cx), - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - direction as usize, - tooltip.to_string(), - Some(action), - tooltip_style, - cx, - ) - .into_any() - } - fn render_action_button( &self, icon: &'static str, @@ -508,19 +489,29 @@ impl BufferSearchBar { 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.style_for(state); + 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(CursorStyle::PointingHand) + .with_cursor_style(cursor_style) .with_tooltip::( action_type_id, tooltip.to_string(), @@ -531,39 +522,13 @@ impl BufferSearchBar { .into_any() } - fn render_close_button( - &self, - theme: &theme::Search, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip = "Dismiss Buffer Search"; - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum CloseButton {} - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx) - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - 0, - tooltip.to_string(), - Some(Box::new(Dismiss)), - tooltip_style, - cx, - ) - .into_any() + pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + if mode == self.current_mode { + return; + } + self.current_mode = mode; + let _ = self.update_matches(cx); + cx.notify(); } fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { @@ -734,8 +699,9 @@ impl BufferSearchBar { self.active_match_index.take(); active_searchable_item.clear_matches(cx); let _ = done_tx.send(()); + cx.notify(); } else { - let query = if self.search_options.contains(SearchOptions::REGEX) { + let query = if self.current_mode == SearchMode::Regex { match SearchQuery::regex( query, self.search_options.contains(SearchOptions::WHOLE_WORD), @@ -830,6 +796,26 @@ impl BufferSearchBar { let _ = self.search(&new_query, Some(self.search_options), cx); } } + fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { + self.activate_search_mode(next_mode(&self.current_mode), cx); + } + fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| { + if bar.show(cx) { + should_propagate = false; + bar.cycle_mode(action, cx); + false + } else { + true + } + }); + } + if should_propagate { + cx.propagate_action(); + } + } } #[cfg(test)] diff --git a/crates/search/src/history.rs b/crates/search/src/history.rs new file mode 100644 index 0000000000..6b06c60293 --- /dev/null +++ b/crates/search/src/history.rs @@ -0,0 +1,184 @@ +use smallvec::SmallVec; +const SEARCH_HISTORY_LIMIT: usize = 20; + +#[derive(Default, Debug, Clone)] +pub struct SearchHistory { + history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, + selected: Option, +} + +impl SearchHistory { + pub fn add(&mut self, search_string: String) { + if let Some(i) = self.selected { + if search_string == self.history[i] { + return; + } + } + + if let Some(previously_searched) = self.history.last_mut() { + if search_string.find(previously_searched.as_str()).is_some() { + *previously_searched = search_string; + self.selected = Some(self.history.len() - 1); + return; + } + } + + self.history.push(search_string); + if self.history.len() > SEARCH_HISTORY_LIMIT { + self.history.remove(0); + } + self.selected = Some(self.history.len() - 1); + } + + pub fn next(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let selected = self.selected?; + if selected == history_size - 1 { + return None; + } + let next_index = selected + 1; + self.selected = Some(next_index); + Some(&self.history[next_index]) + } + + pub fn current(&self) -> Option<&str> { + Some(&self.history[self.selected?]) + } + + pub fn previous(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let prev_index = match self.selected { + Some(selected_index) => { + if selected_index == 0 { + return None; + } else { + selected_index - 1 + } + } + None => history_size - 1, + }; + + self.selected = Some(prev_index); + Some(&self.history[prev_index]) + } + + pub fn reset_selection(&mut self) { + self.selected = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.current(), + None, + "No current selection should be set fo the default search history" + ); + + search_history.add("rust".to_string()); + assert_eq!( + search_history.current(), + Some("rust"), + "Newly added item should be selected" + ); + + // check if duplicates are not added + search_history.add("rust".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should not add a duplicate" + ); + assert_eq!(search_history.current(), Some("rust")); + + // check if new string containing the previous string replaces it + search_history.add("rustlang".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should replace previous item if it's a substring" + ); + assert_eq!(search_history.current(), Some("rustlang")); + + // push enough items to test SEARCH_HISTORY_LIMIT + for i in 0..SEARCH_HISTORY_LIMIT * 2 { + search_history.add(format!("item{i}")); + } + assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); + } + + #[test] + fn test_next_and_previous() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.next(), + None, + "Default search history should not have a next item" + ); + + search_history.add("Rust".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("JavaScript".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("TypeScript".to_string()); + assert_eq!(search_history.next(), None); + + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.previous(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.previous(), Some("Rust")); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.previous(), None); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.next(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.next(), Some("TypeScript")); + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.next(), None); + assert_eq!(search_history.current(), Some("TypeScript")); + } + + #[test] + fn test_reset_selection() { + let mut search_history = SearchHistory::default(); + search_history.add("Rust".to_string()); + search_history.add("JavaScript".to_string()); + search_history.add("TypeScript".to_string()); + + assert_eq!(search_history.current(), Some("TypeScript")); + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + assert_eq!( + search_history.previous(), + Some("TypeScript"), + "Should start from the end after reset on previous item query" + ); + + search_history.previous(); + assert_eq!(search_history.current(), Some("JavaScript")); + search_history.previous(); + assert_eq!(search_history.current(), Some("Rust")); + + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + } +} diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs new file mode 100644 index 0000000000..0163528951 --- /dev/null +++ b/crates/search/src/mode.rs @@ -0,0 +1,74 @@ +use gpui::Action; + +use crate::{ActivateRegexMode, ActivateTextMode}; +// TODO: Update the default search mode to get from config +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub enum SearchMode { + #[default] + Text, + Regex, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) enum Side { + Left, + Right, +} + +impl SearchMode { + pub(crate) fn label(&self) -> &'static str { + match self { + SearchMode::Text => "Text", + SearchMode::Regex => "Regex", + } + } + + pub(crate) fn region_id(&self) -> usize { + match self { + SearchMode::Text => 3, + SearchMode::Regex => 5, + } + } + + pub(crate) fn tooltip_text(&self) -> &'static str { + match self { + SearchMode::Text => "Activate Text Search", + SearchMode::Regex => "Activate Regex Search", + } + } + + pub(crate) fn activate_action(&self) -> Box { + match self { + SearchMode::Text => Box::new(ActivateTextMode), + SearchMode::Regex => Box::new(ActivateRegexMode), + } + } + + pub(crate) fn border_right(&self) -> bool { + match self { + SearchMode::Regex => true, + SearchMode::Text => true, + } + } + + pub(crate) fn border_left(&self) -> bool { + match self { + SearchMode::Text => true, + _ => false, + } + } + + pub(crate) fn button_side(&self) -> Option { + match self { + SearchMode::Text => Some(Side::Left), + SearchMode::Regex => Some(Side::Right), + } + } +} + +pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode { + match mode { + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => SearchMode::Text, + } +} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8bc8d7da88..2cec9610f1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,9 @@ use crate::{ - NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, - SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, + history::SearchHistory, + mode::SearchMode, + search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, + SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use anyhow::Context; use collections::HashMap; @@ -9,20 +12,18 @@ use editor::{ SelectAll, MAX_TAB_TITLE_LEN, }; use futures::StreamExt; + use gpui::{ - actions, - elements::*, - platform::{CursorStyle, MouseButton}, - Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, - Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext, + Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, }; + use menu::Confirm; -use postage::stream::Stream; use project::{ search::{PathMatcher, SearchQuery}, Entry, Project, }; -use semantic_index::SemanticIndex; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -42,7 +43,7 @@ use workspace::{ actions!( project_search, - [SearchInNew, ToggleFocus, NextField, ToggleSemanticSearch] + [SearchInNew, ToggleFocus, NextField, ToggleFilters,] ); #[derive(Default)] @@ -56,13 +57,26 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_prev_match); + cx.add_action(ProjectSearchBar::cycle_mode); cx.add_action(ProjectSearchBar::next_history_query); cx.add_action(ProjectSearchBar::previous_history_query); + // cx.add_action(ProjectSearchBar::activate_regex_mode); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); - add_toggle_option_action::(SearchOptions::REGEX, cx); + add_toggle_filters_action::(cx); +} + +fn add_toggle_filters_action(cx: &mut AppContext) { + cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) { + return; + } + } + cx.propagate_action(); + }); } fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { @@ -86,6 +100,7 @@ struct ProjectSearch { active_query: Option, search_id: usize, search_history: SearchHistory, + no_results: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -99,7 +114,6 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, - semantic: Option, search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, @@ -107,12 +121,8 @@ pub struct ProjectSearchView { query_editor_was_focused: bool, included_files_editor: ViewHandle, excluded_files_editor: ViewHandle, -} - -struct SemanticSearchState { - file_count: usize, - outstanding_file_count: usize, - _progress_task: Task<()>, + filters_enabled: bool, + current_mode: SearchMode, } pub struct ProjectSearchBar { @@ -135,6 +145,7 @@ impl ProjectSearch { active_query: None, search_id: 0, search_history: SearchHistory::default(), + no_results: None, } } @@ -149,6 +160,7 @@ impl ProjectSearch { active_query: self.active_query.clone(), search_id: self.search_id, search_history: self.search_history.clone(), + no_results: self.no_results.clone(), }) } @@ -166,6 +178,7 @@ impl ProjectSearch { let mut matches = matches.into_iter().collect::>(); let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { this.match_ranges.clear(); + this.no_results = Some(true); matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); @@ -179,58 +192,7 @@ impl ProjectSearch { while let Ok(Some(match_range)) = match_ranges.try_next() { this.match_ranges.push(match_range); } - cx.notify(); - }); - } - - this.update(&mut cx, |this, cx| { - this.pending_search.take(); - cx.notify(); - }); - - None - })); - cx.notify(); - } - - fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext) { - let search = SemanticIndex::global(cx).map(|index| { - index.update(cx, |semantic_index, cx| { - semantic_index.search_project( - self.project.clone(), - query.as_str().to_owned(), - 10, - query.files_to_include().to_vec(), - query.files_to_exclude().to_vec(), - cx, - ) - }) - }); - self.search_id += 1; - self.match_ranges.clear(); - self.search_history.add(query.as_str().to_string()); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - let results = search?.await.log_err()?; - - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])) - .collect(); - - excerpts.stream_excerpts_with_context_lines(matches, 3, cx) - }) - }); - - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + this.no_results = Some(false); cx.notify(); }); } @@ -246,10 +208,12 @@ impl ProjectSearch { } } +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ViewEvent { UpdateTab, Activate, EditorEvent(editor::Event), + Dismiss, } impl Entity for ProjectSearchView { @@ -267,22 +231,36 @@ impl View for ProjectSearchView { enum Status {} let theme = theme::current(cx).clone(); - let text = if model.pending_search.is_some() { + + // If Search is Active -> Major: Searching..., Minor: None + // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...} + // If Regex -> Major: "Search using Regex", Minor: {ex...} + // If Text -> Major: "Text search all files and folders", Minor: {...} + + let current_mode = self.current_mode; + let major_text = if model.pending_search.is_some() { Cow::Borrowed("Searching...") - } else if let Some(semantic) = &self.semantic { - if semantic.outstanding_file_count > 0 { - Cow::Owned(format!( - "Indexing. {} of {}...", - semantic.file_count - semantic.outstanding_file_count, - semantic.file_count - )) - } else { - Cow::Borrowed("Indexing complete") - } - } else if self.query_editor.read(cx).text(cx).is_empty() { - Cow::Borrowed("") + } else if model.no_results.is_some_and(|v| v) { + Cow::Borrowed("No Results") } else { - Cow::Borrowed("No results") + match current_mode { + SearchMode::Text => Cow::Borrowed("Text search all files and folders"), + SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), + } + }; + + let minor_text = if let Some(no_results) = model.no_results { + if model.pending_search.is_none() && no_results { + vec!["No results found in this project for the provided query".to_owned()] + } else { + vec![] + } + } else { + vec![ + "".to_owned(), + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ] }; let previous_query_keystrokes = @@ -329,11 +307,27 @@ impl View for ProjectSearchView { }); MouseEventHandler::new::(0, cx, |_, _| { - Label::new(text, theme.search.results_status.clone()) - .aligned() + Flex::column() + .with_child(Flex::column().contained().flex(1., true)) + .with_child( + Flex::column() + .align_children_center() + .with_child(Label::new( + major_text, + theme.search.major_results_status.clone(), + )) + .with_children( + minor_text.into_iter().map(|x| { + Label::new(x, theme.search.minor_results_status.clone()) + }), + ) + .aligned() + .top() + .contained() + .flex(7., true), + ) .contained() .with_background_color(theme.editor.background) - .flex(1., true) }) .on_down(MouseButton::Left, |_, _, cx| { cx.focus_parent(); @@ -374,7 +368,9 @@ impl Item for ProjectSearchView { .then(|| query_text.into()) .or_else(|| Some("Project Search".into())) } - + fn should_close_item_on_event(event: &Self::Event) -> bool { + event == &Self::Event::Dismiss + } fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -411,11 +407,21 @@ impl Item for ProjectSearchView { .contained() .with_margin_right(tab_theme.spacing), ) - .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - let query_text = util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN); - - Label::new(query_text, tab_theme.label.clone()).aligned() - })) + .with_child({ + let tab_name: Option> = + self.model.read(cx).active_query.as_ref().map(|query| { + let query_text = + util::truncate_and_trailoff(query.as_str(), 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() + }) .into_any() } @@ -496,6 +502,7 @@ impl Item for ProjectSearchView { smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab] } ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event), + ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem], _ => SmallVec::new(), } } @@ -528,6 +535,37 @@ impl Item for ProjectSearchView { } impl ProjectSearchView { + fn toggle_search_option(&mut self, option: SearchOptions) { + self.search_options.toggle(option); + } + + fn clear_search(&mut self, cx: &mut ViewContext) { + self.model.update(cx, |model, cx| { + model.pending_search = None; + model.no_results = None; + model.match_ranges.clear(); + + model.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + }); + }); + } + + fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + let previous_mode = self.current_mode; + if previous_mode == mode { + return; + } + + self.clear_search(cx); + self.current_mode = mode; + self.active_match_index = None; + + self.search(cx); + + cx.notify(); + } + fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { let project; let excerpts; @@ -551,6 +589,7 @@ impl ProjectSearchView { Some(Arc::new(|theme| theme.search.editor.input.clone())), cx, ); + editor.set_placeholder_text("Text search all files", cx); editor.set_text(query_text, cx); editor }); @@ -561,7 +600,7 @@ impl ProjectSearchView { .detach(); let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx); + let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); editor.set_searchable(false); editor }); @@ -610,19 +649,22 @@ impl ProjectSearchView { cx.emit(ViewEvent::EditorEvent(event.clone())) }) .detach(); + let filters_enabled = false; + // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { search_id: model.read(cx).search_id, model, query_editor, results_editor, - semantic: None, search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, query_editor_was_focused: false, included_files_editor, excluded_files_editor, + filters_enabled, + current_mode: Default::default(), }; this.model_changed(cx); this @@ -703,16 +745,6 @@ impl ProjectSearchView { } fn search(&mut self, cx: &mut ViewContext) { - if let Some(semantic) = &mut self.semantic { - if semantic.outstanding_file_count > 0 { - return; - } - if let Some(query) = self.build_search_query(cx) { - self.model - .update(cx, |model, cx| model.semantic_search(query, cx)); - } - } - if let Some(query) = self.build_search_query(cx) { self.model.update(cx, |model, cx| model.search(query, cx)); } @@ -744,32 +776,34 @@ impl ProjectSearchView { return None; } }; - if self.search_options.contains(SearchOptions::REGEX) { - match SearchQuery::regex( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - included_files, - excluded_files, - ) { - Ok(query) => { - self.panels_with_errors.remove(&InputPanel::Query); - Some(query) - } - Err(_e) => { - self.panels_with_errors.insert(InputPanel::Query); - cx.notify(); - None + let current_mode = self.current_mode; + match current_mode { + SearchMode::Regex => { + match SearchQuery::regex( + text, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + included_files, + excluded_files, + ) { + Ok(query) => { + self.panels_with_errors.remove(&InputPanel::Query); + Some(query) + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Query); + cx.notify(); + None + } } } - } else { - Some(SearchQuery::text( + _ => Some(SearchQuery::text( text, self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::CASE_SENSITIVE), included_files, excluded_files, - )) + )), } } @@ -906,7 +940,18 @@ impl ProjectSearchBar { subscription: Default::default(), } } - + fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |this, cx| { + let new_mode = crate::mode::next_mode(&this.current_mode); + this.activate_search_mode(new_mode, cx); + cx.focus(&this.query_editor); + }) + } + } fn search(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| search_view.search(cx)); @@ -1016,8 +1061,7 @@ impl ProjectSearchBar { fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - search_view.search_options.toggle(option); - search_view.semantic = None; + search_view.toggle_search_option(option); search_view.search(cx); }); cx.notify(); @@ -1027,52 +1071,30 @@ impl ProjectSearchBar { } } - fn toggle_semantic_search(&mut self, cx: &mut ViewContext) -> bool { + // fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { + // if let Some(search_view) = pane + // .active_item() + // .and_then(|item| item.downcast::()) + // { + // search_view.update(cx, |view, cx| { + // view.activate_search_mode(SearchMode::Regex, cx) + // }); + // } else { + // cx.propagate_action(); + // } + // } + + fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - if search_view.semantic.is_some() { - search_view.semantic = None; - } else if let Some(semantic_index) = SemanticIndex::global(cx) { - // TODO: confirm that it's ok to send this project - search_view.search_options = SearchOptions::none(); - - let project = search_view.model.read(cx).project.clone(); - let index_task = semantic_index.update(cx, |semantic_index, cx| { - semantic_index.index_project(project, cx) - }); - - cx.spawn(|search_view, mut cx| async move { - let (files_to_index, mut files_remaining_rx) = index_task.await?; - - search_view.update(&mut cx, |search_view, cx| { - cx.notify(); - search_view.semantic = Some(SemanticSearchState { - file_count: files_to_index, - outstanding_file_count: files_to_index, - _progress_task: cx.spawn(|search_view, mut cx| async move { - while let Some(count) = files_remaining_rx.recv().await { - search_view - .update(&mut cx, |search_view, cx| { - if let Some(semantic_search_state) = - &mut search_view.semantic - { - semantic_search_state.outstanding_file_count = - count; - cx.notify(); - if count == 0 { - return; - } - } - }) - .ok(); - } - }), - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + search_view.filters_enabled = !search_view.filters_enabled; + search_view + .included_files_editor + .update(cx, |_, cx| cx.notify()); + search_view + .excluded_files_editor + .update(cx, |_, cx| cx.notify()); + cx.refresh_windows(); cx.notify(); }); cx.notify(); @@ -1082,117 +1104,14 @@ impl ProjectSearchBar { } } - fn render_nav_button( - &self, - icon: &'static str, - direction: Direction, - cx: &mut ViewContext, - ) -> AnyElement { - let action: Box; - let tooltip; - match direction { - Direction::Prev => { - action = Box::new(SelectPrevMatch); - tooltip = "Select Previous Match"; - } - Direction::Next => { - action = Box::new(SelectNextMatch); - tooltip = "Select Next Match"; - } - }; - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum NavButton {} - MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme.search.option_button.inactive_state().style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - direction as usize, - tooltip.to_string(), - Some(action), - tooltip_style, - cx, - ) - .into_any() - } - - fn render_option_button( - &self, - icon: &'static str, - option: SearchOptions, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.is_option_enabled(option, cx); - MouseEventHandler::new::(option.bits as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_search_option(option, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - option.bits as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, - ) - .into_any() - } - - fn render_semantic_search_button(&self, cx: &mut ViewContext) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.semantic.is_some() - } else { - false - }; - - let region_id = 3; - - MouseEventHandler::new::(region_id, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Label::new("Semantic", style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_semantic_search(cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - region_id, - format!("Toggle Semantic Search"), - Some(Box::new(ToggleSemanticSearch)), - tooltip_style, - cx, - ) - .into_any() + fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { + // Update Current Mode + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.activate_search_mode(mode, cx); + }); + cx.notify(); + } } fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { @@ -1255,20 +1174,85 @@ impl View for ProjectSearchBar { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); + if let Some(_search) = self.active_project_search.as_ref() { + let search = _search.read(cx); let theme = theme::current(cx).clone(); let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { theme.search.invalid_editor } else { theme.search.editor.input.container }; + + let search = _search.read(cx); + let filter_button = render_option_button_icon( + search.filters_enabled, + "icons/filter_12.svg", + 0, + "Toggle filters", + Box::new(ToggleFilters), + move |_, this, cx| { + this.toggle_filters(cx); + }, + cx, + ); + + let render_option_button_icon = |path, option, cx: &mut ViewContext| { + crate::search_bar::render_option_button_icon( + self.is_option_enabled(option, cx), + path, + option.bits as usize, + format!("Toggle {}", option.label()), + option.to_toggle_action(), + move |_, this, cx| { + this.toggle_search_option(option, cx); + }, + cx, + ) + }; + let case_sensitive = render_option_button_icon( + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + ); + + let whole_word = render_option_button_icon( + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + ); + + let search = _search.read(cx); + let icon_style = theme.search.editor_icon.clone(); + + // Editor Functionality + let query = Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_child(case_sensitive) + .with_child(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .flex(1., true); + + let search = _search.read(cx); + let include_container_style = if search.panels_with_errors.contains(&InputPanel::Include) { theme.search.invalid_include_exclude_editor } else { theme.search.include_exclude_editor.input.container }; + let exclude_container_style = if search.panels_with_errors.contains(&InputPanel::Exclude) { theme.search.invalid_include_exclude_editor @@ -1277,115 +1261,134 @@ impl View for ProjectSearchBar { }; let included_files_view = ChildView::new(&search.included_files_editor, cx) - .aligned() - .left() - .flex(1.0, true); + .contained() + .flex(1., true); let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) - .aligned() - .right() - .flex(1.0, true); + .contained() + .flex(1., true); + let filters = search.filters_enabled.then(|| { + Flex::row() + .with_child( + included_files_view + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .with_min_width(theme.search.include_exclude_editor.min_width) + .with_max_width(theme.search.include_exclude_editor.max_width), + ) + .with_child( + excluded_files_view + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .with_min_width(theme.search.include_exclude_editor.min_width) + .with_max_width(theme.search.include_exclude_editor.max_width), + ) + .contained() + .with_padding_top(theme.workspace.toolbar.container.padding.bottom) + }); - let row_spacing = theme.workspace.toolbar.container.padding.bottom; - - Flex::column() + let editor_column = Flex::column() .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child( - ChildView::new(&search.query_editor, cx) - .aligned() - .left() - .flex(1., true), - ) - .with_children(search.active_match_index.map(|match_ix| { - Label::new( - format!( - "{}/{}", - match_ix + 1, - search.model.read(cx).match_ranges.len() - ), - theme.search.match_index.text.clone(), - ) - .contained() - .with_style(theme.search.match_index.container) - .aligned() - })) - .contained() - .with_style(query_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .aligned(), - ) - .with_child({ - let row = if SemanticIndex::enabled(cx) { - Flex::row().with_child(self.render_semantic_search_button(cx)) - } else { - Flex::row() - }; - - let row = row - .with_child(self.render_option_button( - "Case", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_child(self.render_option_button( - "Word", - SearchOptions::WHOLE_WORD, - cx, - )) - .with_child(self.render_option_button( - "Regex", - SearchOptions::REGEX, - cx, - )) - .contained() - .with_style(theme.search.option_button_group) - .aligned(); - - row - }) + query .contained() - .with_margin_bottom(row_spacing), + .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), ) - .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child(included_files_view) - .contained() - .with_style(include_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(excluded_files_view) - .contained() - .with_style(exclude_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width) - .flex(1., false), - ), + .with_children(filters) + .flex(1., false); + + let matches = search.active_match_index.map(|match_ix| { + Label::new( + format!( + "{}/{}", + match_ix + 1, + search.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), ) .contained() - .with_style(theme.search.container) + .with_style(theme.search.match_index.container) .aligned() - .left() + }); + + let search_button_for_mode = |mode, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, + ) + }; + let is_active = search.active_match_index.is_some(); + + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + is_active, + move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + } + }, + cx, + ) + }; + + let nav_column = Flex::row() + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .with_child(Flex::row().with_children(matches)) + .constrained() + .with_height(theme.search.search_bar_row_height); + + let mode_column = Flex::row() + .with_child( + Flex::row() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .contained() + .with_style(theme.search.modes_container), + ) + .with_child(super::search_bar::render_close_button( + "Dismiss Project Search", + &theme.search, + cx, + |_, this, cx| { + if let Some(search) = this.active_project_search.as_mut() { + search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) + } + }, + None, + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .aligned() + .right() + .top() + .flex_float(); + + Flex::row() + .with_child(editor_column) + .with_child(nav_column) + .with_child(mode_column) + .contained() + .with_style(theme.search.container) .into_any_named("project search") } else { Empty::new().into_any() @@ -1413,8 +1416,14 @@ impl ToolbarItemView for ProjectSearchBar { } } - fn row_count(&self) -> usize { - 2 + fn row_count(&self, cx: &ViewContext) -> usize { + self.active_project_search + .as_ref() + .map(|search| { + let offset = search.read(cx).filters_enabled as usize; + 2 + offset + }) + .unwrap_or_else(|| 2) } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index f1711afec2..7132efa5e3 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,12 +1,22 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; -use gpui::{actions, Action, AppContext}; +use gpui::{ + actions, + elements::{Component, StyleableComponent, TooltipStyle}, + Action, AnyElement, AppContext, Element, View, +}; +pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use smallvec::SmallVec; +use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, +}; pub mod buffer_search; +mod history; +mod mode; pub mod project_search; +pub(crate) mod search_bar; pub fn init(cx: &mut AppContext) { buffer_search::init(cx); @@ -16,14 +26,16 @@ pub fn init(cx: &mut AppContext) { actions!( search, [ + CycleMode, ToggleWholeWord, ToggleCaseSensitive, - ToggleRegex, SelectNextMatch, SelectPrevMatch, SelectAllMatches, NextHistoryQuery, PreviousHistoryQuery, + ActivateTextMode, + ActivateRegexMode ] ); @@ -33,7 +45,6 @@ bitflags! { const NONE = 0b000; const WHOLE_WORD = 0b001; const CASE_SENSITIVE = 0b010; - const REGEX = 0b100; } } @@ -42,7 +53,14 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => "Match Whole Word", SearchOptions::CASE_SENSITIVE => "Match Case", - SearchOptions::REGEX => "Use Regular Expression", + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn icon(&self) -> &'static str { + match *self { + SearchOptions::WHOLE_WORD => "icons/word_search_12.svg", + SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg", _ => panic!("{:?} is not a named SearchOption", self), } } @@ -51,7 +69,6 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), - SearchOptions::REGEX => Box::new(ToggleRegex), _ => panic!("{:?} is not a named SearchOption", self), } } @@ -64,191 +81,24 @@ impl SearchOptions { let mut options = SearchOptions::NONE; options.set(SearchOptions::WHOLE_WORD, query.whole_word()); options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); - options.set(SearchOptions::REGEX, query.is_regex()); options } -} -const SEARCH_HISTORY_LIMIT: usize = 20; - -#[derive(Default, Debug, Clone)] -pub struct SearchHistory { - history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, - selected: Option, -} - -impl SearchHistory { - pub fn add(&mut self, search_string: String) { - if let Some(i) = self.selected { - if search_string == self.history[i] { - return; - } - } - - if let Some(previously_searched) = self.history.last_mut() { - if search_string.find(previously_searched.as_str()).is_some() { - *previously_searched = search_string; - self.selected = Some(self.history.len() - 1); - return; - } - } - - self.history.push(search_string); - if self.history.len() > SEARCH_HISTORY_LIMIT { - self.history.remove(0); - } - self.selected = Some(self.history.len() - 1); - } - - pub fn next(&mut self) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - - let selected = self.selected?; - if selected == history_size - 1 { - return None; - } - let next_index = selected + 1; - self.selected = Some(next_index); - Some(&self.history[next_index]) - } - - pub fn current(&self) -> Option<&str> { - Some(&self.history[self.selected?]) - } - - pub fn previous(&mut self) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - - let prev_index = match self.selected { - Some(selected_index) => { - if selected_index == 0 { - return None; - } else { - selected_index - 1 - } - } - None => history_size - 1, - }; - - self.selected = Some(prev_index); - Some(&self.history[prev_index]) - } - - pub fn reset_selection(&mut self) { - self.selected = None; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_add() { - let mut search_history = SearchHistory::default(); - assert_eq!( - search_history.current(), - None, - "No current selection should be set fo the default search history" - ); - - search_history.add("rust".to_string()); - assert_eq!( - search_history.current(), - Some("rust"), - "Newly added item should be selected" - ); - - // check if duplicates are not added - search_history.add("rust".to_string()); - assert_eq!( - search_history.history.len(), - 1, - "Should not add a duplicate" - ); - assert_eq!(search_history.current(), Some("rust")); - - // check if new string containing the previous string replaces it - search_history.add("rustlang".to_string()); - assert_eq!( - search_history.history.len(), - 1, - "Should replace previous item if it's a substring" - ); - assert_eq!(search_history.current(), Some("rustlang")); - - // push enough items to test SEARCH_HISTORY_LIMIT - for i in 0..SEARCH_HISTORY_LIMIT * 2 { - search_history.add(format!("item{i}")); - } - assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); - } - - #[test] - fn test_next_and_previous() { - let mut search_history = SearchHistory::default(); - assert_eq!( - search_history.next(), - None, - "Default search history should not have a next item" - ); - - search_history.add("Rust".to_string()); - assert_eq!(search_history.next(), None); - search_history.add("JavaScript".to_string()); - assert_eq!(search_history.next(), None); - search_history.add("TypeScript".to_string()); - assert_eq!(search_history.next(), None); - - assert_eq!(search_history.current(), Some("TypeScript")); - - assert_eq!(search_history.previous(), Some("JavaScript")); - assert_eq!(search_history.current(), Some("JavaScript")); - - assert_eq!(search_history.previous(), Some("Rust")); - assert_eq!(search_history.current(), Some("Rust")); - - assert_eq!(search_history.previous(), None); - assert_eq!(search_history.current(), Some("Rust")); - - assert_eq!(search_history.next(), Some("JavaScript")); - assert_eq!(search_history.current(), Some("JavaScript")); - - assert_eq!(search_history.next(), Some("TypeScript")); - assert_eq!(search_history.current(), Some("TypeScript")); - - assert_eq!(search_history.next(), None); - assert_eq!(search_history.current(), Some("TypeScript")); - } - - #[test] - fn test_reset_selection() { - let mut search_history = SearchHistory::default(); - search_history.add("Rust".to_string()); - search_history.add("JavaScript".to_string()); - search_history.add("TypeScript".to_string()); - - assert_eq!(search_history.current(), Some("TypeScript")); - search_history.reset_selection(); - assert_eq!(search_history.current(), None); - assert_eq!( - search_history.previous(), - Some("TypeScript"), - "Should start from the end after reset on previous item query" - ); - - search_history.previous(); - assert_eq!(search_history.current(), Some("JavaScript")); - search_history.previous(); - assert_eq!(search_history.current(), Some("Rust")); - - search_history.reset_selection(); - assert_eq!(search_history.current(), None); + pub fn as_button( + &self, + active: bool, + tooltip_style: TooltipStyle, + button_style: ToggleIconButtonStyle, + ) -> AnyElement { + ActionButton::new_dynamic( + self.to_toggle_action(), + format!("Toggle {}", self.label()), + tooltip_style, + ) + .with_contents(Svg::new(self.icon())) + .toggleable(active) + .with_style(button_style) + .into_element() + .into_any() } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs new file mode 100644 index 0000000000..7d3c5261ea --- /dev/null +++ b/crates/search/src/search_bar.rs @@ -0,0 +1,201 @@ +use std::borrow::Cow; + +use gpui::{ + elements::{Label, MouseEventHandler, Svg}, + platform::{CursorStyle, MouseButton}, + scene::{CornerRadii, MouseClick}, + Action, AnyElement, Element, EventContext, View, ViewContext, +}; +use workspace::searchable::Direction; + +use crate::{ + mode::{SearchMode, Side}, + SelectNextMatch, SelectPrevMatch, +}; + +pub(super) fn render_close_button( + tooltip: &'static str, + theme: &theme::Search, + cx: &mut ViewContext, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + dismiss_action: Option>, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + + enum CloseButton {} + MouseEventHandler::new::(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state); + Svg::new("icons/x_mark_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.search_bar_row_height) + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) + .into_any() +} + +pub(super) fn render_nav_button( + icon: &'static str, + direction: Direction, + active: bool, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> AnyElement { + let action: Box; + let tooltip; + + match direction { + Direction::Prev => { + action = Box::new(SelectPrevMatch); + tooltip = "Select Previous Match"; + } + Direction::Next => { + action = Box::new(SelectNextMatch); + tooltip = "Select Next Match"; + } + }; + let tooltip_style = theme::current(cx).tooltip.clone(); + let cursor_style = if active { + CursorStyle::PointingHand + } else { + CursorStyle::default() + }; + enum NavButton {} + MouseEventHandler::new::(direction as usize, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .nav_button + .in_state(active) + .style_for(state) + .clone(); + let mut container_style = style.container.clone(); + let label = Label::new(icon, style.label.clone()).aligned().contained(); + container_style.corner_radii = match direction { + Direction::Prev => CornerRadii { + bottom_right: 0., + top_right: 0., + ..container_style.corner_radii + }, + Direction::Next => CornerRadii { + bottom_left: 0., + top_left: 0., + ..container_style.corner_radii + }, + }; + if direction == Direction::Prev { + // Remove right border so that when both Next and Prev buttons are + // next to one another, there's no double border between them. + container_style.border.right = false; + } + label.with_style(container_style) + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(cursor_style) + .with_tooltip::( + direction as usize, + tooltip.to_string(), + Some(action), + tooltip_style, + cx, + ) + .into_any() +} + +pub(crate) fn render_search_mode_button( + mode: SearchMode, + is_active: bool, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + enum SearchModeButton {} + MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { + let theme = theme::current(cx); + let mut style = theme + .search + .mode_button + .in_state(is_active) + .style_for(state) + .clone(); + style.container.border.left = mode.border_left(); + style.container.border.right = mode.border_right(); + + let label = Label::new(mode.label(), style.text.clone()) + .aligned() + .contained(); + let mut container_style = style.container.clone(); + if let Some(button_side) = mode.button_side() { + if button_side == Side::Left { + container_style.corner_radii = CornerRadii { + bottom_right: 0., + top_right: 0., + ..container_style.corner_radii + }; + label.with_style(container_style) + } else { + container_style.corner_radii = CornerRadii { + bottom_left: 0., + top_left: 0., + ..container_style.corner_radii + }; + label.with_style(container_style) + } + } else { + container_style.corner_radii = CornerRadii::default(); + label.with_style(container_style) + } + .constrained() + .with_height(theme.search.search_bar_row_height) + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + mode.region_id(), + mode.tooltip_text().to_owned(), + Some(mode.activate_action()), + tooltip_style, + cx, + ) + .into_any() +} + +pub(crate) fn render_option_button_icon( + is_active: bool, + icon: &'static str, + id: usize, + label: impl Into>, + action: Box, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::new::(id, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); + Svg::new(icon) + .with_color(style.color.clone()) + .constrained() + .with_width(style.icon_width) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.search.option_button_height) + .with_width(style.button_width) + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::(id, label, Some(action), tooltip_style, cx) + .into_any() +} diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 851c656d9a..8c9877b9d3 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -49,9 +49,8 @@ pub fn init( .join(Path::new(RELEASE_CHANNEL_NAME.as_str())) .join("embeddings_db"); - if *RELEASE_CHANNEL == ReleaseChannel::Stable - || !settings::get::(cx).enabled - { + // This needs to be removed at some point before stable. + if *RELEASE_CHANNEL == ReleaseChannel::Stable { return; } @@ -501,26 +500,12 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { - let worktree_scans_complete = project - .read(cx) - .worktrees(cx) - .map(|worktree| { - let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete(); - async move { - scan_complete.await; - } - }) - .collect::>(); - let worktrees_indexed_previously = project .read(cx) .worktrees(cx) .map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path())) .collect::>(); - cx.spawn(|_, _cx| async move { - futures::future::join_all(worktree_scans_complete).await; - let worktree_indexed_previously = futures::future::join_all(worktrees_indexed_previously).await; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 8c3587d942..5287c999e8 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case, non_upper_case_globals)] + mod keymap_file; mod settings_file; mod settings_store; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index bd275c18ad..73eb67f959 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -483,10 +483,8 @@ fn possible_open_targets( } pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { - let searcher = match query { - project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query), - project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query), - }; + let query = query.as_str(); + let searcher = RegexSearch::new(&query); searcher.ok() } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs new file mode 100644 index 0000000000..a74b9ed4a4 --- /dev/null +++ b/crates/theme/src/components.rs @@ -0,0 +1,344 @@ +use gpui::elements::StyleableComponent; + +use crate::{Interactive, Toggleable}; + +use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; + +pub type ToggleIconButtonStyle = Toggleable>>; + +pub trait ComponentExt { + fn toggleable(self, active: bool) -> Toggle; +} + +impl ComponentExt for C { + fn toggleable(self, active: bool) -> Toggle { + Toggle::new(self, active) + } +} + +pub mod toggle { + use gpui::elements::{GeneralComponent, StyleableComponent}; + + use crate::Toggleable; + + pub struct Toggle { + style: S, + active: bool, + component: C, + } + + impl Toggle { + pub fn new(component: C, active: bool) -> Self { + Toggle { + active, + component, + style: (), + } + } + } + + impl StyleableComponent for Toggle { + type Style = Toggleable; + + type Output = Toggle; + + fn with_style(self, style: Self::Style) -> Self::Output { + Toggle { + active: self.active, + component: self.component, + style, + } + } + } + + impl GeneralComponent for Toggle> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + self.component + .with_style(self.style.in_state(self.active).clone()) + .render(v, cx) + } + } +} + +pub mod action_button { + use std::borrow::Cow; + + use gpui::{ + elements::{ + ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle, + }, + platform::{CursorStyle, MouseButton}, + Action, Element, TypeTag, View, + }; + use schemars::JsonSchema; + use serde_derive::Deserialize; + + use crate::Interactive; + + pub struct ActionButton { + action: Box, + tooltip: Cow<'static, str>, + tooltip_style: TooltipStyle, + tag: TypeTag, + contents: C, + style: Interactive, + } + + #[derive(Clone, Deserialize, Default, JsonSchema)] + pub struct ButtonStyle { + #[serde(flatten)] + container: ContainerStyle, + button_width: Option, + button_height: Option, + #[serde(flatten)] + contents: C, + } + + impl ActionButton<(), ()> { + pub fn new_dynamic( + action: Box, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + ) -> Self { + Self { + contents: (), + tag: action.type_tag(), + style: Interactive::new_blank(), + tooltip: tooltip.into(), + tooltip_style, + action, + } + } + + pub fn new( + action: A, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + ) -> Self { + Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + } + + pub fn with_contents(self, contents: C) -> ActionButton { + ActionButton { + action: self.action, + tag: self.tag, + style: self.style, + tooltip: self.tooltip, + tooltip_style: self.tooltip_style, + contents, + } + } + } + + impl StyleableComponent for ActionButton { + type Style = Interactive>; + type Output = ActionButton>; + + fn with_style(self, style: Self::Style) -> Self::Output { + ActionButton { + action: self.action, + tag: self.tag, + contents: self.contents, + tooltip: self.tooltip, + tooltip_style: self.tooltip_style, + style, + } + } + } + + impl GeneralComponent for ActionButton> { + fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + let style = self.style.style_for(state); + let mut contents = self + .contents + .with_style(style.contents.to_owned()) + .render(v, cx) + .contained() + .with_style(style.container) + .constrained(); + + if let Some(height) = style.button_height { + contents = contents.with_height(height); + } + + if let Some(width) = style.button_width { + contents = contents.with_width(width); + } + + contents.into_any() + }) + .on_click(MouseButton::Left, { + let action = self.action.boxed_clone(); + move |_, _, cx| { + let window = cx.window(); + let view = cx.view_id(); + let action = action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action(view, action.as_ref(), &mut cx); + }) + .detach(); + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_dynamic_tooltip( + self.tag, + 0, + self.tooltip, + Some(self.action), + self.tooltip_style, + cx, + ) + .into_any() + } + } +} + +pub mod svg { + use std::borrow::Cow; + + use gpui::{ + elements::{GeneralComponent, StyleableComponent}, + Element, + }; + use schemars::JsonSchema; + use serde::Deserialize; + + #[derive(Clone, Default, JsonSchema)] + pub struct SvgStyle { + icon_width: f32, + icon_height: f32, + color: gpui::color::Color, + } + + impl<'de> Deserialize<'de> for SvgStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + pub enum IconSize { + IconSize { icon_size: f32 }, + Dimensions { width: f32, height: f32 }, + } + + #[derive(Deserialize)] + struct SvgStyleHelper { + #[serde(flatten)] + size: IconSize, + color: gpui::color::Color, + } + + let json = SvgStyleHelper::deserialize(deserializer)?; + let color = json.color; + + let result = match json.size { + IconSize::IconSize { icon_size } => SvgStyle { + icon_width: icon_size, + icon_height: icon_size, + color, + }, + IconSize::Dimensions { width, height } => SvgStyle { + icon_width: width, + icon_height: height, + color, + }, + }; + + Ok(result) + } + } + + pub struct Svg { + path: Cow<'static, str>, + style: S, + } + + impl Svg<()> { + pub fn new(path: impl Into>) -> Self { + Self { + path: path.into(), + style: (), + } + } + } + + impl StyleableComponent for Svg<()> { + type Style = SvgStyle; + + type Output = Svg; + + fn with_style(self, style: Self::Style) -> Self::Output { + Svg { + path: self.path, + style, + } + } + } + + impl GeneralComponent for Svg { + fn render( + self, + _: &mut V, + _: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + gpui::elements::Svg::new(self.path) + .with_color(self.style.color) + .constrained() + .with_width(self.style.icon_width) + .with_height(self.style.icon_height) + .into_any() + } + } +} + +pub mod label { + use std::borrow::Cow; + + use gpui::{ + elements::{GeneralComponent, LabelStyle, StyleableComponent}, + Element, + }; + + pub struct Label { + text: Cow<'static, str>, + style: S, + } + + impl Label<()> { + pub fn new(text: impl Into>) -> Self { + Self { + text: text.into(), + style: (), + } + } + } + + impl StyleableComponent for Label<()> { + type Style = LabelStyle; + + type Output = Label; + + fn with_style(self, style: Self::Style) -> Self::Output { + Label { + text: self.text, + style, + } + } + } + + impl GeneralComponent for Label { + fn render( + self, + _: &mut V, + _: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new(self.text, self.style).into_any() + } + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 30a2e8caec..80e823632a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1,7 +1,9 @@ +pub mod components; mod theme_registry; mod theme_settings; pub mod ui; +use components::ToggleIconButtonStyle; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -13,7 +15,7 @@ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle}; +use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; pub use theme_registry::*; pub use theme_settings::*; @@ -182,7 +184,7 @@ pub struct CopilotAuth { pub prompting: CopilotAuthPrompting, pub not_authorized: CopilotAuthNotAuthorized, pub authorized: CopilotAuthAuthorized, - pub cta_button: ButtonStyle, + pub cta_button: CopilotCTAButton, pub header: IconStyle, } @@ -196,7 +198,7 @@ pub struct CopilotAuthPrompting { #[derive(Deserialize, Default, Clone, JsonSchema)] pub struct DeviceCode { pub text: TextStyle, - pub cta: ButtonStyle, + pub cta: CopilotCTAButton, pub left: f32, pub left_container: ContainerStyle, pub right: f32, @@ -334,6 +336,7 @@ pub struct TabBar { pub inactive_pane: TabStyles, pub dragged_tab: Tab, pub height: f32, + pub nav_button: Interactive, } impl TabBar { @@ -398,7 +401,6 @@ pub struct Toolbar { pub container: ContainerStyle, pub height: f32, pub item_spacing: f32, - pub nav_button: Interactive, pub toggleable_tool: Toggleable>, } @@ -419,12 +421,20 @@ pub struct Search { pub include_exclude_editor: FindEditor, pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, - pub option_button: Toggleable>, - pub action_button: Interactive, + pub option_button: Toggleable>, + pub option_button_component: ToggleIconButtonStyle, + pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, - pub results_status: TextStyle, + pub major_results_status: TextStyle, + pub minor_results_status: TextStyle, pub dismiss_button: Interactive, + pub editor_icon: IconStyle, + pub mode_button: Toggleable>, + pub nav_button: Toggleable>, + pub search_bar_row_height: f32, + pub option_button_height: f32, + pub modes_container: ContainerStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] @@ -880,12 +890,32 @@ pub struct Interactive { pub disabled: Option, } +impl Interactive<()> { + pub fn new_blank() -> Self { + Self { + default: (), + hovered: None, + clicked: None, + disabled: None, + } + } +} + #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] pub struct Toggleable { active: T, inactive: T, } +impl Toggleable<()> { + pub fn new_blank() -> Self { + Self { + active: (), + inactive: (), + } + } +} + impl Toggleable { pub fn new(active: T, inactive: T) -> Self { Self { active, inactive } diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index f4a249e74e..7f0b05731e 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -145,12 +145,12 @@ pub fn keystroke_label( .with_style(label_style.container) } -pub type ButtonStyle = Interactive; +pub type CopilotCTAButton = Interactive; pub fn cta_button( label: L, max_width: f32, - style: &ButtonStyle, + style: &CopilotCTAButton, cx: &mut ViewContext, f: F, ) -> MouseEventHandler diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 9375c4e78d..5f1a68cfe9 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,5 +1,5 @@ use gpui::{actions, impl_actions, AppContext, ViewContext}; -use search::{buffer_search, BufferSearchBar, SearchOptions}; +use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions}; use serde_derive::Deserialize; use workspace::{searchable::Direction, Pane, Workspace}; @@ -65,10 +65,8 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext)>( + svg_path: &'static str, + style: theme::Interactive, + nav_button_height: f32, + tooltip_style: TooltipStyle, + enabled: bool, + on_click: F, + tooltip_action: A, + action_name: &str, + cx: &mut ViewContext, +) -> AnyElement { + MouseEventHandler::new::(0, cx, |state, _| { + let style = if enabled { + style.style_for(state) + } else { + style.disabled_style() + }; + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(nav_button_height) + .aligned() + .top() + }) + .with_cursor_style(if enabled { + CursorStyle::PointingHand + } else { + CursorStyle::default() + }) + .on_click(MouseButton::Left, move |_, toolbar, cx| { + on_click(toolbar, cx) + }) + .with_tooltip::( + 0, + action_name.to_string(), + Some(Box::new(tooltip_action)), + tooltip_style, + cx, + ) + .contained() + .into_any_named("nav button") +} + impl Pane { pub fn new( workspace: WeakViewHandle, @@ -253,7 +303,7 @@ impl Pane { pane: handle.clone(), next_timestamp, }))), - toolbar: cx.add_view(|_| Toolbar::new(Some(handle))), + toolbar: cx.add_view(|_| Toolbar::new()), tab_bar_context_menu: TabBarContextMenu { kind: TabBarContextMenuKind::New, handle: context_menu, @@ -265,7 +315,7 @@ impl Pane { has_focus: false, can_drop: Rc::new(|_, _| true), can_split: true, - render_tab_bar_buttons: Rc::new(|pane, cx| { + render_tab_bar_buttons: Rc::new(move |pane, cx| { Flex::row() // New menu .with_child(Self::render_tab_bar_button( @@ -1571,8 +1621,70 @@ impl View for Pane { }, ), ); + let tooltip_style = theme.tooltip.clone(); + let tab_bar_theme = theme.workspace.tab_bar.clone(); + + let nav_button_height = tab_bar_theme.height; + let button_style = tab_bar_theme.nav_button; + let border_for_nav_buttons = tab_bar_theme + .tab_style(false, false) + .container + .border + .clone(); let mut tab_row = Flex::row() + .with_child(nav_button( + "icons/arrow_left_16.svg", + button_style.clone(), + nav_button_height, + tooltip_style.clone(), + self.can_navigate_backward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_back(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoBack, + "Go Back", + cx, + )) + .with_child( + nav_button( + "icons/arrow_right_16.svg", + button_style.clone(), + nav_button_height, + tooltip_style, + self.can_navigate_forward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_forward(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoForward, + "Go Forward", + cx, + ) + .contained() + .with_border(border_for_nav_buttons), + ) .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); if self.has_focus { diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 0516f3a145..72c879d6d4 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -1,7 +1,7 @@ -use crate::{ItemHandle, Pane}; +use crate::ItemHandle; use gpui::{ - elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle, - AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle, + WindowContext, }; pub trait ToolbarItemView: View { @@ -25,7 +25,7 @@ pub trait ToolbarItemView: View { /// Number of times toolbar's height will be repeated to get the effective height. /// Useful when multiple rows one under each other are needed. /// The rows have the same width and act as a whole when reacting to resizes and similar events. - fn row_count(&self) -> usize { + fn row_count(&self, _cx: &ViewContext) -> usize { 1 } } @@ -54,7 +54,6 @@ pub struct Toolbar { active_item: Option>, hidden: bool, can_navigate: bool, - pane: Option>, items: Vec<(Box, ToolbarItemLocation)>, } @@ -118,76 +117,10 @@ impl View for Toolbar { } } - let pane = self.pane.clone(); - let mut enable_go_backward = false; - let mut enable_go_forward = false; - if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) { - let pane = pane.read(cx); - enable_go_backward = pane.can_navigate_backward(); - enable_go_forward = pane.can_navigate_forward(); - } - let container_style = theme.container; let height = theme.height * primary_items_row_count as f32; - let nav_button_height = theme.height; - let button_style = theme.nav_button; - let tooltip_style = theme::current(cx).tooltip.clone(); let mut primary_items = Flex::row(); - if self.can_navigate { - primary_items.add_child(nav_button( - "icons/arrow_left_16.svg", - button_style, - nav_button_height, - tooltip_style.clone(), - enable_go_backward, - spacing, - { - move |toolbar, cx| { - if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx)) - { - if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) { - let pane = pane.downgrade(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, cx).detach_and_log_err(cx); - }); - }) - } - } - } - }, - super::GoBack, - "Go Back", - cx, - )); - primary_items.add_child(nav_button( - "icons/arrow_right_16.svg", - button_style, - nav_button_height, - tooltip_style, - enable_go_forward, - spacing, - { - move |toolbar, cx| { - if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx)) - { - if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) { - let pane = pane.downgrade(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_forward(pane, cx).detach_and_log_err(cx); - }); - }) - } - } - } - }, - super::GoForward, - "Go Forward", - cx, - )); - } primary_items.extend(primary_left_items); primary_items.extend(primary_right_items); @@ -210,63 +143,65 @@ impl View for Toolbar { } } -#[allow(clippy::too_many_arguments)] -fn nav_button)>( - svg_path: &'static str, - style: theme::Interactive, - nav_button_height: f32, - tooltip_style: TooltipStyle, - enabled: bool, - spacing: f32, - on_click: F, - tooltip_action: A, - action_name: &'static str, - cx: &mut ViewContext, -) -> AnyElement { - MouseEventHandler::new::(0, cx, |state, _| { - let style = if enabled { - style.style_for(state) - } else { - style.disabled_style() - }; - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(nav_button_height) - .aligned() - .top() - }) - .with_cursor_style(if enabled { - CursorStyle::PointingHand - } else { - CursorStyle::default() - }) - .on_click(MouseButton::Left, move |_, toolbar, cx| { - on_click(toolbar, cx) - }) - .with_tooltip::( - 0, - action_name, - Some(Box::new(tooltip_action)), - tooltip_style, - cx, - ) - .contained() - .with_margin_right(spacing) - .into_any_named("nav button") -} +// <<<<<<< HEAD +// ======= +// #[allow(clippy::too_many_arguments)] +// fn nav_button)>( +// svg_path: &'static str, +// style: theme::Interactive, +// nav_button_height: f32, +// tooltip_style: TooltipStyle, +// enabled: bool, +// spacing: f32, +// on_click: F, +// tooltip_action: A, +// action_name: &'static str, +// cx: &mut ViewContext, +// ) -> AnyElement { +// MouseEventHandler::new::(0, cx, |state, _| { +// let style = if enabled { +// style.style_for(state) +// } else { +// style.disabled_style() +// }; +// Svg::new(svg_path) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .contained() +// .with_style(style.container) +// .constrained() +// .with_width(style.button_width) +// .with_height(nav_button_height) +// .aligned() +// .top() +// }) +// .with_cursor_style(if enabled { +// CursorStyle::PointingHand +// } else { +// CursorStyle::default() +// }) +// .on_click(MouseButton::Left, move |_, toolbar, cx| { +// on_click(toolbar, cx) +// }) +// .with_tooltip::( +// 0, +// action_name, +// Some(Box::new(tooltip_action)), +// tooltip_style, +// cx, +// ) +// .contained() +// .with_margin_right(spacing) +// .into_any_named("nav button") +// } +// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e impl Toolbar { - pub fn new(pane: Option>) -> Self { + pub fn new() -> Self { Self { active_item: None, - pane, items: Default::default(), hidden: false, can_navigate: true, @@ -362,7 +297,7 @@ impl ToolbarItemViewHandle for ViewHandle { } fn row_count(&self, cx: &WindowContext) -> usize { - self.read(cx).row_count() + self.read_with(cx, |this, cx| this.row_count(cx)) } } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 28940a8367..4493634a8e 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -17,13 +17,13 @@ export default function search(): any { text: text(theme.highest, "mono", "default"), border: border(theme.highest), margin: { - right: 12, + right: 9, }, padding: { - top: 3, - bottom: 3, - left: 12, - right: 8, + top: 4, + bottom: 4, + left: 10, + right: 4, }, } @@ -34,6 +34,7 @@ export default function search(): any { } return { + padding: { top: 16, bottom: 16, left: 16, right: 16 }, // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive match_background: with_opacity( foreground(theme.highest, "accent"), @@ -42,76 +43,159 @@ export default function search(): any { option_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "on"), + icon_width: 14, + button_width: 32, + color: foreground(theme.highest, "variant"), background: background(theme.highest, "on"), - corner_radius: 6, - border: border(theme.highest, "on"), - margin: { - right: 4, + corner_radius: 2, + margin: { right: 2 }, + border: { + width: 1., color: background(theme.highest, "on") }, padding: { - bottom: 2, - left: 10, - right: 10, - top: 2, + left: 4, + right: 4, + top: 4, + bottom: 4, }, }, state: { hovered: { - ...text(theme.highest, "mono", "on", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), + border: { + width: 1., color: background(theme.highest, "on", "hovered") + }, }, clicked: { - ...text(theme.highest, "mono", "on", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed"), background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), + border: { + width: 1., color: background(theme.highest, "on", "pressed") + }, }, }, }), state: { active: { default: { - ...text(theme.highest, "mono", "accent"), + icon_width: 14, + button_width: 32, + color: foreground(theme.highest, "variant"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), }, hovered: { - ...text(theme.highest, "mono", "accent", "hovered"), + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "accent", "pressed"), + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), }, }, }, }), - action_button: interactive({ - base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - corner_radius: 6, - border: border(theme.highest, "on"), - margin: { - right: 4, + option_button_component: toggleable({ + base: interactive({ + base: { + icon_size: 14, + color: foreground(theme.highest, "variant"), + + button_width: 32, + background: background(theme.highest, "on"), + corner_radius: 2, + margin: { right: 2 }, + border: { + width: 1., color: background(theme.highest, "on") + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, }, - padding: { - bottom: 2, - left: 10, - right: 10, - top: 2, + 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") + }, + }, }, - }, + }), state: { - hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), - }, - clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), + active: { + default: { + icon_size: 14, + button_width: 32, + color: foreground(theme.highest, "variant"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), + }, + hovered: { + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), + }, + clicked: { + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), + }, }, }, }), + action_button: toggleable({ + base: interactive({ + base: { + ...text(theme.highest, "mono", "disabled"), + background: background(theme.highest, "disabled"), + corner_radius: 6, + border: border(theme.highest, "disabled"), + padding: { + // bottom: 2, + left: 10, + right: 10, + // top: 2, + }, + margin: { + right: 9, + } + }, + state: { + hovered: {} + }, + }), + state: { + active: interactive({ + base: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + border: border(theme.highest, "on"), + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }) + } + }), editor, invalid_editor: { ...editor, @@ -125,7 +209,7 @@ export default function search(): any { match_index: { ...text(theme.highest, "mono", "variant"), padding: { - left: 6, + left: 9, }, }, option_button_group: { @@ -140,28 +224,164 @@ export default function search(): any { right: 6, }, }, - results_status: { + major_results_status: { ...text(theme.highest, "mono", "on"), - size: 18, + size: 15, + }, + minor_results_status: { + ...text(theme.highest, "mono", "variant"), + size: 13, }, dismiss_button: interactive({ base: { color: foreground(theme.highest, "variant"), - icon_width: 12, - button_width: 14, + icon_width: 14, + button_width: 32, + corner_radius: 6, padding: { + // // top: 10, + // bottom: 10, left: 10, right: 10, }, + + background: background(theme.highest, "variant"), + + border: border(theme.highest, "on"), }, state: { hovered: { color: foreground(theme.highest, "hovered"), + background: background(theme.highest, "variant", "hovered") }, clicked: { color: foreground(theme.highest, "pressed"), + background: background(theme.highest, "variant", "pressed") }, }, }), + editor_icon: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/magnifying_glass_12.svg", + dimensions: { + width: 12, + height: 12, + } + }, + container: { + margin: { right: 6 }, + padding: { left: 2, right: 2 }, + } + }, + mode_button: toggleable({ + base: interactive({ + base: { + ...text(theme.highest, "mono", "variant"), + background: background(theme.highest, "variant"), + + border: { + ...border(theme.highest, "on"), + left: false, + right: false + }, + + padding: { + left: 10, + right: 10, + }, + corner_radius: 6, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "variant", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "variant", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }), + state: { + active: { + default: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on") + }, + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered") + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed") + }, + }, + }, + }), + nav_button: toggleable({ + state: { + inactive: interactive({ + base: { + background: background(theme.highest, "disabled"), + text: text(theme.highest, "mono", "disabled"), + corner_radius: 6, + border: { + ...border(theme.highest, "disabled"), + left: false, + right: false, + }, + + padding: { + left: 10, + right: 10, + }, + }, + state: { + hovered: {} + } + }), + active: interactive({ + base: { + text: text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 6, + border: { + ...border(theme.highest, "on"), + left: false, + right: false, + }, + + padding: { + left: 10, + right: 10, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }) + } + }), + search_bar_row_height: 32, + option_button_height: 22, + modes_container: { + margin: { + right: 9 + } + } + } } diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index e7b04246c4..129bd17869 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -84,6 +84,27 @@ export default function tab_bar(): any { bottom: false, }, } + const nav_button = interactive({ + base: { + color: foreground(theme.highest, "on"), + icon_width: 12, + + button_width: active_pane_active_tab.height, + border: border(theme.lowest, "on", { + bottom: true, + overlay: true, + }) + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + disabled: { + color: foreground(theme.highest, "on", "disabled") + }, + }, + }) const dragged_tab = { ...active_pane_active_tab, @@ -141,5 +162,6 @@ export default function tab_bar(): any { right: false, }, }, + nav_button: nav_button } } diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index d4eaeb99da..ecfb572f7e 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -133,23 +133,6 @@ export default function workspace(): any { background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, - nav_button: interactive({ - base: { - color: foreground(theme.highest, "on"), - icon_width: 12, - button_width: 24, - corner_radius: 6, - }, - state: { - hovered: { - color: foreground(theme.highest, "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - }, - disabled: { - color: foreground(theme.highest, "on", "disabled"), - }, - }, - }), toggleable_tool: toggleable_icon_button(theme, { margin: { left: 8 }, variant: "ghost", diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index e0da345bc5..ab3c96f280 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -1,4 +1,4 @@ -import chroma, { Scale, Color } from "chroma-js" +import { Scale, Color } from "chroma-js" import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" export { Syntax, ThemeSyntax, SyntaxHighlightStyle } import {