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