Project search design (#2834)
TODO before merging: - [x] Re-run project search when options (case, word, regex) change /cc @PixelJanitor Release Notes: - Revamped project & buffer search UI. - Added "Cycle Mode" command for search
This commit is contained in:
commit
8451e7eb7e
33 changed files with 2150 additions and 1064 deletions
|
@ -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
|
||||
|
|
|
@ -3313,11 +3313,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||
&mut self,
|
||||
element_id: usize,
|
||||
initial: T,
|
||||
) -> ElementStateHandle<T> {
|
||||
self.element_state_dynamic(TypeTag::new::<Tag>(), element_id, initial)
|
||||
}
|
||||
|
||||
pub fn element_state_dynamic<T: 'static>(
|
||||
&mut self,
|
||||
tag: TypeTag,
|
||||
element_id: usize,
|
||||
initial: T,
|
||||
) -> ElementStateHandle<T> {
|
||||
let id = ElementStateId {
|
||||
view_id: self.view_id(),
|
||||
element_id,
|
||||
tag: TypeId::of::<Tag>(),
|
||||
tag,
|
||||
};
|
||||
self.element_states
|
||||
.entry(id)
|
||||
|
@ -3331,11 +3340,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||
) -> ElementStateHandle<T> {
|
||||
self.element_state::<Tag, T>(element_id, T::default())
|
||||
}
|
||||
|
||||
pub fn default_element_state_dynamic<T: 'static + Default>(
|
||||
&mut self,
|
||||
tag: TypeTag,
|
||||
element_id: usize,
|
||||
) -> ElementStateHandle<T> {
|
||||
self.element_state_dynamic::<T>(tag, element_id, T::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct TypeTag {
|
||||
tag: TypeId,
|
||||
composed: Option<TypeId>,
|
||||
#[cfg(debug_assertions)]
|
||||
tag_type_name: &'static str,
|
||||
}
|
||||
|
@ -3344,6 +3362,7 @@ impl TypeTag {
|
|||
pub fn new<Tag: 'static>() -> Self {
|
||||
Self {
|
||||
tag: TypeId::of::<Tag>(),
|
||||
composed: None,
|
||||
#[cfg(debug_assertions)]
|
||||
tag_type_name: std::any::type_name::<Tag>(),
|
||||
}
|
||||
|
@ -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<T> {
|
||||
|
|
|
@ -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<dyn Action>;
|
||||
fn eq(&self, other: &dyn Action) -> bool;
|
||||
|
||||
|
@ -107,6 +110,10 @@ macro_rules! __impl_action {
|
|||
}
|
||||
}
|
||||
|
||||
fn type_tag(&self) -> $crate::TypeTag {
|
||||
$crate::TypeTag::new::<Self>()
|
||||
}
|
||||
|
||||
$from_json_fn
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<V: View>: 'static {
|
|||
FlexItem::new(self.into_any()).float()
|
||||
}
|
||||
|
||||
fn with_dynamic_tooltip(
|
||||
self,
|
||||
tag: TypeTag,
|
||||
id: usize,
|
||||
text: impl Into<Cow<'static, str>>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
style: TooltipStyle,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Tooltip<V>
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx)
|
||||
}
|
||||
fn with_tooltip<Tag: 'static>(
|
||||
self,
|
||||
id: usize,
|
||||
|
|
|
@ -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<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
||||
}
|
||||
|
||||
pub trait StyleableComponent {
|
||||
type Style: Clone;
|
||||
type Output: GeneralComponent;
|
||||
|
||||
fn with_style(self, style: Self::Style) -> Self::Output;
|
||||
}
|
||||
|
||||
impl GeneralComponent for () {
|
||||
fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleableComponent for () {
|
||||
type Style = ();
|
||||
type Output = ();
|
||||
|
||||
fn with_style(self, _: Self::Style) -> Self::Output {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Component<V: View> {
|
||||
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
||||
|
||||
|
@ -18,22 +44,32 @@ pub trait Component<V: View> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ComponentAdapter<V, E> {
|
||||
component: Option<E>,
|
||||
phantom: PhantomData<V>,
|
||||
impl<V: View, C: GeneralComponent> Component<V> for C {
|
||||
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
||||
self.render(v, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, V> ComponentAdapter<V, E> {
|
||||
pub struct ComponentAdapter<V: View, E> {
|
||||
component: Option<E>,
|
||||
element: Option<AnyElement<V>>,
|
||||
#[cfg(debug_assertions)]
|
||||
_component_name: &'static str,
|
||||
}
|
||||
|
||||
impl<E, V: View> ComponentAdapter<V, E> {
|
||||
pub fn new(e: E) -> Self {
|
||||
Self {
|
||||
component: Some(e),
|
||||
phantom: PhantomData,
|
||||
element: None,
|
||||
#[cfg(debug_assertions)]
|
||||
_component_name: std::any::type_name::<E>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
||||
type LayoutState = AnyElement<V>;
|
||||
type LayoutState = ();
|
||||
|
||||
type PaintState = ();
|
||||
|
||||
|
@ -43,10 +79,12 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
|||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
) -> (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<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
|||
scene: &mut SceneBuilder,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
layout: &mut Self::LayoutState,
|
||||
_: &mut Self::LayoutState,
|
||||
view: &mut V,
|
||||
cx: &mut PaintContext<V>,
|
||||
) -> 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<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
|||
range_utf16: std::ops::Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
element: &Self::LayoutState,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
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<V>,
|
||||
) -> 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<V: View> Tooltip<V> {
|
|||
child: AnyElement<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self {
|
||||
struct ElementState<Tag>(Tag);
|
||||
struct MouseEventHandlerState<Tag>(Tag);
|
||||
Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
|
||||
}
|
||||
|
||||
pub fn new_dynamic(
|
||||
mut tag: TypeTag,
|
||||
id: usize,
|
||||
text: impl Into<Cow<'static, str>>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
style: TooltipStyle,
|
||||
child: AnyElement<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self {
|
||||
tag = tag.compose(TypeTag::new::<Self>());
|
||||
|
||||
let focused_view_id = cx.focused_view_id();
|
||||
|
||||
let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
|
||||
let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
|
||||
let state = state_handle.read(cx).clone();
|
||||
let text = text.into();
|
||||
|
||||
|
@ -95,7 +107,7 @@ impl<V: View> Tooltip<V> {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let child = MouseEventHandler::new::<MouseEventHandlerState<Tag>, _>(id, cx, |_, _| child)
|
||||
let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
|
||||
.on_hover(move |e, _, cx| {
|
||||
let position = e.position;
|
||||
if e.started {
|
||||
|
|
|
@ -13,24 +13,39 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SearchInputs {
|
||||
query: Arc<str>,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
}
|
||||
|
||||
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<AhoCorasick<usize>>,
|
||||
query: Arc<str>,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
Regex {
|
||||
regex: Regex,
|
||||
query: Arc<str>,
|
||||
|
||||
multiline: bool,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
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<Vec<PathMatcher>> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::<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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<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::new::<Self, _>(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::new::<NavButton, _>(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,
|
||||
|
@ -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::<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(),
|
||||
|
@ -531,39 +522,13 @@ 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::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()
|
||||
.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>) {
|
||||
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<Pane>) {
|
||||
|
@ -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>) {
|
||||
self.activate_search_mode(next_mode(&self.current_mode), 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)]
|
||||
|
|
184
crates/search/src/history.rs
Normal file
184
crates/search/src/history.rs
Normal 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);
|
||||
}
|
||||
}
|
74
crates/search/src/mode.rs
Normal file
74
crates/search/src/mode.rs
Normal file
|
@ -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<dyn Action> {
|
||||
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<Side> {
|
||||
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,
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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<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(Svg::new(self.icon()))
|
||||
.toggleable(active)
|
||||
.with_style(button_style)
|
||||
.into_element()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
|
201
crates/search/src/search_bar.rs
Normal file
201
crates/search/src/search_bar.rs
Normal 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()
|
||||
}
|
|
@ -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::<SemanticIndexSettings>(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<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
let worktrees_indexed_previously = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(non_snake_case, non_upper_case_globals)]
|
||||
|
||||
mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_store;
|
||||
|
|
|
@ -483,10 +483,8 @@ fn possible_open_targets(
|
|||
}
|
||||
|
||||
pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
344
crates/theme/src/components.rs
Normal file
344
crates/theme/src/components.rs
Normal file
|
@ -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<Interactive<ButtonStyle<SvgStyle>>>;
|
||||
|
||||
pub trait ComponentExt<C: StyleableComponent> {
|
||||
fn toggleable(self, active: bool) -> Toggle<C, ()>;
|
||||
}
|
||||
|
||||
impl<C: StyleableComponent> ComponentExt<C> for C {
|
||||
fn toggleable(self, active: bool) -> Toggle<C, ()> {
|
||||
Toggle::new(self, active)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod toggle {
|
||||
use gpui::elements::{GeneralComponent, StyleableComponent};
|
||||
|
||||
use crate::Toggleable;
|
||||
|
||||
pub struct Toggle<C, S> {
|
||||
style: S,
|
||||
active: bool,
|
||||
component: C,
|
||||
}
|
||||
|
||||
impl<C: StyleableComponent> Toggle<C, ()> {
|
||||
pub fn new(component: C, active: bool) -> Self {
|
||||
Toggle {
|
||||
active,
|
||||
component,
|
||||
style: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: StyleableComponent> StyleableComponent for Toggle<C, ()> {
|
||||
type Style = Toggleable<C::Style>;
|
||||
|
||||
type Output = Toggle<C, Self::Style>;
|
||||
|
||||
fn with_style(self, style: Self::Style) -> Self::Output {
|
||||
Toggle {
|
||||
active: self.active,
|
||||
component: self.component,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: StyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> {
|
||||
fn render<V: gpui::View>(
|
||||
self,
|
||||
v: &mut V,
|
||||
cx: &mut gpui::ViewContext<V>,
|
||||
) -> gpui::AnyElement<V> {
|
||||
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<C, S> {
|
||||
action: Box<dyn Action>,
|
||||
tooltip: Cow<'static, str>,
|
||||
tooltip_style: TooltipStyle,
|
||||
tag: TypeTag,
|
||||
contents: C,
|
||||
style: Interactive<S>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct ButtonStyle<C> {
|
||||
#[serde(flatten)]
|
||||
container: ContainerStyle,
|
||||
button_width: Option<f32>,
|
||||
button_height: Option<f32>,
|
||||
#[serde(flatten)]
|
||||
contents: C,
|
||||
}
|
||||
|
||||
impl ActionButton<(), ()> {
|
||||
pub fn new_dynamic(
|
||||
action: Box<dyn Action>,
|
||||
tooltip: impl Into<Cow<'static, str>>,
|
||||
tooltip_style: TooltipStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
contents: (),
|
||||
tag: action.type_tag(),
|
||||
style: Interactive::new_blank(),
|
||||
tooltip: tooltip.into(),
|
||||
tooltip_style,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<A: Action + Clone>(
|
||||
action: A,
|
||||
tooltip: impl Into<Cow<'static, str>>,
|
||||
tooltip_style: TooltipStyle,
|
||||
) -> Self {
|
||||
Self::new_dynamic(Box::new(action), tooltip, tooltip_style)
|
||||
}
|
||||
|
||||
pub fn with_contents<C: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
|
||||
ActionButton {
|
||||
action: self.action,
|
||||
tag: self.tag,
|
||||
style: self.style,
|
||||
tooltip: self.tooltip,
|
||||
tooltip_style: self.tooltip_style,
|
||||
contents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> {
|
||||
type Style = Interactive<ButtonStyle<C::Style>>;
|
||||
type Output = ActionButton<C, ButtonStyle<C::Style>>;
|
||||
|
||||
fn with_style(self, style: Self::Style) -> Self::Output {
|
||||
ActionButton {
|
||||
action: self.action,
|
||||
tag: self.tag,
|
||||
contents: self.contents,
|
||||
tooltip: self.tooltip,
|
||||
tooltip_style: self.tooltip_style,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: StyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> {
|
||||
fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
|
||||
MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| {
|
||||
let style = self.style.style_for(state);
|
||||
let mut contents = self
|
||||
.contents
|
||||
.with_style(style.contents.to_owned())
|
||||
.render(v, cx)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained();
|
||||
|
||||
if let Some(height) = style.button_height {
|
||||
contents = contents.with_height(height);
|
||||
}
|
||||
|
||||
if let Some(width) = style.button_width {
|
||||
contents = contents.with_width(width);
|
||||
}
|
||||
|
||||
contents.into_any()
|
||||
})
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = self.action.boxed_clone();
|
||||
move |_, _, cx| {
|
||||
let window = cx.window();
|
||||
let view = cx.view_id();
|
||||
let action = action.boxed_clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
window.dispatch_action(view, action.as_ref(), &mut cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_dynamic_tooltip(
|
||||
self.tag,
|
||||
0,
|
||||
self.tooltip,
|
||||
Some(self.action),
|
||||
self.tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod svg {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use gpui::{
|
||||
elements::{GeneralComponent, StyleableComponent},
|
||||
Element,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Default, JsonSchema)]
|
||||
pub struct SvgStyle {
|
||||
icon_width: f32,
|
||||
icon_height: f32,
|
||||
color: gpui::color::Color,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SvgStyle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum IconSize {
|
||||
IconSize { icon_size: f32 },
|
||||
Dimensions { width: f32, height: f32 },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SvgStyleHelper {
|
||||
#[serde(flatten)]
|
||||
size: IconSize,
|
||||
color: gpui::color::Color,
|
||||
}
|
||||
|
||||
let json = SvgStyleHelper::deserialize(deserializer)?;
|
||||
let color = json.color;
|
||||
|
||||
let result = match json.size {
|
||||
IconSize::IconSize { icon_size } => SvgStyle {
|
||||
icon_width: icon_size,
|
||||
icon_height: icon_size,
|
||||
color,
|
||||
},
|
||||
IconSize::Dimensions { width, height } => SvgStyle {
|
||||
icon_width: width,
|
||||
icon_height: height,
|
||||
color,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Svg<S> {
|
||||
path: Cow<'static, str>,
|
||||
style: S,
|
||||
}
|
||||
|
||||
impl Svg<()> {
|
||||
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
style: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleableComponent for Svg<()> {
|
||||
type Style = SvgStyle;
|
||||
|
||||
type Output = Svg<SvgStyle>;
|
||||
|
||||
fn with_style(self, style: Self::Style) -> Self::Output {
|
||||
Svg {
|
||||
path: self.path,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneralComponent for Svg<SvgStyle> {
|
||||
fn render<V: gpui::View>(
|
||||
self,
|
||||
_: &mut V,
|
||||
_: &mut gpui::ViewContext<V>,
|
||||
) -> gpui::AnyElement<V> {
|
||||
gpui::elements::Svg::new(self.path)
|
||||
.with_color(self.style.color)
|
||||
.constrained()
|
||||
.with_width(self.style.icon_width)
|
||||
.with_height(self.style.icon_height)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod label {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use gpui::{
|
||||
elements::{GeneralComponent, LabelStyle, StyleableComponent},
|
||||
Element,
|
||||
};
|
||||
|
||||
pub struct Label<S> {
|
||||
text: Cow<'static, str>,
|
||||
style: S,
|
||||
}
|
||||
|
||||
impl Label<()> {
|
||||
pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
style: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleableComponent for Label<()> {
|
||||
type Style = LabelStyle;
|
||||
|
||||
type Output = Label<LabelStyle>;
|
||||
|
||||
fn with_style(self, style: Self::Style) -> Self::Output {
|
||||
Label {
|
||||
text: self.text,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneralComponent for Label<LabelStyle> {
|
||||
fn render<V: gpui::View>(
|
||||
self,
|
||||
_: &mut V,
|
||||
_: &mut gpui::ViewContext<V>,
|
||||
) -> gpui::AnyElement<V> {
|
||||
gpui::elements::Label::new(self.text, self.style).into_any()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
pub mod components;
|
||||
mod theme_registry;
|
||||
mod theme_settings;
|
||||
pub mod ui;
|
||||
|
||||
use components::ToggleIconButtonStyle;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
|
||||
|
@ -13,7 +15,7 @@ use serde::{de::DeserializeOwned, Deserialize};
|
|||
use serde_json::Value;
|
||||
use settings::SettingsStore;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
|
||||
use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
|
||||
|
||||
pub use theme_registry::*;
|
||||
pub use theme_settings::*;
|
||||
|
@ -182,7 +184,7 @@ pub struct CopilotAuth {
|
|||
pub prompting: CopilotAuthPrompting,
|
||||
pub not_authorized: CopilotAuthNotAuthorized,
|
||||
pub authorized: CopilotAuthAuthorized,
|
||||
pub cta_button: ButtonStyle,
|
||||
pub cta_button: CopilotCTAButton,
|
||||
pub header: IconStyle,
|
||||
}
|
||||
|
||||
|
@ -196,7 +198,7 @@ pub struct CopilotAuthPrompting {
|
|||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct DeviceCode {
|
||||
pub text: TextStyle,
|
||||
pub cta: ButtonStyle,
|
||||
pub cta: CopilotCTAButton,
|
||||
pub left: f32,
|
||||
pub left_container: ContainerStyle,
|
||||
pub right: f32,
|
||||
|
@ -334,6 +336,7 @@ pub struct TabBar {
|
|||
pub inactive_pane: TabStyles,
|
||||
pub dragged_tab: Tab,
|
||||
pub height: f32,
|
||||
pub nav_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
|
@ -398,7 +401,6 @@ pub struct Toolbar {
|
|||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub item_spacing: f32,
|
||||
pub nav_button: Interactive<IconButton>,
|
||||
pub toggleable_tool: Toggleable<Interactive<IconButton>>,
|
||||
}
|
||||
|
||||
|
@ -419,12 +421,20 @@ pub struct Search {
|
|||
pub include_exclude_editor: FindEditor,
|
||||
pub invalid_include_exclude_editor: ContainerStyle,
|
||||
pub include_exclude_inputs: ContainedText,
|
||||
pub option_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub action_button: Interactive<ContainedText>,
|
||||
pub option_button: Toggleable<Interactive<IconButton>>,
|
||||
pub option_button_component: ToggleIconButtonStyle,
|
||||
pub action_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub match_background: Color,
|
||||
pub match_index: ContainedText,
|
||||
pub results_status: TextStyle,
|
||||
pub major_results_status: TextStyle,
|
||||
pub minor_results_status: TextStyle,
|
||||
pub dismiss_button: Interactive<IconButton>,
|
||||
pub editor_icon: IconStyle,
|
||||
pub mode_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub nav_button: Toggleable<Interactive<ContainedLabel>>,
|
||||
pub search_bar_row_height: f32,
|
||||
pub option_button_height: f32,
|
||||
pub modes_container: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
|
@ -880,12 +890,32 @@ pub struct Interactive<T> {
|
|||
pub disabled: Option<T>,
|
||||
}
|
||||
|
||||
impl Interactive<()> {
|
||||
pub fn new_blank() -> Self {
|
||||
Self {
|
||||
default: (),
|
||||
hovered: None,
|
||||
clicked: None,
|
||||
disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct Toggleable<T> {
|
||||
active: T,
|
||||
inactive: T,
|
||||
}
|
||||
|
||||
impl Toggleable<()> {
|
||||
pub fn new_blank() -> Self {
|
||||
Self {
|
||||
active: (),
|
||||
inactive: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Toggleable<T> {
|
||||
pub fn new(active: T, inactive: T) -> Self {
|
||||
Self { active, inactive }
|
||||
|
|
|
@ -145,12 +145,12 @@ pub fn keystroke_label<V: View>(
|
|||
.with_style(label_style.container)
|
||||
}
|
||||
|
||||
pub type ButtonStyle = Interactive<ContainedText>;
|
||||
pub type CopilotCTAButton = Interactive<ContainedText>;
|
||||
|
||||
pub fn cta_button<Tag, L, V, F>(
|
||||
label: L,
|
||||
max_width: f32,
|
||||
style: &ButtonStyle,
|
||||
style: &CopilotCTAButton,
|
||||
cx: &mut ViewContext<V>,
|
||||
f: F,
|
||||
) -> MouseEventHandler<V>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use gpui::{actions, impl_actions, AppContext, ViewContext};
|
||||
use search::{buffer_search, BufferSearchBar, SearchOptions};
|
||||
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{searchable::Direction, Pane, Workspace};
|
||||
|
||||
|
@ -65,10 +65,8 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
|
|||
cx.focus_self();
|
||||
|
||||
if query.is_empty() {
|
||||
search_bar.set_search_options(
|
||||
SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
|
||||
cx,
|
||||
);
|
||||
search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
||||
}
|
||||
vim.state.search = SearchState {
|
||||
direction,
|
||||
|
|
|
@ -222,6 +222,56 @@ impl TabBarContextMenu {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
|
||||
svg_path: &'static str,
|
||||
style: theme::Interactive<theme::IconButton>,
|
||||
nav_button_height: f32,
|
||||
tooltip_style: TooltipStyle,
|
||||
enabled: bool,
|
||||
on_click: F,
|
||||
tooltip_action: A,
|
||||
action_name: &str,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> AnyElement<Pane> {
|
||||
MouseEventHandler::new::<A, _>(0, cx, |state, _| {
|
||||
let style = if enabled {
|
||||
style.style_for(state)
|
||||
} else {
|
||||
style.disabled_style()
|
||||
};
|
||||
Svg::new(svg_path)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(nav_button_height)
|
||||
.aligned()
|
||||
.top()
|
||||
})
|
||||
.with_cursor_style(if enabled {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::default()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, toolbar, cx| {
|
||||
on_click(toolbar, cx)
|
||||
})
|
||||
.with_tooltip::<A>(
|
||||
0,
|
||||
action_name.to_string(),
|
||||
Some(Box::new(tooltip_action)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.into_any_named("nav button")
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
|
@ -253,7 +303,7 @@ impl Pane {
|
|||
pane: handle.clone(),
|
||||
next_timestamp,
|
||||
}))),
|
||||
toolbar: cx.add_view(|_| Toolbar::new(Some(handle))),
|
||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
||||
tab_bar_context_menu: TabBarContextMenu {
|
||||
kind: TabBarContextMenuKind::New,
|
||||
handle: context_menu,
|
||||
|
@ -265,7 +315,7 @@ impl Pane {
|
|||
has_focus: false,
|
||||
can_drop: Rc::new(|_, _| true),
|
||||
can_split: true,
|
||||
render_tab_bar_buttons: Rc::new(|pane, cx| {
|
||||
render_tab_bar_buttons: Rc::new(move |pane, cx| {
|
||||
Flex::row()
|
||||
// New menu
|
||||
.with_child(Self::render_tab_bar_button(
|
||||
|
@ -1571,8 +1621,70 @@ impl View for Pane {
|
|||
},
|
||||
),
|
||||
);
|
||||
let tooltip_style = theme.tooltip.clone();
|
||||
let tab_bar_theme = theme.workspace.tab_bar.clone();
|
||||
|
||||
let nav_button_height = tab_bar_theme.height;
|
||||
let button_style = tab_bar_theme.nav_button;
|
||||
let border_for_nav_buttons = tab_bar_theme
|
||||
.tab_style(false, false)
|
||||
.container
|
||||
.border
|
||||
.clone();
|
||||
|
||||
let mut tab_row = Flex::row()
|
||||
.with_child(nav_button(
|
||||
"icons/arrow_left_16.svg",
|
||||
button_style.clone(),
|
||||
nav_button_height,
|
||||
tooltip_style.clone(),
|
||||
self.can_navigate_backward(),
|
||||
{
|
||||
move |pane, cx| {
|
||||
if let Some(workspace) = pane.workspace.upgrade(cx) {
|
||||
let pane = cx.weak_handle();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.go_back(pane, cx)
|
||||
.detach_and_log_err(cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoBack,
|
||||
"Go Back",
|
||||
cx,
|
||||
))
|
||||
.with_child(
|
||||
nav_button(
|
||||
"icons/arrow_right_16.svg",
|
||||
button_style.clone(),
|
||||
nav_button_height,
|
||||
tooltip_style,
|
||||
self.can_navigate_forward(),
|
||||
{
|
||||
move |pane, cx| {
|
||||
if let Some(workspace) = pane.workspace.upgrade(cx) {
|
||||
let pane = cx.weak_handle();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.go_forward(pane, cx)
|
||||
.detach_and_log_err(cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoForward,
|
||||
"Go Forward",
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_border(border_for_nav_buttons),
|
||||
)
|
||||
.with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
|
||||
|
||||
if self.has_focus {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{ItemHandle, Pane};
|
||||
use crate::ItemHandle;
|
||||
use gpui::{
|
||||
elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle,
|
||||
AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
pub trait ToolbarItemView: View {
|
||||
|
@ -25,7 +25,7 @@ pub trait ToolbarItemView: View {
|
|||
/// Number of times toolbar's height will be repeated to get the effective height.
|
||||
/// Useful when multiple rows one under each other are needed.
|
||||
/// The rows have the same width and act as a whole when reacting to resizes and similar events.
|
||||
fn row_count(&self) -> usize {
|
||||
fn row_count(&self, _cx: &ViewContext<Self>) -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ pub struct Toolbar {
|
|||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
hidden: bool,
|
||||
can_navigate: bool,
|
||||
pane: Option<WeakViewHandle<Pane>>,
|
||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||
}
|
||||
|
||||
|
@ -118,76 +117,10 @@ impl View for Toolbar {
|
|||
}
|
||||
}
|
||||
|
||||
let pane = self.pane.clone();
|
||||
let mut enable_go_backward = false;
|
||||
let mut enable_go_forward = false;
|
||||
if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) {
|
||||
let pane = pane.read(cx);
|
||||
enable_go_backward = pane.can_navigate_backward();
|
||||
enable_go_forward = pane.can_navigate_forward();
|
||||
}
|
||||
|
||||
let container_style = theme.container;
|
||||
let height = theme.height * primary_items_row_count as f32;
|
||||
let nav_button_height = theme.height;
|
||||
let button_style = theme.nav_button;
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
let mut primary_items = Flex::row();
|
||||
if self.can_navigate {
|
||||
primary_items.add_child(nav_button(
|
||||
"icons/arrow_left_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style.clone(),
|
||||
enable_go_backward,
|
||||
spacing,
|
||||
{
|
||||
move |toolbar, cx| {
|
||||
if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
|
||||
{
|
||||
if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
|
||||
let pane = pane.downgrade();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.go_back(pane, cx).detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoBack,
|
||||
"Go Back",
|
||||
cx,
|
||||
));
|
||||
primary_items.add_child(nav_button(
|
||||
"icons/arrow_right_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style,
|
||||
enable_go_forward,
|
||||
spacing,
|
||||
{
|
||||
move |toolbar, cx| {
|
||||
if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
|
||||
{
|
||||
if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
|
||||
let pane = pane.downgrade();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.go_forward(pane, cx).detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoForward,
|
||||
"Go Forward",
|
||||
cx,
|
||||
));
|
||||
}
|
||||
primary_items.extend(primary_left_items);
|
||||
primary_items.extend(primary_right_items);
|
||||
|
||||
|
@ -210,63 +143,65 @@ impl View for Toolbar {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
|
||||
svg_path: &'static str,
|
||||
style: theme::Interactive<theme::IconButton>,
|
||||
nav_button_height: f32,
|
||||
tooltip_style: TooltipStyle,
|
||||
enabled: bool,
|
||||
spacing: f32,
|
||||
on_click: F,
|
||||
tooltip_action: A,
|
||||
action_name: &'static str,
|
||||
cx: &mut ViewContext<Toolbar>,
|
||||
) -> AnyElement<Toolbar> {
|
||||
MouseEventHandler::new::<A, _>(0, cx, |state, _| {
|
||||
let style = if enabled {
|
||||
style.style_for(state)
|
||||
} else {
|
||||
style.disabled_style()
|
||||
};
|
||||
Svg::new(svg_path)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(nav_button_height)
|
||||
.aligned()
|
||||
.top()
|
||||
})
|
||||
.with_cursor_style(if enabled {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::default()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, toolbar, cx| {
|
||||
on_click(toolbar, cx)
|
||||
})
|
||||
.with_tooltip::<A>(
|
||||
0,
|
||||
action_name,
|
||||
Some(Box::new(tooltip_action)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_right(spacing)
|
||||
.into_any_named("nav button")
|
||||
}
|
||||
// <<<<<<< HEAD
|
||||
// =======
|
||||
// #[allow(clippy::too_many_arguments)]
|
||||
// fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
|
||||
// svg_path: &'static str,
|
||||
// style: theme::Interactive<theme::IconButton>,
|
||||
// nav_button_height: f32,
|
||||
// tooltip_style: TooltipStyle,
|
||||
// enabled: bool,
|
||||
// spacing: f32,
|
||||
// on_click: F,
|
||||
// tooltip_action: A,
|
||||
// action_name: &'static str,
|
||||
// cx: &mut ViewContext<Toolbar>,
|
||||
// ) -> AnyElement<Toolbar> {
|
||||
// MouseEventHandler::new::<A, _>(0, cx, |state, _| {
|
||||
// let style = if enabled {
|
||||
// style.style_for(state)
|
||||
// } else {
|
||||
// style.disabled_style()
|
||||
// };
|
||||
// Svg::new(svg_path)
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(nav_button_height)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// })
|
||||
// .with_cursor_style(if enabled {
|
||||
// CursorStyle::PointingHand
|
||||
// } else {
|
||||
// CursorStyle::default()
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, toolbar, cx| {
|
||||
// on_click(toolbar, cx)
|
||||
// })
|
||||
// .with_tooltip::<A>(
|
||||
// 0,
|
||||
// action_name,
|
||||
// Some(Box::new(tooltip_action)),
|
||||
// tooltip_style,
|
||||
// cx,
|
||||
// )
|
||||
// .contained()
|
||||
// .with_margin_right(spacing)
|
||||
// .into_any_named("nav button")
|
||||
// }
|
||||
|
||||
// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e
|
||||
impl Toolbar {
|
||||
pub fn new(pane: Option<WeakViewHandle<Pane>>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_item: None,
|
||||
pane,
|
||||
items: Default::default(),
|
||||
hidden: false,
|
||||
can_navigate: true,
|
||||
|
@ -362,7 +297,7 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
|||
}
|
||||
|
||||
fn row_count(&self, cx: &WindowContext) -> usize {
|
||||
self.read(cx).row_count()
|
||||
self.read_with(cx, |this, cx| this.row_count(cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue