Merge branch 'main' into divs

This commit is contained in:
Nathan Sobo 2023-08-22 16:35:56 -06:00
commit d375f7992d
277 changed files with 19044 additions and 8896 deletions

View file

@ -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

View file

@ -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,
@ -36,7 +40,7 @@ pub enum Event {
}
pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::deploy);
cx.add_action(BufferSearchBar::deploy_bar);
cx.add_action(BufferSearchBar::dismiss);
cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::select_next_match);
@ -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::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
}
fn add_toggle_option_action<A: 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<Self>) -> AnyElement<Self> {
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<BufferSearchBar>| {
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<Self>| {
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<Self>) -> usize {
2
}
}
impl BufferSearchBar {
@ -304,6 +369,7 @@ impl BufferSearchBar {
query_contains_error: false,
dismissed: true,
search_history: SearchHistory::default(),
current_mode: SearchMode::default(),
}
}
@ -327,6 +393,19 @@ impl BufferSearchBar {
cx.notify();
}
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
if self.show(cx) {
self.search_suggested(cx);
if deploy.focus {
self.select_query(cx);
cx.focus_self();
}
return true;
}
false
}
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() {
return false;
@ -402,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<Self>,
) -> Option<AnyElement<Self>> {
if !option_supported {
return None;
}
let tooltip_style = theme::current(cx).tooltip.clone();
let is_active = self.search_options.contains(option);
Some(
MouseEventHandler::<Self, _>::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::<Self>(
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<Self>,
) -> AnyElement<Self> {
let action: Box<dyn Action>;
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::<NavButton, _>::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::<NavButton>(
direction as usize,
tooltip.to_string(),
Some(action),
tooltip_style,
cx,
)
.into_any()
}
fn render_action_button(
&self,
icon: &'static str,
@ -495,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::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
MouseEventHandler::new::<ActionButton, _>(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::<ActionButton>(
action_type_id,
tooltip.to_string(),
@ -518,56 +522,29 @@ impl BufferSearchBar {
.into_any()
}
fn render_close_button(
&self,
theme: &theme::Search,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let tooltip = "Dismiss Buffer Search";
let tooltip_style = theme::current(cx).tooltip.clone();
enum CloseButton {}
MouseEventHandler::<CloseButton, _>::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::<CloseButton>(
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<Self>) {
assert_ne!(
mode,
SearchMode::Semantic,
"Semantic search is not supported in buffer search"
);
if mode == self.current_mode {
return;
}
self.current_mode = mode;
let _ = self.update_matches(cx);
cx.notify();
}
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
let mut propagate_action = true;
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) {
search_bar.search_suggested(cx);
if action.focus {
search_bar.select_query(cx);
cx.focus_self();
}
if search_bar.deploy(action, cx) {
propagate_action = false;
}
});
}
if propagate_action {
cx.propagate_action();
}
@ -727,8 +704,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),
@ -823,6 +801,26 @@ impl BufferSearchBar {
let _ = self.search(&new_query, Some(self.search_options), cx);
}
}
fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
self.activate_search_mode(next_mode(&self.current_mode, false), cx);
}
fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
let mut should_propagate = true;
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
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)]

View file

@ -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<usize>,
}
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);
}
}

88
crates/search/src/mode.rs Normal file
View file

@ -0,0 +1,88 @@
use gpui::Action;
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
// TODO: Update the default search mode to get from config
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum SearchMode {
#[default]
Text,
Semantic,
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::Semantic => "Semantic",
SearchMode::Regex => "Regex",
}
}
pub(crate) fn region_id(&self) -> usize {
match self {
SearchMode::Text => 3,
SearchMode::Semantic => 4,
SearchMode::Regex => 5,
}
}
pub(crate) fn tooltip_text(&self) -> &'static str {
match self {
SearchMode::Text => "Activate Text Search",
SearchMode::Semantic => "Activate Semantic Search",
SearchMode::Regex => "Activate Regex Search",
}
}
pub(crate) fn activate_action(&self) -> Box<dyn Action> {
match self {
SearchMode::Text => Box::new(ActivateTextMode),
SearchMode::Semantic => Box::new(ActivateSemanticMode),
SearchMode::Regex => Box::new(ActivateRegexMode),
}
}
pub(crate) fn border_right(&self) -> bool {
match self {
SearchMode::Regex => true,
SearchMode::Text => true,
SearchMode::Semantic => true,
}
}
pub(crate) fn border_left(&self) -> bool {
match self {
SearchMode::Text => true,
_ => false,
}
}
pub(crate) fn button_side(&self) -> Option<Side> {
match self {
SearchMode::Text => Some(Side::Left),
SearchMode::Semantic => None,
SearchMode::Regex => Some(Side::Right),
}
}
}
pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
let next_text_state = if semantic_enabled {
SearchMode::Semantic
} else {
SearchMode::Regex
};
match mode {
SearchMode::Text => next_text_state,
SearchMode::Semantic => SearchMode::Regex,
SearchMode::Regex => SearchMode::Text,
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,20 @@
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, 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 +24,17 @@ pub fn init(cx: &mut AppContext) {
actions!(
search,
[
CycleMode,
ToggleWholeWord,
ToggleCaseSensitive,
ToggleRegex,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,
NextHistoryQuery,
PreviousHistoryQuery,
ActivateTextMode,
ActivateSemanticMode,
ActivateRegexMode
]
);
@ -33,7 +44,6 @@ bitflags! {
const NONE = 0b000;
const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010;
const REGEX = 0b100;
}
}
@ -42,7 +52,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 +68,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 +80,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<usize>,
}
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<V: View>(
&self,
active: bool,
tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle,
) -> AnyElement<V> {
ActionButton::new_dynamic(
self.to_toggle_action(),
format!("Toggle {}", self.label()),
tooltip_style,
)
.with_contents(theme::components::svg::Svg::new(self.icon()))
.toggleable(active)
.with_style(button_style)
.element()
.into_any()
}
}

View file

@ -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<V: View>(
tooltip: &'static str,
theme: &theme::Search,
cx: &mut ViewContext<V>,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
dismiss_action: Option<Box<dyn Action>>,
) -> AnyElement<V> {
let tooltip_style = theme::current(cx).tooltip.clone();
enum CloseButton {}
MouseEventHandler::new::<CloseButton, _>(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::<CloseButton>(0, tooltip.to_string(), dismiss_action, tooltip_style, cx)
.into_any()
}
pub(super) fn render_nav_button<V: View>(
icon: &'static str,
direction: Direction,
active: bool,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let action: Box<dyn Action>;
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::<NavButton, _>(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::<NavButton>(
direction as usize,
tooltip.to_string(),
Some(action),
tooltip_style,
cx,
)
.into_any()
}
pub(crate) fn render_search_mode_button<V: View>(
mode: SearchMode,
is_active: bool,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let tooltip_style = theme::current(cx).tooltip.clone();
enum SearchModeButton {}
MouseEventHandler::new::<SearchModeButton, _>(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::<SearchModeButton>(
mode.region_id(),
mode.tooltip_text().to_owned(),
Some(mode.activate_action()),
tooltip_style,
cx,
)
.into_any()
}
pub(crate) fn render_option_button_icon<V: View>(
is_active: bool,
icon: &'static str,
id: usize,
label: impl Into<Cow<'static, str>>,
action: Box<dyn Action>,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let tooltip_style = theme::current(cx).tooltip.clone();
MouseEventHandler::new::<V, _>(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::<V>(id, label, Some(action), tooltip_style, cx)
.into_any()
}