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:
Kyle Caverly 2023-08-18 14:38:01 +02:00 committed by GitHub
commit 8451e7eb7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 2150 additions and 1064 deletions

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="12px" viewBox="0 0 12 12" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 2.976562 2.746094 L 4.226562 2.746094 L 6.105469 9.296875 L 5.285156 9.296875 L 4.804688 7.640625 L 2.386719 7.640625 L 1.914062 9.296875 L 1.097656 9.296875 Z M 4.621094 6.917969 L 3.640625 3.449219 L 3.5625 3.449219 L 2.582031 6.917969 Z M 4.621094 6.917969 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 2.878906 2.617188 L 4.324219 2.617188 L 6.277344 9.425781 L 5.191406 9.425781 L 4.707031 7.769531 L 2.484375 7.769531 L 2.011719 9.425781 L 0.925781 9.425781 Z M 3.601562 3.785156 L 2.75 6.789062 L 4.453125 6.789062 Z M 3.601562 3.785156 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 7.285156 9.378906 L 7.28125 9.378906 C 7.03125 9.277344 6.851562 9.101562 6.738281 8.859375 L 6.738281 8.855469 C 6.636719 8.621094 6.59375 8.296875 6.59375 7.894531 C 6.59375 7.421875 6.660156 7.035156 6.8125 6.757812 C 6.964844 6.464844 7.21875 6.265625 7.5625 6.152344 C 7.90625 6.039062 8.375 5.980469 8.96875 5.980469 L 9.75 5.980469 L 9.75 5.867188 C 9.75 5.605469 9.71875 5.417969 9.65625 5.292969 L 9.65625 5.289062 C 9.605469 5.175781 9.519531 5.09375 9.378906 5.039062 C 9.238281 4.984375 9.023438 4.949219 8.726562 4.949219 C 8.535156 4.949219 8.378906 4.964844 8.253906 4.988281 C 8.128906 5.011719 8.046875 5.042969 8 5.082031 L 7.996094 5.082031 C 7.902344 5.144531 7.832031 5.285156 7.820312 5.554688 L 7.8125 5.675781 L 6.746094 5.675781 L 6.746094 5.546875 C 6.746094 5.164062 6.804688 4.851562 6.925781 4.625 C 7.054688 4.382812 7.273438 4.21875 7.5625 4.128906 C 7.847656 4.03125 8.238281 3.984375 8.726562 3.984375 C 9.238281 3.984375 9.640625 4.039062 9.933594 4.148438 C 10.242188 4.261719 10.464844 4.464844 10.589844 4.75 C 10.714844 5.027344 10.773438 5.402344 10.773438 5.867188 L 10.773438 9.429688 L 9.78125 9.429688 L 9.78125 8.964844 C 9.644531 9.140625 9.488281 9.269531 9.316406 9.355469 C 9.078125 9.472656 8.730469 9.523438 8.289062 9.523438 C 7.871094 9.523438 7.53125 9.480469 7.285156 9.378906 Z M 8.058594 7.039062 C 7.914062 7.085938 7.816406 7.167969 7.753906 7.277344 C 7.699219 7.386719 7.664062 7.558594 7.664062 7.808594 C 7.664062 8.132812 7.730469 8.3125 7.824219 8.398438 C 7.921875 8.480469 8.132812 8.542969 8.496094 8.542969 C 8.84375 8.542969 9.097656 8.488281 9.261719 8.394531 C 9.421875 8.296875 9.546875 8.136719 9.621094 7.894531 C 9.691406 7.671875 9.734375 7.351562 9.742188 6.929688 L 8.777344 6.9375 C 8.460938 6.945312 8.222656 6.980469 8.0625 7.035156 Z M 8.058594 7.039062 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6748 1.40617C10.8058 1.24248 10.6892 1 10.4796 1H1.51991C1.31028 1 1.19374 1.24248 1.32469 1.40617L4.14578 4.93255C4.34144 5.1771 4.44803 5.48097 4.44803 5.79421C4.44803 6.4689 4.44803 9.33412 4.44803 10.5017C4.44803 10.7779 4.67189 11 4.94803 11H7.05148C7.32762 11 7.55148 10.7779 7.55148 10.5017C7.55148 9.33412 7.55148 6.4689 7.55148 5.79421C7.55148 5.48097 7.6581 5.1771 7.85376 4.93255L10.6748 1.40617Z" stroke="#787D87" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
<g id="surface1">
<path style="fill:none;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(47.058824%,49.019608%,52.941176%);stroke-opacity:1;stroke-miterlimit:4;" d="M 10.674107 1.40625 C 10.804688 1.242188 10.690848 1.001116 10.479911 1.001116 L 1.520089 1.001116 C 1.309152 1.001116 1.195312 1.242188 1.325893 1.40625 L 4.145089 4.93192 C 4.342634 5.176339 4.446429 5.481027 4.446429 5.795759 C 4.446429 6.46875 4.446429 9.334821 4.446429 10.503348 C 4.446429 10.777902 4.670759 10.998884 4.948661 10.998884 L 7.051339 10.998884 C 7.329241 10.998884 7.550223 10.777902 7.550223 10.503348 C 7.550223 9.334821 7.550223 6.46875 7.550223 5.795759 C 7.550223 5.481027 7.657366 5.176339 7.854911 4.93192 Z M 10.674107 1.40625 " transform="matrix(1.166667,0,0,1.166667,0,0)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 991 B

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="12px" viewBox="0 0 12 12" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 4.070312 8.132812 L 3.488281 5.171875 L 2.902344 8.132812 L 1.867188 8.132812 L 0.859375 3.433594 L 1.949219 3.433594 L 2.414062 6.359375 L 2.988281 3.445312 L 3.992188 3.445312 L 4.558594 6.351562 L 5.019531 3.433594 L 6.113281 3.433594 L 5.105469 8.132812 Z M 4.070312 8.132812 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 7.414062 7.992188 C 7.125 7.828125 6.925781 7.558594 6.8125 7.207031 C 6.699219 6.859375 6.644531 6.378906 6.644531 5.773438 C 6.644531 5.175781 6.695312 4.699219 6.804688 4.359375 C 6.917969 4.007812 7.117188 3.738281 7.40625 3.582031 C 7.6875 3.421875 8.066406 3.351562 8.527344 3.351562 C 8.863281 3.351562 9.140625 3.390625 9.339844 3.484375 C 9.433594 3.527344 9.523438 3.589844 9.601562 3.667969 L 9.601562 1.738281 L 10.636719 1.738281 L 10.636719 8.128906 L 9.601562 8.128906 L 9.601562 7.878906 C 9.507812 7.964844 9.410156 8.03125 9.308594 8.082031 C 9.113281 8.175781 8.855469 8.214844 8.554688 8.214844 C 8.085938 8.214844 7.703125 8.144531 7.417969 7.992188 Z M 9.527344 6.734375 C 9.585938 6.515625 9.621094 6.199219 9.621094 5.773438 C 9.621094 5.355469 9.585938 5.042969 9.527344 4.832031 C 9.46875 4.628906 9.378906 4.507812 9.269531 4.441406 L 9.265625 4.4375 C 9.15625 4.367188 8.976562 4.316406 8.703125 4.316406 C 8.40625 4.316406 8.203125 4.367188 8.078125 4.441406 L 8.074219 4.445312 C 7.953125 4.511719 7.859375 4.632812 7.792969 4.835938 C 7.730469 5.042969 7.695312 5.355469 7.695312 5.773438 C 7.695312 6.199219 7.730469 6.511719 7.792969 6.71875 C 7.859375 6.921875 7.957031 7.050781 8.078125 7.121094 C 8.203125 7.199219 8.40625 7.246094 8.703125 7.246094 C 8.976562 7.246094 9.15625 7.203125 9.265625 7.132812 C 9.375 7.0625 9.46875 6.9375 9.527344 6.734375 Z M 9.527344 6.734375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 1.519531 9.1875 L 10.480469 9.1875 C 10.777344 9.1875 11.023438 9.429688 11.023438 9.730469 C 11.023438 10.03125 10.777344 10.273438 10.480469 10.273438 L 1.519531 10.273438 C 1.222656 10.273438 0.976562 10.03125 0.976562 9.730469 C 0.976562 9.429688 1.222656 9.1875 1.519531 9.1875 Z M 1.519531 9.1875 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.74672 9.48686L4.07031 6.03232L3.38584 9.48686H2.17614L1.00281 4.00781H2.27559L2.81566 7.41754L3.48439 4.01752H4.65865L5.31819 7.41176L5.85736 4.00781H7.13014L5.9568 9.48686H4.74672Z" fill="#787D87"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64907 9.32382C8.313 9.13287 8.08213 8.81954 7.94725 8.4078C7.8147 8.00318 7.75317 7.44207 7.75317 6.73677C7.75317 6.03845 7.81141 5.48454 7.9369 5.08716L7.93755 5.08512C8.07231 4.67373 8.3034 4.36258 8.64088 4.17794C8.96806 3.99257 9.41119 3.9104 9.9496 3.9104C10.3406 3.9104 10.6632 3.95585 10.8967 4.06485C11.0079 4.11675 11.1099 4.18844 11.2033 4.27745V2.03027H12.4077V9.4856H11.2033V9.18983C11.0945 9.29074 10.98 9.37096 10.8591 9.42752C10.6327 9.53648 10.3335 9.58252 9.97867 9.58252C9.4339 9.58252 8.98592 9.50355 8.65375 9.3264L8.64907 9.32382ZM11.1139 7.85508C11.1841 7.60311 11.2227 7.23354 11.2227 6.73677C11.2227 6.24602 11.1841 5.88331 11.1141 5.63844C11.0457 5.39902 10.9401 5.25863 10.8149 5.18266L10.8077 5.17826C10.6804 5.09342 10.4713 5.03726 10.1531 5.03726C9.80785 5.03726 9.5719 5.09359 9.42256 5.1832L9.41829 5.18576C9.28002 5.26412 9.16722 5.40602 9.09399 5.64263C9.01876 5.88566 8.97694 6.24668 8.97694 6.73677C8.97694 7.23363 9.01882 7.59774 9.09399 7.8406C9.1673 8.07745 9.28097 8.22477 9.42256 8.30972C9.5719 8.39933 9.80785 8.45566 10.1531 8.45566C10.4721 8.45566 10.683 8.40265 10.8114 8.32216C10.9396 8.23944 11.0456 8.09373 11.1139 7.85508Z" fill="#787D87"/>
<rect x="1.14087" y="10.7188" width="11.7183" height="1.26565" rx="0.632824" fill="#787D87"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -225,7 +225,8 @@
"tab": "buffer_search::FocusEditor",
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches"
"alt-enter": "search::SelectAllMatches",
"alt-tab": "search::CycleMode"
}
},
{
@ -238,7 +239,8 @@
{
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus"
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode"
}
},
{
@ -251,7 +253,8 @@
{
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus"
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode"
}
},
{
@ -263,7 +266,8 @@
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
"alt-cmd-r": "search::ToggleRegex"
"alt-tab": "search::CycleMode",
"alt-cmd-f": "project_search::ToggleFilters"
}
},
// Bindings from VS Code

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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,
@ -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)]

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);
}
}

74
crates/search/src/mode.rs Normal file
View 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

View file

@ -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()
}
}

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()
}

View file

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

View file

@ -1,3 +1,5 @@
#![allow(non_snake_case, non_upper_case_globals)]
mod keymap_file;
mod settings_file;
mod settings_store;

View file

@ -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()
}

View 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()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,13 +17,13 @@ export default function search(): any {
text: text(theme.highest, "mono", "default"),
border: border(theme.highest),
margin: {
right: 12,
right: 9,
},
padding: {
top: 3,
bottom: 3,
left: 12,
right: 8,
top: 4,
bottom: 4,
left: 10,
right: 4,
},
}
@ -34,6 +34,7 @@ export default function search(): any {
}
return {
padding: { top: 16, bottom: 16, left: 16, right: 16 },
// TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
match_background: with_opacity(
foreground(theme.highest, "accent"),
@ -42,76 +43,159 @@ export default function search(): any {
option_button: toggleable({
base: interactive({
base: {
...text(theme.highest, "mono", "on"),
icon_width: 14,
button_width: 32,
color: foreground(theme.highest, "variant"),
background: background(theme.highest, "on"),
corner_radius: 6,
border: border(theme.highest, "on"),
margin: {
right: 4,
corner_radius: 2,
margin: { right: 2 },
border: {
width: 1., color: background(theme.highest, "on")
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
left: 4,
right: 4,
top: 4,
bottom: 4,
},
},
state: {
hovered: {
...text(theme.highest, "mono", "on", "hovered"),
...text(theme.highest, "mono", "variant", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: border(theme.highest, "on", "hovered"),
border: {
width: 1., color: background(theme.highest, "on", "hovered")
},
},
clicked: {
...text(theme.highest, "mono", "on", "pressed"),
...text(theme.highest, "mono", "variant", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: border(theme.highest, "on", "pressed"),
border: {
width: 1., color: background(theme.highest, "on", "pressed")
},
},
},
}),
state: {
active: {
default: {
...text(theme.highest, "mono", "accent"),
icon_width: 14,
button_width: 32,
color: foreground(theme.highest, "variant"),
background: background(theme.highest, "accent"),
border: border(theme.highest, "accent"),
},
hovered: {
...text(theme.highest, "mono", "accent", "hovered"),
background: background(theme.highest, "accent", "hovered"),
border: border(theme.highest, "accent", "hovered"),
},
clicked: {
...text(theme.highest, "mono", "accent", "pressed"),
background: background(theme.highest, "accent", "pressed"),
border: border(theme.highest, "accent", "pressed"),
},
},
},
}),
action_button: interactive({
base: {
...text(theme.highest, "mono", "on"),
background: background(theme.highest, "on"),
corner_radius: 6,
border: border(theme.highest, "on"),
margin: {
right: 4,
option_button_component: toggleable({
base: interactive({
base: {
icon_size: 14,
color: foreground(theme.highest, "variant"),
button_width: 32,
background: background(theme.highest, "on"),
corner_radius: 2,
margin: { right: 2 },
border: {
width: 1., color: background(theme.highest, "on")
},
padding: {
left: 4,
right: 4,
top: 4,
bottom: 4,
},
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
state: {
hovered: {
...text(theme.highest, "mono", "variant", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: {
width: 1., color: background(theme.highest, "on", "hovered")
},
},
clicked: {
...text(theme.highest, "mono", "variant", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: {
width: 1., color: background(theme.highest, "on", "pressed")
},
},
},
},
}),
state: {
hovered: {
...text(theme.highest, "mono", "on", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: border(theme.highest, "on", "hovered"),
},
clicked: {
...text(theme.highest, "mono", "on", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: border(theme.highest, "on", "pressed"),
active: {
default: {
icon_size: 14,
button_width: 32,
color: foreground(theme.highest, "variant"),
background: background(theme.highest, "accent"),
border: border(theme.highest, "accent"),
},
hovered: {
background: background(theme.highest, "accent", "hovered"),
border: border(theme.highest, "accent", "hovered"),
},
clicked: {
background: background(theme.highest, "accent", "pressed"),
border: border(theme.highest, "accent", "pressed"),
},
},
},
}),
action_button: toggleable({
base: interactive({
base: {
...text(theme.highest, "mono", "disabled"),
background: background(theme.highest, "disabled"),
corner_radius: 6,
border: border(theme.highest, "disabled"),
padding: {
// bottom: 2,
left: 10,
right: 10,
// top: 2,
},
margin: {
right: 9,
}
},
state: {
hovered: {}
},
}),
state: {
active: interactive({
base: {
...text(theme.highest, "mono", "on"),
background: background(theme.highest, "on"),
border: border(theme.highest, "on"),
},
state: {
hovered: {
...text(theme.highest, "mono", "on", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: border(theme.highest, "on", "hovered"),
},
clicked: {
...text(theme.highest, "mono", "on", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: border(theme.highest, "on", "pressed"),
},
},
})
}
}),
editor,
invalid_editor: {
...editor,
@ -125,7 +209,7 @@ export default function search(): any {
match_index: {
...text(theme.highest, "mono", "variant"),
padding: {
left: 6,
left: 9,
},
},
option_button_group: {
@ -140,28 +224,164 @@ export default function search(): any {
right: 6,
},
},
results_status: {
major_results_status: {
...text(theme.highest, "mono", "on"),
size: 18,
size: 15,
},
minor_results_status: {
...text(theme.highest, "mono", "variant"),
size: 13,
},
dismiss_button: interactive({
base: {
color: foreground(theme.highest, "variant"),
icon_width: 12,
button_width: 14,
icon_width: 14,
button_width: 32,
corner_radius: 6,
padding: {
// // top: 10,
// bottom: 10,
left: 10,
right: 10,
},
background: background(theme.highest, "variant"),
border: border(theme.highest, "on"),
},
state: {
hovered: {
color: foreground(theme.highest, "hovered"),
background: background(theme.highest, "variant", "hovered")
},
clicked: {
color: foreground(theme.highest, "pressed"),
background: background(theme.highest, "variant", "pressed")
},
},
}),
editor_icon: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/magnifying_glass_12.svg",
dimensions: {
width: 12,
height: 12,
}
},
container: {
margin: { right: 6 },
padding: { left: 2, right: 2 },
}
},
mode_button: toggleable({
base: interactive({
base: {
...text(theme.highest, "mono", "variant"),
background: background(theme.highest, "variant"),
border: {
...border(theme.highest, "on"),
left: false,
right: false
},
padding: {
left: 10,
right: 10,
},
corner_radius: 6,
},
state: {
hovered: {
...text(theme.highest, "mono", "variant", "hovered"),
background: background(theme.highest, "variant", "hovered"),
border: border(theme.highest, "on", "hovered"),
},
clicked: {
...text(theme.highest, "mono", "variant", "pressed"),
background: background(theme.highest, "variant", "pressed"),
border: border(theme.highest, "on", "pressed"),
},
},
}),
state: {
active: {
default: {
...text(theme.highest, "mono", "on"),
background: background(theme.highest, "on")
},
hovered: {
...text(theme.highest, "mono", "on", "hovered"),
background: background(theme.highest, "on", "hovered")
},
clicked: {
...text(theme.highest, "mono", "on", "pressed"),
background: background(theme.highest, "on", "pressed")
},
},
},
}),
nav_button: toggleable({
state: {
inactive: interactive({
base: {
background: background(theme.highest, "disabled"),
text: text(theme.highest, "mono", "disabled"),
corner_radius: 6,
border: {
...border(theme.highest, "disabled"),
left: false,
right: false,
},
padding: {
left: 10,
right: 10,
},
},
state: {
hovered: {}
}
}),
active: interactive({
base: {
text: text(theme.highest, "mono", "on"),
background: background(theme.highest, "on"),
corner_radius: 6,
border: {
...border(theme.highest, "on"),
left: false,
right: false,
},
padding: {
left: 10,
right: 10,
},
},
state: {
hovered: {
...text(theme.highest, "mono", "on", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: border(theme.highest, "on", "hovered"),
},
clicked: {
...text(theme.highest, "mono", "on", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: border(theme.highest, "on", "pressed"),
},
},
})
}
}),
search_bar_row_height: 32,
option_button_height: 22,
modes_container: {
margin: {
right: 9
}
}
}
}

View file

@ -84,6 +84,27 @@ export default function tab_bar(): any {
bottom: false,
},
}
const nav_button = interactive({
base: {
color: foreground(theme.highest, "on"),
icon_width: 12,
button_width: active_pane_active_tab.height,
border: border(theme.lowest, "on", {
bottom: true,
overlay: true,
})
},
state: {
hovered: {
color: foreground(theme.highest, "on", "hovered"),
background: background(theme.highest, "on", "hovered"),
},
disabled: {
color: foreground(theme.highest, "on", "disabled")
},
},
})
const dragged_tab = {
...active_pane_active_tab,
@ -141,5 +162,6 @@ export default function tab_bar(): any {
right: false,
},
},
nav_button: nav_button
}
}

View file

@ -133,23 +133,6 @@ export default function workspace(): any {
background: background(theme.highest),
border: border(theme.highest, { bottom: true }),
item_spacing: 8,
nav_button: interactive({
base: {
color: foreground(theme.highest, "on"),
icon_width: 12,
button_width: 24,
corner_radius: 6,
},
state: {
hovered: {
color: foreground(theme.highest, "on", "hovered"),
background: background(theme.highest, "on", "hovered"),
},
disabled: {
color: foreground(theme.highest, "on", "disabled"),
},
},
}),
toggleable_tool: toggleable_icon_button(theme, {
margin: { left: 8 },
variant: "ghost",

View file

@ -1,4 +1,4 @@
import chroma, { Scale, Color } from "chroma-js"
import { Scale, Color } from "chroma-js"
import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
import {