mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 04:43:17 +03:00
Merge pull request #705 from zed-industries/breadcrumbs
Introduce breadcrumbs
This commit is contained in:
commit
bdd95a82d7
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -719,6 +719,19 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "breadcrumbs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"search",
|
||||
"theme",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.3.0"
|
||||
@ -5963,6 +5976,7 @@ dependencies = [
|
||||
"async-compression",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"breadcrumbs",
|
||||
"chat_panel",
|
||||
"client",
|
||||
"clock",
|
||||
|
22
crates/breadcrumbs/Cargo.toml
Normal file
22
crates/breadcrumbs/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "breadcrumbs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/breadcrumbs.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
search = { path = "../search" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
146
crates/breadcrumbs/src/breadcrumbs.rs
Normal file
146
crates/breadcrumbs/src/breadcrumbs.rs
Normal file
@ -0,0 +1,146 @@
|
||||
use editor::{Anchor, Editor};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{BufferSnapshot, OutlineItem};
|
||||
use search::ProjectSearchView;
|
||||
use std::borrow::Cow;
|
||||
use theme::SyntaxTheme;
|
||||
use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
}
|
||||
|
||||
pub struct Breadcrumbs {
|
||||
editor: Option<ViewHandle<Editor>>,
|
||||
project_search: Option<ViewHandle<ProjectSearchView>>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
editor: Default::default(),
|
||||
subscriptions: Default::default(),
|
||||
project_search: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn active_symbols(
|
||||
&self,
|
||||
theme: &SyntaxTheme,
|
||||
cx: &AppContext,
|
||||
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
|
||||
let editor = self.editor.as_ref()?.read(cx);
|
||||
let cursor = editor.newest_anchor_selection().head();
|
||||
let (buffer, symbols) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.symbols_containing(cursor, Some(theme))?;
|
||||
Some((buffer, symbols))
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Breadcrumbs {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for Breadcrumbs {
|
||||
fn ui_name() -> &'static str {
|
||||
"Breadcrumbs"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let (buffer, symbols) =
|
||||
if let Some((buffer, symbols)) = self.active_symbols(&theme.editor.syntax, cx) {
|
||||
(buffer, symbols)
|
||||
} else {
|
||||
return Empty::new().boxed();
|
||||
};
|
||||
|
||||
let filename = if let Some(path) = buffer.path() {
|
||||
path.to_string_lossy()
|
||||
} else {
|
||||
Cow::Borrowed("untitled")
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(Label::new(filename.to_string(), theme.breadcrumbs.text.clone()).boxed())
|
||||
.with_children(symbols.into_iter().flat_map(|symbol| {
|
||||
[
|
||||
Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed(),
|
||||
Text::new(symbol.text, theme.breadcrumbs.text.clone())
|
||||
.with_highlights(symbol.highlight_ranges)
|
||||
.boxed(),
|
||||
]
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.breadcrumbs.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.subscriptions.clear();
|
||||
self.editor = None;
|
||||
self.project_search = None;
|
||||
if let Some(item) = active_pane_item {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
self.subscriptions
|
||||
.push(cx.subscribe(&editor, |_, _, event, cx| match event {
|
||||
editor::Event::BufferEdited => cx.notify(),
|
||||
editor::Event::SelectionsChanged { local } if *local => cx.notify(),
|
||||
_ => {}
|
||||
}));
|
||||
self.editor = Some(editor);
|
||||
if let Some(project_search) = item.downcast::<ProjectSearchView>() {
|
||||
self.subscriptions
|
||||
.push(cx.subscribe(&project_search, |_, _, _, cx| {
|
||||
cx.emit(Event::UpdateLocation);
|
||||
}));
|
||||
self.project_search = Some(project_search.clone());
|
||||
|
||||
if project_search.read(cx).has_matches() {
|
||||
ToolbarItemLocation::Secondary
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn location_for_event(
|
||||
&self,
|
||||
_: &Event,
|
||||
current_location: ToolbarItemLocation,
|
||||
cx: &AppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(project_search) = self.project_search.as_ref() {
|
||||
if project_search.read(cx).has_matches() {
|
||||
ToolbarItemLocation::Secondary
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
} else {
|
||||
current_location
|
||||
}
|
||||
}
|
||||
}
|
@ -219,7 +219,7 @@ impl ChatPanel {
|
||||
Empty::new().boxed()
|
||||
};
|
||||
|
||||
Flexible::new(1., true, messages).boxed()
|
||||
FlexItem::new(messages).flex(1., true).boxed()
|
||||
}
|
||||
|
||||
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
|
||||
|
@ -212,7 +212,7 @@ impl ContactsPanel {
|
||||
}));
|
||||
}
|
||||
})
|
||||
.flexible(1., true)
|
||||
.flex(1., true)
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
|
@ -2264,6 +2264,33 @@ impl MultiBufferSnapshot {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn symbols_containing<T: ToOffset>(
|
||||
&self,
|
||||
offset: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
|
||||
let anchor = self.anchor_before(offset);
|
||||
let excerpt_id = anchor.excerpt_id();
|
||||
let excerpt = self.excerpt(excerpt_id)?;
|
||||
Some((
|
||||
excerpt.buffer.clone(),
|
||||
excerpt
|
||||
.buffer
|
||||
.symbols_containing(anchor.text_anchor, theme)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|item| OutlineItem {
|
||||
depth: item.depth,
|
||||
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
|
||||
text: item.text,
|
||||
highlight_ranges: item.highlight_ranges,
|
||||
name_ranges: item.name_ranges,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(excerpt_id), Bias::Left, &());
|
||||
|
@ -78,7 +78,11 @@ impl View for FileFinder {
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
|
||||
.with_child(
|
||||
FlexItem::new(self.render_matches(cx))
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.container)
|
||||
@ -166,23 +170,19 @@ impl FileFinder {
|
||||
// .boxed(),
|
||||
// )
|
||||
.with_child(
|
||||
Flexible::new(
|
||||
1.0,
|
||||
false,
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(file_name.to_string(), style.label.clone())
|
||||
.with_highlights(file_name_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(full_path, style.label.clone())
|
||||
.with_highlights(full_path_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(file_name.to_string(), style.label.clone())
|
||||
.with_highlights(file_name_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(full_path, style.label.clone())
|
||||
.with_highlights(full_path_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
|
@ -41,6 +41,10 @@ impl Color {
|
||||
Self(ColorU::from_u32(0x0000ffff))
|
||||
}
|
||||
|
||||
pub fn yellow() -> Self {
|
||||
Self(ColorU::from_u32(0x00ffffff))
|
||||
}
|
||||
|
||||
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self(ColorU::new(r, g, b, a))
|
||||
}
|
||||
|
@ -139,11 +139,18 @@ pub trait Element {
|
||||
Expanded::new(self.boxed())
|
||||
}
|
||||
|
||||
fn flexible(self, flex: f32, expanded: bool) -> Flexible
|
||||
fn flex(self, flex: f32, expanded: bool) -> FlexItem
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Flexible::new(flex, expanded, self.boxed())
|
||||
FlexItem::new(self.boxed()).flex(flex, expanded)
|
||||
}
|
||||
|
||||
fn flex_float(self) -> FlexItem
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
FlexItem::new(self.boxed()).float()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ impl Flex {
|
||||
|
||||
fn layout_flex_children(
|
||||
&mut self,
|
||||
expanded: bool,
|
||||
layout_expanded: bool,
|
||||
constraint: SizeConstraint,
|
||||
remaining_space: &mut f32,
|
||||
remaining_flex: &mut f32,
|
||||
@ -44,32 +44,33 @@ impl Flex {
|
||||
let cross_axis = self.axis.invert();
|
||||
for child in &mut self.children {
|
||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
||||
if metadata.expanded != expanded {
|
||||
continue;
|
||||
}
|
||||
if let Some((flex, expanded)) = metadata.flex {
|
||||
if expanded != layout_expanded {
|
||||
continue;
|
||||
}
|
||||
|
||||
let flex = metadata.flex;
|
||||
let child_max = if *remaining_flex == 0.0 {
|
||||
*remaining_space
|
||||
} else {
|
||||
let space_per_flex = *remaining_space / *remaining_flex;
|
||||
space_per_flex * flex
|
||||
};
|
||||
let child_min = if expanded { child_max } else { 0. };
|
||||
let child_constraint = match self.axis {
|
||||
Axis::Horizontal => SizeConstraint::new(
|
||||
vec2f(child_min, constraint.min.y()),
|
||||
vec2f(child_max, constraint.max.y()),
|
||||
),
|
||||
Axis::Vertical => SizeConstraint::new(
|
||||
vec2f(constraint.min.x(), child_min),
|
||||
vec2f(constraint.max.x(), child_max),
|
||||
),
|
||||
};
|
||||
let child_size = child.layout(child_constraint, cx);
|
||||
*remaining_space -= child_size.along(self.axis);
|
||||
*remaining_flex -= flex;
|
||||
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
|
||||
let child_max = if *remaining_flex == 0.0 {
|
||||
*remaining_space
|
||||
} else {
|
||||
let space_per_flex = *remaining_space / *remaining_flex;
|
||||
space_per_flex * flex
|
||||
};
|
||||
let child_min = if expanded { child_max } else { 0. };
|
||||
let child_constraint = match self.axis {
|
||||
Axis::Horizontal => SizeConstraint::new(
|
||||
vec2f(child_min, constraint.min.y()),
|
||||
vec2f(child_max, constraint.max.y()),
|
||||
),
|
||||
Axis::Vertical => SizeConstraint::new(
|
||||
vec2f(constraint.min.x(), child_min),
|
||||
vec2f(constraint.max.x(), child_max),
|
||||
),
|
||||
};
|
||||
let child_size = child.layout(child_constraint, cx);
|
||||
*remaining_space -= child_size.along(self.axis);
|
||||
*remaining_flex -= flex;
|
||||
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,7 +83,7 @@ impl Extend<ElementBox> for Flex {
|
||||
}
|
||||
|
||||
impl Element for Flex {
|
||||
type LayoutState = bool;
|
||||
type LayoutState = f32;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
@ -96,8 +97,11 @@ impl Element for Flex {
|
||||
let cross_axis = self.axis.invert();
|
||||
let mut cross_axis_max: f32 = 0.0;
|
||||
for child in &mut self.children {
|
||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
||||
*total_flex.get_or_insert(0.) += metadata.flex;
|
||||
if let Some(flex) = child
|
||||
.metadata::<FlexParentData>()
|
||||
.and_then(|metadata| metadata.flex.map(|(flex, _)| flex))
|
||||
{
|
||||
*total_flex.get_or_insert(0.) += flex;
|
||||
} else {
|
||||
let child_constraint = match self.axis {
|
||||
Axis::Horizontal => SizeConstraint::new(
|
||||
@ -115,12 +119,12 @@ impl Element for Flex {
|
||||
}
|
||||
}
|
||||
|
||||
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
|
||||
let mut size = if let Some(mut remaining_flex) = total_flex {
|
||||
if constraint.max_along(self.axis).is_infinite() {
|
||||
if remaining_space.is_infinite() {
|
||||
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
|
||||
}
|
||||
|
||||
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
|
||||
self.layout_flex_children(
|
||||
false,
|
||||
constraint,
|
||||
@ -156,38 +160,47 @@ impl Element for Flex {
|
||||
size.set_y(size.y().max(constraint.min.y()));
|
||||
}
|
||||
|
||||
let mut overflowing = false;
|
||||
if size.x() > constraint.max.x() {
|
||||
size.set_x(constraint.max.x());
|
||||
overflowing = true;
|
||||
}
|
||||
if size.y() > constraint.max.y() {
|
||||
size.set_y(constraint.max.y());
|
||||
overflowing = true;
|
||||
}
|
||||
|
||||
(size, overflowing)
|
||||
(size, remaining_space)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
overflowing: &mut Self::LayoutState,
|
||||
remaining_space: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
if *overflowing {
|
||||
let overflowing = *remaining_space < 0.;
|
||||
if overflowing {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
}
|
||||
let mut child_origin = bounds.origin();
|
||||
for child in &mut self.children {
|
||||
if *remaining_space > 0. {
|
||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
||||
if metadata.float {
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin += vec2f(*remaining_space, 0.0),
|
||||
Axis::Vertical => child_origin += vec2f(0.0, *remaining_space),
|
||||
}
|
||||
*remaining_space = 0.;
|
||||
}
|
||||
}
|
||||
}
|
||||
child.paint(child_origin, visible_bounds, cx);
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
|
||||
}
|
||||
}
|
||||
if *overflowing {
|
||||
if overflowing {
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
}
|
||||
@ -224,25 +237,38 @@ impl Element for Flex {
|
||||
}
|
||||
|
||||
struct FlexParentData {
|
||||
flex: f32,
|
||||
expanded: bool,
|
||||
flex: Option<(f32, bool)>,
|
||||
float: bool,
|
||||
}
|
||||
|
||||
pub struct Flexible {
|
||||
pub struct FlexItem {
|
||||
metadata: FlexParentData,
|
||||
child: ElementBox,
|
||||
}
|
||||
|
||||
impl Flexible {
|
||||
pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self {
|
||||
Flexible {
|
||||
metadata: FlexParentData { flex, expanded },
|
||||
impl FlexItem {
|
||||
pub fn new(child: ElementBox) -> Self {
|
||||
FlexItem {
|
||||
metadata: FlexParentData {
|
||||
flex: None,
|
||||
float: false,
|
||||
},
|
||||
child,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
|
||||
self.metadata.flex = Some((flex, expanded));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn float(mut self) -> Self {
|
||||
self.metadata.float = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Flexible {
|
||||
impl Element for FlexItem {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
|
@ -1674,6 +1674,32 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
self.outline_items_containing(0..self.len(), theme)
|
||||
.map(Outline::new)
|
||||
}
|
||||
|
||||
pub fn symbols_containing<T: ToOffset>(
|
||||
&self,
|
||||
position: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<Vec<OutlineItem<Anchor>>> {
|
||||
let position = position.to_offset(&self);
|
||||
let mut items =
|
||||
self.outline_items_containing(position.saturating_sub(1)..position + 1, theme)?;
|
||||
let mut prev_depth = None;
|
||||
items.retain(|item| {
|
||||
let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
|
||||
prev_depth = Some(item.depth);
|
||||
result
|
||||
});
|
||||
Some(items)
|
||||
}
|
||||
|
||||
fn outline_items_containing(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<Vec<OutlineItem<Anchor>>> {
|
||||
let tree = self.tree.as_ref()?;
|
||||
let grammar = self
|
||||
.language
|
||||
@ -1681,6 +1707,7 @@ impl BufferSnapshot {
|
||||
.and_then(|language| language.grammar.as_ref())?;
|
||||
|
||||
let mut cursor = QueryCursorHandle::new();
|
||||
cursor.set_byte_range(range);
|
||||
let matches = cursor.matches(
|
||||
&grammar.outline_query,
|
||||
tree.root_node(),
|
||||
@ -1773,12 +1800,7 @@ impl BufferSnapshot {
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Outline::new(items))
|
||||
}
|
||||
Some(items)
|
||||
}
|
||||
|
||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
||||
|
@ -10,7 +10,7 @@ pub struct Outline<T> {
|
||||
path_candidate_prefixes: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OutlineItem<T> {
|
||||
pub depth: usize,
|
||||
pub range: Range<T>,
|
||||
|
@ -282,36 +282,6 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_outline(cx: &mut gpui::TestAppContext) {
|
||||
let language = Arc::new(
|
||||
rust_lang()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(struct_item
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
(enum_item
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
(enum_variant
|
||||
name: (_) @name) @item
|
||||
(field_declaration
|
||||
name: (_) @name) @item
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_) @name
|
||||
"for" @context
|
||||
type: (_) @name) @item
|
||||
(function_item
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
(mod_item
|
||||
"mod" @context
|
||||
name: (_) @name) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let text = r#"
|
||||
struct Person {
|
||||
name: String,
|
||||
@ -339,7 +309,8 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let outline = buffer
|
||||
.read_with(cx, |buffer, _| buffer.snapshot().outline(None))
|
||||
.unwrap();
|
||||
@ -413,6 +384,93 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
|
||||
let text = r#"
|
||||
impl Person {
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
|
||||
fn two() {
|
||||
2
|
||||
}fn three() {
|
||||
3
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
// point is at the start of an item
|
||||
assert_eq!(
|
||||
symbols_containing(Point::new(1, 4), &snapshot),
|
||||
vec![
|
||||
(
|
||||
"impl Person".to_string(),
|
||||
Point::new(0, 0)..Point::new(10, 1)
|
||||
),
|
||||
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
|
||||
]
|
||||
);
|
||||
|
||||
// point is in the middle of an item
|
||||
assert_eq!(
|
||||
symbols_containing(Point::new(2, 8), &snapshot),
|
||||
vec![
|
||||
(
|
||||
"impl Person".to_string(),
|
||||
Point::new(0, 0)..Point::new(10, 1)
|
||||
),
|
||||
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
|
||||
]
|
||||
);
|
||||
|
||||
// point is at the end of an item
|
||||
assert_eq!(
|
||||
symbols_containing(Point::new(3, 5), &snapshot),
|
||||
vec![
|
||||
(
|
||||
"impl Person".to_string(),
|
||||
Point::new(0, 0)..Point::new(10, 1)
|
||||
),
|
||||
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
|
||||
]
|
||||
);
|
||||
|
||||
// point is in between two adjacent items
|
||||
assert_eq!(
|
||||
symbols_containing(Point::new(7, 5), &snapshot),
|
||||
vec![
|
||||
(
|
||||
"impl Person".to_string(),
|
||||
Point::new(0, 0)..Point::new(10, 1)
|
||||
),
|
||||
("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
|
||||
]
|
||||
);
|
||||
|
||||
fn symbols_containing<'a>(
|
||||
position: Point,
|
||||
snapshot: &'a BufferSnapshot,
|
||||
) -> Vec<(String, Range<Point>)> {
|
||||
snapshot
|
||||
.symbols_containing(position, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
(
|
||||
item.text,
|
||||
item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
let buffer = cx.add_model(|cx| {
|
||||
@ -889,6 +947,32 @@ fn rust_lang() -> Language {
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(struct_item
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
(enum_item
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
(enum_variant
|
||||
name: (_) @name) @item
|
||||
(field_declaration
|
||||
name: (_) @name) @item
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_)? @name
|
||||
"for"? @context
|
||||
type: (_) @name) @item
|
||||
(function_item
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
(mod_item
|
||||
"mod" @context
|
||||
name: (_) @name) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn empty(point: Point) -> Range<Point> {
|
||||
|
@ -77,7 +77,11 @@ impl View for OutlineView {
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
|
||||
.with_child(
|
||||
FlexItem::new(self.render_matches(cx))
|
||||
.flex(1.0, false)
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(settings.theme.selector.container)
|
||||
.constrained()
|
||||
|
@ -76,7 +76,11 @@ impl View for ProjectSymbolsView {
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
|
||||
.with_child(
|
||||
FlexItem::new(self.render_matches(cx))
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(settings.theme.selector.container)
|
||||
.constrained()
|
||||
|
@ -2,43 +2,52 @@ use crate::{active_match_index, match_index_for_direction, Direction, SearchOpti
|
||||
use collections::HashMap;
|
||||
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
|
||||
use gpui::{
|
||||
action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity,
|
||||
MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use language::OffsetRangeExt;
|
||||
use project::search::SearchQuery;
|
||||
use std::ops::Range;
|
||||
use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
|
||||
use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
action!(Deploy, bool);
|
||||
action!(Dismiss);
|
||||
action!(FocusEditor);
|
||||
action!(ToggleSearchOption, SearchOption);
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings([
|
||||
Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
|
||||
Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
|
||||
Binding::new("escape", Dismiss, Some("SearchBar")),
|
||||
Binding::new("cmd-f", FocusEditor, Some("SearchBar")),
|
||||
Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")),
|
||||
Binding::new("escape", Dismiss, Some("BufferSearchBar")),
|
||||
Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
|
||||
Binding::new(
|
||||
"enter",
|
||||
SelectMatch(Direction::Next),
|
||||
Some("BufferSearchBar"),
|
||||
),
|
||||
Binding::new(
|
||||
"shift-enter",
|
||||
SelectMatch(Direction::Prev),
|
||||
Some("SearchBar"),
|
||||
Some("BufferSearchBar"),
|
||||
),
|
||||
Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
|
||||
Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
|
||||
]);
|
||||
cx.add_action(SearchBar::deploy);
|
||||
cx.add_action(SearchBar::dismiss);
|
||||
cx.add_action(SearchBar::focus_editor);
|
||||
cx.add_action(SearchBar::toggle_search_option);
|
||||
cx.add_action(SearchBar::select_match);
|
||||
cx.add_action(SearchBar::select_match_on_pane);
|
||||
cx.add_action(BufferSearchBar::deploy);
|
||||
cx.add_action(BufferSearchBar::dismiss);
|
||||
cx.add_action(BufferSearchBar::focus_editor);
|
||||
cx.add_action(BufferSearchBar::toggle_search_option);
|
||||
cx.add_action(BufferSearchBar::select_match);
|
||||
cx.add_action(BufferSearchBar::select_match_on_pane);
|
||||
}
|
||||
|
||||
struct SearchBar {
|
||||
pub struct BufferSearchBar {
|
||||
query_editor: ViewHandle<Editor>,
|
||||
active_editor: Option<ViewHandle<Editor>>,
|
||||
active_match_index: Option<usize>,
|
||||
@ -52,13 +61,13 @@ struct SearchBar {
|
||||
dismissed: bool,
|
||||
}
|
||||
|
||||
impl Entity for SearchBar {
|
||||
type Event = ();
|
||||
impl Entity for BufferSearchBar {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for SearchBar {
|
||||
impl View for BufferSearchBar {
|
||||
fn ui_name() -> &'static str {
|
||||
"SearchBar"
|
||||
"BufferSearchBar"
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@ -74,12 +83,44 @@ impl View for SearchBar {
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor)
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.active_editor.as_ref().and_then(|editor| {
|
||||
let matches = self.editors_with_matches.get(&editor.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()
|
||||
.boxed(),
|
||||
)
|
||||
}))
|
||||
.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)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
@ -92,43 +133,19 @@ impl View for SearchBar {
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.active_editor.as_ref().and_then(|editor| {
|
||||
let matches = self.editors_with_matches.get(&editor.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()
|
||||
.boxed(),
|
||||
)
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.search.container)
|
||||
.constrained()
|
||||
.with_height(theme.workspace.toolbar.height)
|
||||
.named("search bar")
|
||||
}
|
||||
}
|
||||
|
||||
impl Toolbar for SearchBar {
|
||||
fn active_item_changed(
|
||||
impl ToolbarItemView for BufferSearchBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_editor_subscription.take();
|
||||
self.active_editor.take();
|
||||
self.pending_search.take();
|
||||
@ -139,26 +156,31 @@ impl Toolbar for SearchBar {
|
||||
Some(cx.subscribe(&editor, Self::on_active_editor_event));
|
||||
self.active_editor = Some(editor);
|
||||
self.update_matches(false, cx);
|
||||
return true;
|
||||
if !self.dismissed {
|
||||
return ToolbarItemLocation::Secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
|
||||
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.dismissed = true;
|
||||
for (editor, _) in &self.editors_with_matches {
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx)
|
||||
});
|
||||
}
|
||||
fn location_for_event(
|
||||
&self,
|
||||
_: &Self::Event,
|
||||
_: ToolbarItemLocation,
|
||||
_: &AppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
if self.active_editor.is_some() && !self.dismissed {
|
||||
ToolbarItemLocation::Secondary
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchBar {
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
impl BufferSearchBar {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
|
||||
});
|
||||
@ -176,10 +198,75 @@ impl SearchBar {
|
||||
regex: false,
|
||||
pending_search: None,
|
||||
query_contains_error: false,
|
||||
dismissed: false,
|
||||
dismissed: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||
self.dismissed = true;
|
||||
for (editor, _) in &self.editors_with_matches {
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
cx.focus(active_editor);
|
||||
}
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show(&mut self, focus: bool, cx: &mut ViewContext<Self>) -> bool {
|
||||
let editor = if let Some(editor) = self.active_editor.clone() {
|
||||
editor
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let display_map = editor
|
||||
.update(cx, |editor, cx| editor.snapshot(cx))
|
||||
.display_snapshot;
|
||||
let selection = editor
|
||||
.read(cx)
|
||||
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
|
||||
|
||||
let mut text: String;
|
||||
if selection.start == selection.end {
|
||||
let point = selection.start.to_display_point(&display_map);
|
||||
let range = editor::movement::surrounding_word(&display_map, point);
|
||||
let range = range.start.to_offset(&display_map, Bias::Left)
|
||||
..range.end.to_offset(&display_map, Bias::Right);
|
||||
text = display_map.buffer_snapshot.text_for_range(range).collect();
|
||||
if text.trim().is_empty() {
|
||||
text = String::new();
|
||||
}
|
||||
} else {
|
||||
text = display_map
|
||||
.buffer_snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
self.set_query(&text, cx);
|
||||
}
|
||||
|
||||
if focus {
|
||||
let query_editor = self.query_editor.clone();
|
||||
query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.select_all(&editor::SelectAll, cx);
|
||||
});
|
||||
cx.focus_self();
|
||||
}
|
||||
|
||||
self.dismissed = false;
|
||||
cx.notify();
|
||||
cx.emit(Event::UpdateLocation);
|
||||
true
|
||||
}
|
||||
|
||||
fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
||||
self.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.buffer().update(cx, |query_buffer, cx| {
|
||||
@ -238,61 +325,13 @@ impl SearchBar {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.show_toolbar(cx, |cx| SearchBar::new(cx));
|
||||
|
||||
if let Some(search_bar) = pane
|
||||
.active_toolbar()
|
||||
.and_then(|toolbar| toolbar.downcast::<Self>())
|
||||
{
|
||||
search_bar.update(cx, |search_bar, _| search_bar.dismissed = false);
|
||||
let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
|
||||
let display_map = editor
|
||||
.update(cx, |editor, cx| editor.snapshot(cx))
|
||||
.display_snapshot;
|
||||
let selection = editor
|
||||
.read(cx)
|
||||
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
|
||||
|
||||
let mut text: String;
|
||||
if selection.start == selection.end {
|
||||
let point = selection.start.to_display_point(&display_map);
|
||||
let range = editor::movement::surrounding_word(&display_map, point);
|
||||
let range = range.start.to_offset(&display_map, Bias::Left)
|
||||
..range.end.to_offset(&display_map, Bias::Right);
|
||||
text = display_map.buffer_snapshot.text_for_range(range).collect();
|
||||
if text.trim().is_empty() {
|
||||
text = String::new();
|
||||
}
|
||||
} else {
|
||||
text = display_map
|
||||
.buffer_snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx));
|
||||
}
|
||||
|
||||
if *focus {
|
||||
let query_editor = search_bar.read(cx).query_editor.clone();
|
||||
query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.select_all(&editor::SelectAll, cx);
|
||||
});
|
||||
cx.focus(&search_bar);
|
||||
}
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
|
||||
if pane.toolbar::<SearchBar>().is_some() {
|
||||
pane.dismiss_toolbar(cx);
|
||||
}
|
||||
cx.propagate_action();
|
||||
}
|
||||
|
||||
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
|
||||
@ -346,7 +385,7 @@ impl SearchBar {
|
||||
}
|
||||
|
||||
fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_bar) = pane.toolbar::<SearchBar>() {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
|
||||
}
|
||||
}
|
||||
@ -540,8 +579,9 @@ mod tests {
|
||||
});
|
||||
|
||||
let search_bar = cx.add_view(Default::default(), |cx| {
|
||||
let mut search_bar = SearchBar::new(cx);
|
||||
search_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
|
||||
let mut search_bar = BufferSearchBar::new(cx);
|
||||
search_bar.set_active_pane_item(Some(&editor), cx);
|
||||
search_bar.show(false, cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
|
@ -6,8 +6,8 @@ use collections::HashMap;
|
||||
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
|
||||
use gpui::{
|
||||
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
|
||||
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{search::SearchQuery, Project};
|
||||
use std::{
|
||||
@ -16,7 +16,9 @@ use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Item, ItemNavHistory, Settings, Workspace};
|
||||
use workspace::{
|
||||
Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
action!(Deploy);
|
||||
action!(Search);
|
||||
@ -31,29 +33,21 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.set_global(ActiveSearches::default());
|
||||
cx.add_bindings([
|
||||
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
|
||||
Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
|
||||
Binding::new("cmd-shift-F", ToggleFocus, Some("Pane")),
|
||||
Binding::new("cmd-f", ToggleFocus, Some("Pane")),
|
||||
Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
|
||||
Binding::new("enter", Search, Some("ProjectSearchView")),
|
||||
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")),
|
||||
Binding::new(
|
||||
"cmd-g",
|
||||
SelectMatch(Direction::Next),
|
||||
Some("ProjectSearchView"),
|
||||
),
|
||||
Binding::new(
|
||||
"cmd-shift-G",
|
||||
SelectMatch(Direction::Prev),
|
||||
Some("ProjectSearchView"),
|
||||
),
|
||||
Binding::new("enter", Search, Some("ProjectSearchBar")),
|
||||
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
|
||||
Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
|
||||
Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
|
||||
]);
|
||||
cx.add_action(ProjectSearchView::deploy);
|
||||
cx.add_action(ProjectSearchView::search);
|
||||
cx.add_action(ProjectSearchView::search_in_new);
|
||||
cx.add_action(ProjectSearchView::toggle_search_option);
|
||||
cx.add_action(ProjectSearchView::select_match);
|
||||
cx.add_action(ProjectSearchView::toggle_focus);
|
||||
cx.capture_action(ProjectSearchView::tab);
|
||||
cx.add_action(ProjectSearchBar::search);
|
||||
cx.add_action(ProjectSearchBar::search_in_new);
|
||||
cx.add_action(ProjectSearchBar::toggle_search_option);
|
||||
cx.add_action(ProjectSearchBar::select_match);
|
||||
cx.add_action(ProjectSearchBar::toggle_focus);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
}
|
||||
|
||||
struct ProjectSearch {
|
||||
@ -64,7 +58,7 @@ struct ProjectSearch {
|
||||
active_query: Option<SearchQuery>,
|
||||
}
|
||||
|
||||
struct ProjectSearchView {
|
||||
pub struct ProjectSearchView {
|
||||
model: ModelHandle<ProjectSearch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
results_editor: ViewHandle<Editor>,
|
||||
@ -75,6 +69,11 @@ struct ProjectSearchView {
|
||||
active_match_index: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct ProjectSearchBar {
|
||||
active_project_search: Option<ViewHandle<ProjectSearchView>>,
|
||||
subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Entity for ProjectSearch {
|
||||
type Event = ();
|
||||
}
|
||||
@ -139,7 +138,7 @@ impl ProjectSearch {
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewEvent {
|
||||
pub enum ViewEvent {
|
||||
UpdateTab,
|
||||
}
|
||||
|
||||
@ -154,7 +153,7 @@ impl View for ProjectSearchView {
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let model = &self.model.read(cx);
|
||||
let results = if model.match_ranges.is_empty() {
|
||||
if model.match_ranges.is_empty() {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let text = if self.query_editor.read(cx).text(cx).is_empty() {
|
||||
""
|
||||
@ -167,18 +166,11 @@ impl View for ProjectSearchView {
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_background_color(theme.editor.background)
|
||||
.flexible(1., true)
|
||||
.flex(1., true)
|
||||
.boxed()
|
||||
} else {
|
||||
ChildView::new(&self.results_editor)
|
||||
.flexible(1., true)
|
||||
.boxed()
|
||||
};
|
||||
|
||||
Flex::column()
|
||||
.with_child(self.render_query_editor(cx))
|
||||
.with_child(results)
|
||||
.boxed()
|
||||
ChildView::new(&self.results_editor).flex(1., true).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@ -402,45 +394,12 @@ impl ProjectSearchView {
|
||||
}
|
||||
}
|
||||
|
||||
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
|
||||
fn search(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(query) = self.build_search_query(cx) {
|
||||
self.model.update(cx, |model, cx| model.search(query, cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(search_view) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
let new_query = search_view.update(cx, |search_view, cx| {
|
||||
let new_query = search_view.build_search_query(cx);
|
||||
if new_query.is_some() {
|
||||
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
|
||||
search_view.query_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(old_query.as_str(), cx);
|
||||
});
|
||||
search_view.regex = old_query.is_regex();
|
||||
search_view.whole_word = old_query.whole_word();
|
||||
search_view.case_sensitive = old_query.case_sensitive();
|
||||
}
|
||||
}
|
||||
new_query
|
||||
});
|
||||
if let Some(new_query) = new_query {
|
||||
let model = cx.add_model(|cx| {
|
||||
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
|
||||
model.search(new_query, cx);
|
||||
model
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
|
||||
let text = self.query_editor.read(cx).text(cx);
|
||||
if self.regex {
|
||||
@ -461,22 +420,7 @@ impl ProjectSearchView {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_search_option(
|
||||
&mut self,
|
||||
ToggleSearchOption(option): &ToggleSearchOption,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let value = match option {
|
||||
SearchOption::WholeWord => &mut self.whole_word,
|
||||
SearchOption::CaseSensitive => &mut self.case_sensitive,
|
||||
SearchOption::Regex => &mut self.regex,
|
||||
};
|
||||
*value = !*value;
|
||||
self.search(&Search, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
let model = self.model.read(cx);
|
||||
let results_editor = self.results_editor.read(cx);
|
||||
@ -495,26 +439,6 @@ impl ProjectSearchView {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
|
||||
if self.query_editor.is_focused(cx) {
|
||||
if !self.model.read(cx).match_ranges.is_empty() {
|
||||
self.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
self.focus_query_editor(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||
if self.query_editor.is_focused(cx) {
|
||||
if !self.model.read(cx).match_ranges.is_empty() {
|
||||
self.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
cx.propagate_action()
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_query_editor(&self, cx: &mut ViewContext<Self>) {
|
||||
self.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.select_all(&SelectAll, cx);
|
||||
@ -564,93 +488,126 @@ impl ProjectSearchView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let editor_container = if self.query_contains_error {
|
||||
theme.search.invalid_editor
|
||||
pub fn has_matches(&self) -> bool {
|
||||
self.active_match_index.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectSearchBar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_project_search: Default::default(),
|
||||
subscription: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
search_view.update(cx, |search_view, cx| search_view.search(cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(search_view) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
let new_query = search_view.update(cx, |search_view, cx| {
|
||||
let new_query = search_view.build_search_query(cx);
|
||||
if new_query.is_some() {
|
||||
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
|
||||
search_view.query_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(old_query.as_str(), cx);
|
||||
});
|
||||
search_view.regex = old_query.is_regex();
|
||||
search_view.whole_word = old_query.whole_word();
|
||||
search_view.case_sensitive = old_query.case_sensitive();
|
||||
}
|
||||
}
|
||||
new_query
|
||||
});
|
||||
if let Some(new_query) = new_query {
|
||||
let model = cx.add_model(|cx| {
|
||||
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
|
||||
model.search(new_query, cx);
|
||||
model
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_match(
|
||||
pane: &mut Pane,
|
||||
&SelectMatch(direction): &SelectMatch,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) {
|
||||
if let Some(search_view) = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view.select_match(direction, cx);
|
||||
});
|
||||
} else {
|
||||
theme.search.editor.input.container
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor)
|
||||
.contained()
|
||||
.with_style(editor_container)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_max_width(theme.search.editor.max_width)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx))
|
||||
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
|
||||
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
|
||||
.contained()
|
||||
.with_style(theme.search.option_button_group)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children({
|
||||
self.active_match_index.into_iter().flat_map(|match_ix| {
|
||||
[
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned()
|
||||
.boxed(),
|
||||
Label::new(
|
||||
format!(
|
||||
"{}/{}",
|
||||
match_ix + 1,
|
||||
self.model.read(cx).match_ranges.len()
|
||||
),
|
||||
theme.search.match_index.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.search.match_index.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
]
|
||||
})
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.search.container)
|
||||
.constrained()
|
||||
.with_height(theme.workspace.toolbar.height)
|
||||
.named("project search")
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_option_button(
|
||||
&self,
|
||||
icon: &str,
|
||||
option: SearchOption,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let is_active = self.is_option_enabled(option);
|
||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
let style = match (is_active, state.hovered) {
|
||||
(false, false) => &theme.option_button,
|
||||
(false, true) => &theme.hovered_option_button,
|
||||
(true, false) => &theme.active_option_button,
|
||||
(true, true) => &theme.active_hovered_option_button,
|
||||
};
|
||||
Label::new(icon.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_view) = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if search_view.query_editor.is_focused(cx) {
|
||||
if !search_view.model.read(cx).match_ranges.is_empty() {
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
search_view.focus_query_editor(cx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_option_enabled(&self, option: SearchOption) -> bool {
|
||||
match option {
|
||||
SearchOption::WholeWord => self.whole_word,
|
||||
SearchOption::CaseSensitive => self.case_sensitive,
|
||||
SearchOption::Regex => self.regex,
|
||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if search_view.query_editor.is_focused(cx) {
|
||||
if !search_view.model.read(cx).match_ranges.is_empty() {
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_search_option(
|
||||
&mut self,
|
||||
ToggleSearchOption(option): &ToggleSearchOption,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
let value = match option {
|
||||
SearchOption::WholeWord => &mut search_view.whole_word,
|
||||
SearchOption::CaseSensitive => &mut search_view.case_sensitive,
|
||||
SearchOption::Regex => &mut search_view.regex,
|
||||
};
|
||||
*value = !*value;
|
||||
search_view.search(cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@ -677,6 +634,148 @@ impl ProjectSearchView {
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_option_button(
|
||||
&self,
|
||||
icon: &str,
|
||||
option: SearchOption,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let is_active = self.is_option_enabled(option, cx);
|
||||
MouseEventHandler::new::<ProjectSearchBar, _, _>(option as usize, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.search;
|
||||
let style = match (is_active, state.hovered) {
|
||||
(false, false) => &theme.option_button,
|
||||
(false, true) => &theme.hovered_option_button,
|
||||
(true, false) => &theme.active_option_button,
|
||||
(true, true) => &theme.active_hovered_option_button,
|
||||
};
|
||||
Label::new(icon.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
|
||||
if let Some(search) = self.active_project_search.as_ref() {
|
||||
let search = search.read(cx);
|
||||
match option {
|
||||
SearchOption::WholeWord => search.whole_word,
|
||||
SearchOption::CaseSensitive => search.case_sensitive,
|
||||
SearchOption::Regex => search.regex,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectSearchBar {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ProjectSearchBar {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectSearchBar"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(search) = self.active_project_search.as_ref() {
|
||||
let search = search.read(cx);
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let editor_container = if search.query_contains_error {
|
||||
theme.search.invalid_editor
|
||||
} else {
|
||||
theme.search.editor.input.container
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&search.query_editor)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(search.active_match_index.map(|match_ix| {
|
||||
Label::new(
|
||||
format!(
|
||||
"{}/{}",
|
||||
match_ix + 1,
|
||||
search.model.read(cx).match_ranges.len()
|
||||
),
|
||||
theme.search.match_index.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.search.match_index.container)
|
||||
.aligned()
|
||||
.boxed()
|
||||
}))
|
||||
.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)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_option_button(
|
||||
"Case",
|
||||
SearchOption::CaseSensitive,
|
||||
cx,
|
||||
))
|
||||
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
|
||||
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
|
||||
.contained()
|
||||
.with_style(theme.search.option_button_group)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.search.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.named("project search")
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for ProjectSearchBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn workspace::ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.subscription = None;
|
||||
self.active_project_search = None;
|
||||
if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
|
||||
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
|
||||
self.active_project_search = Some(search);
|
||||
ToolbarItemLocation::PrimaryLeft {
|
||||
flex: Some((1., false)),
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -726,7 +825,7 @@ mod tests {
|
||||
search_view
|
||||
.query_editor
|
||||
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
|
||||
search_view.search(&Search, cx);
|
||||
search_view.search(cx);
|
||||
});
|
||||
search_view.next_notification(&cx).await;
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
@ -763,7 +862,7 @@ mod tests {
|
||||
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
|
||||
);
|
||||
|
||||
search_view.select_match(&SelectMatch(Direction::Next), cx);
|
||||
search_view.select_match(Direction::Next, cx);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
@ -774,7 +873,7 @@ mod tests {
|
||||
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
|
||||
);
|
||||
search_view.select_match(&SelectMatch(Direction::Next), cx);
|
||||
search_view.select_match(Direction::Next, cx);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
@ -785,7 +884,7 @@ mod tests {
|
||||
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
|
||||
);
|
||||
search_view.select_match(&SelectMatch(Direction::Next), cx);
|
||||
search_view.select_match(Direction::Next, cx);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
@ -796,7 +895,7 @@ mod tests {
|
||||
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
|
||||
);
|
||||
search_view.select_match(&SelectMatch(Direction::Prev), cx);
|
||||
search_view.select_match(Direction::Prev, cx);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
@ -807,7 +906,7 @@ mod tests {
|
||||
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
|
||||
);
|
||||
search_view.select_match(&SelectMatch(Direction::Prev), cx);
|
||||
search_view.select_match(Direction::Prev, cx);
|
||||
});
|
||||
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
|
@ -1,13 +1,14 @@
|
||||
pub use buffer_search::BufferSearchBar;
|
||||
use editor::{Anchor, MultiBufferSnapshot};
|
||||
use gpui::{action, MutableAppContext};
|
||||
pub use project_search::{ProjectSearchBar, ProjectSearchView};
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use editor::{Anchor, MultiBufferSnapshot};
|
||||
use gpui::{action, MutableAppContext};
|
||||
|
||||
mod buffer_search;
|
||||
mod project_search;
|
||||
pub mod buffer_search;
|
||||
pub mod project_search;
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
buffer_search::init(cx);
|
||||
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub timestamp: clock::Local,
|
||||
pub offset: usize,
|
||||
|
@ -26,6 +26,7 @@ pub struct Theme {
|
||||
pub editor: Editor,
|
||||
pub search: Search,
|
||||
pub project_diagnostics: ProjectDiagnostics,
|
||||
pub breadcrumbs: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@ -94,7 +95,10 @@ pub struct Tab {
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Toolbar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub item_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
@ -119,6 +123,7 @@ pub struct Search {
|
||||
pub struct FindEditor {
|
||||
#[serde(flatten)]
|
||||
pub input: FieldEditor,
|
||||
pub min_width: f32,
|
||||
pub max_width: f32,
|
||||
}
|
||||
|
||||
|
@ -310,7 +310,11 @@ impl View for ThemeSelector {
|
||||
.with_style(theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
|
||||
.with_child(
|
||||
FlexItem::new(self.render_matches(cx))
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.selector.container)
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{Item, Settings, WeakItemHandle, Workspace};
|
||||
use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
|
||||
use collections::{HashMap, VecDeque};
|
||||
use gpui::{
|
||||
action,
|
||||
@ -7,16 +7,11 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{ProjectEntryId, ProjectPath};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
cmp, mem,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
|
||||
use util::ResultExt;
|
||||
|
||||
action!(Split, SplitDirection);
|
||||
@ -101,28 +96,7 @@ pub struct Pane {
|
||||
items: Vec<Box<dyn ItemHandle>>,
|
||||
active_item_index: usize,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
|
||||
active_toolbar_type: Option<TypeId>,
|
||||
active_toolbar_visible: bool,
|
||||
}
|
||||
|
||||
pub trait Toolbar: View {
|
||||
fn active_item_changed(
|
||||
&mut self,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool;
|
||||
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
|
||||
}
|
||||
|
||||
trait ToolbarHandle {
|
||||
fn active_item_changed(
|
||||
&self,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> bool;
|
||||
fn on_dismiss(&self, cx: &mut MutableAppContext);
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
@ -158,14 +132,12 @@ pub struct NavigationEntry {
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_item_index: 0,
|
||||
nav_history: Default::default(),
|
||||
toolbars: Default::default(),
|
||||
active_toolbar_type: Default::default(),
|
||||
active_toolbar_visible: false,
|
||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +374,7 @@ impl Pane {
|
||||
self.items[prev_active_item_ix].deactivated(cx);
|
||||
cx.emit(Event::ActivateItem { local });
|
||||
}
|
||||
self.update_active_toolbar(cx);
|
||||
self.update_toolbar(cx);
|
||||
if local {
|
||||
self.focus_active_item(cx);
|
||||
self.activate(cx);
|
||||
@ -487,7 +459,7 @@ impl Pane {
|
||||
self.focus_active_item(cx);
|
||||
self.activate(cx);
|
||||
}
|
||||
self.update_active_toolbar(cx);
|
||||
self.update_toolbar(cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
@ -502,63 +474,18 @@ impl Pane {
|
||||
cx.emit(Event::Split(direction));
|
||||
}
|
||||
|
||||
pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Toolbar,
|
||||
{
|
||||
let type_id = TypeId::of::<V>();
|
||||
if self.active_toolbar_type != Some(type_id) {
|
||||
self.dismiss_toolbar(cx);
|
||||
|
||||
let active_item = self.active_item();
|
||||
self.toolbars
|
||||
.entry(type_id)
|
||||
.or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
|
||||
|
||||
self.active_toolbar_type = Some(type_id);
|
||||
self.active_toolbar_visible =
|
||||
self.toolbars[&type_id].active_item_changed(active_item, cx);
|
||||
cx.notify();
|
||||
}
|
||||
pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
|
||||
&self.toolbar
|
||||
}
|
||||
|
||||
pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
|
||||
self.toolbars
|
||||
.get_mut(&active_toolbar_type)
|
||||
.unwrap()
|
||||
.on_dismiss(cx);
|
||||
self.active_toolbar_visible = false;
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
|
||||
self.toolbars
|
||||
.get(&TypeId::of::<T>())
|
||||
.and_then(|toolbar| toolbar.to_any().downcast())
|
||||
}
|
||||
|
||||
pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
|
||||
let type_id = self.active_toolbar_type?;
|
||||
let toolbar = self.toolbars.get(&type_id)?;
|
||||
if self.active_toolbar_visible {
|
||||
Some(toolbar.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let active_item = self.items.get(self.active_item_index);
|
||||
for (toolbar_type_id, toolbar) in &self.toolbars {
|
||||
let visible = toolbar.active_item_changed(active_item.cloned(), cx);
|
||||
if Some(*toolbar_type_id) == self.active_toolbar_type {
|
||||
self.active_toolbar_visible = visible;
|
||||
}
|
||||
}
|
||||
fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let active_item = self
|
||||
.items
|
||||
.get(self.active_item_index)
|
||||
.map(|item| item.as_ref());
|
||||
self.toolbar.update(cx, |toolbar, cx| {
|
||||
toolbar.set_active_pane_item(active_item, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
@ -685,7 +612,7 @@ impl Pane {
|
||||
Empty::new()
|
||||
.contained()
|
||||
.with_border(theme.workspace.tab.container.border)
|
||||
.flexible(0., true)
|
||||
.flex(0., true)
|
||||
.named("filler"),
|
||||
);
|
||||
|
||||
@ -713,12 +640,8 @@ impl View for Pane {
|
||||
EventHandler::new(if let Some(active_item) = self.active_item() {
|
||||
Flex::column()
|
||||
.with_child(self.render_tabs(cx))
|
||||
.with_children(
|
||||
self.active_toolbar()
|
||||
.as_ref()
|
||||
.map(|view| ChildView::new(view).boxed()),
|
||||
)
|
||||
.with_child(ChildView::new(active_item).flexible(1., true).boxed())
|
||||
.with_child(ChildView::new(&self.toolbar).boxed())
|
||||
.with_child(ChildView::new(active_item).flex(1., true).boxed())
|
||||
.boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
@ -740,24 +663,6 @@ impl View for Pane {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
|
||||
fn active_item_changed(
|
||||
&self,
|
||||
item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> bool {
|
||||
self.update(cx, |this, cx| this.active_item_changed(item, cx))
|
||||
}
|
||||
|
||||
fn on_dismiss(&self, cx: &mut MutableAppContext) {
|
||||
self.update(cx, |this, cx| this.on_dismiss(cx));
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemNavHistory {
|
||||
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
|
||||
Self {
|
||||
|
@ -248,7 +248,7 @@ impl PaneAxis {
|
||||
member = Container::new(member).with_border(border).boxed();
|
||||
}
|
||||
|
||||
Flexible::new(1.0, true, member).boxed()
|
||||
FlexItem::new(member).flex(1.0, true).boxed()
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ impl Sidebar {
|
||||
let width = self.width.clone();
|
||||
move |size, _| *width.borrow_mut() = size.x()
|
||||
})
|
||||
.flexible(1., false)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
);
|
||||
if matches!(self.side, Side::Left) {
|
||||
|
@ -47,12 +47,12 @@ impl View for StatusBar {
|
||||
.with_margin_right(theme.item_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(Empty::new().flexible(1., true).boxed())
|
||||
.with_children(self.right_items.iter().map(|i| {
|
||||
ChildView::new(i.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.flex_float()
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
|
193
crates/workspace/src/toolbar.rs
Normal file
193
crates/workspace/src/toolbar.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use crate::{ItemHandle, Settings};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
pub trait ToolbarItemView: View {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn crate::ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation;
|
||||
|
||||
fn location_for_event(
|
||||
&self,
|
||||
_event: &Self::Event,
|
||||
current_location: ToolbarItemLocation,
|
||||
_cx: &AppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
current_location
|
||||
}
|
||||
}
|
||||
|
||||
trait ToolbarItemViewHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
fn set_active_pane_item(
|
||||
&self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> ToolbarItemLocation;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum ToolbarItemLocation {
|
||||
Hidden,
|
||||
PrimaryLeft { flex: Option<(f32, bool)> },
|
||||
PrimaryRight { flex: Option<(f32, bool)> },
|
||||
Secondary,
|
||||
}
|
||||
|
||||
pub struct Toolbar {
|
||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||
}
|
||||
|
||||
impl Entity for Toolbar {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for Toolbar {
|
||||
fn ui_name() -> &'static str {
|
||||
"Toolbar"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme.workspace.toolbar;
|
||||
|
||||
let mut primary_left_items = Vec::new();
|
||||
let mut primary_right_items = Vec::new();
|
||||
let mut secondary_item = None;
|
||||
|
||||
for (item, position) in &self.items {
|
||||
match *position {
|
||||
ToolbarItemLocation::Hidden => {}
|
||||
ToolbarItemLocation::PrimaryLeft { flex } => {
|
||||
let left_item = ChildView::new(item.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(theme.item_spacing);
|
||||
if let Some((flex, expanded)) = flex {
|
||||
primary_left_items.push(left_item.flex(flex, expanded).boxed());
|
||||
} else {
|
||||
primary_left_items.push(left_item.boxed());
|
||||
}
|
||||
}
|
||||
ToolbarItemLocation::PrimaryRight { flex } => {
|
||||
let right_item = ChildView::new(item.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.flex_float();
|
||||
if let Some((flex, expanded)) = flex {
|
||||
primary_right_items.push(right_item.flex(flex, expanded).boxed());
|
||||
} else {
|
||||
primary_right_items.push(right_item.boxed());
|
||||
}
|
||||
}
|
||||
ToolbarItemLocation::Secondary => {
|
||||
secondary_item = Some(
|
||||
ChildView::new(item.as_ref())
|
||||
.constrained()
|
||||
.with_height(theme.height)
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_children(primary_left_items)
|
||||
.with_children(primary_right_items)
|
||||
.constrained()
|
||||
.with_height(theme.height)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(secondary_item)
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Toolbar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_pane_item: None,
|
||||
items: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
T: 'static + ToolbarItemView,
|
||||
{
|
||||
let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
|
||||
cx.subscribe(&item, |this, item, event, cx| {
|
||||
if let Some((_, current_location)) =
|
||||
this.items.iter_mut().find(|(i, _)| i.id() == item.id())
|
||||
{
|
||||
let new_location = item
|
||||
.read(cx)
|
||||
.location_for_event(event, *current_location, cx);
|
||||
if new_location != *current_location {
|
||||
*current_location = new_location;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.items.push((Box::new(item), location));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_active_pane_item(
|
||||
&mut self,
|
||||
pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
|
||||
for (toolbar_item, current_location) in self.items.iter_mut() {
|
||||
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
|
||||
if new_location != *current_location {
|
||||
*current_location = new_location;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
|
||||
self.items
|
||||
.iter()
|
||||
.find_map(|(item, _)| item.to_any().downcast())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
||||
fn id(&self) -> usize {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
|
||||
fn set_active_pane_item(
|
||||
&self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_active_pane_item(active_pane_item, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<AnyViewHandle> for &dyn ToolbarItemViewHandle {
|
||||
fn into(self) -> AnyViewHandle {
|
||||
self.to_any()
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ pub mod pane_group;
|
||||
pub mod settings;
|
||||
pub mod sidebar;
|
||||
mod status_bar;
|
||||
mod toolbar;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{
|
||||
@ -47,6 +48,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use theme::{Theme, ThemeRegistry};
|
||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||
use util::ResultExt;
|
||||
|
||||
type ProjectItemBuilders = HashMap<
|
||||
@ -650,6 +652,10 @@ impl WorkspaceParams {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
PaneAdded(ViewHandle<Pane>),
|
||||
}
|
||||
|
||||
pub struct Workspace {
|
||||
weak_self: WeakViewHandle<Self>,
|
||||
client: Arc<Client>,
|
||||
@ -716,7 +722,7 @@ impl Workspace {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let pane = cx.add_view(|_| Pane::new());
|
||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||
let pane_id = pane.id();
|
||||
cx.observe(&pane, move |me, _, cx| {
|
||||
let active_entry = me.active_project_path(cx);
|
||||
@ -729,6 +735,7 @@ impl Workspace {
|
||||
})
|
||||
.detach();
|
||||
cx.focus(&pane);
|
||||
cx.emit(Event::PaneAdded(pane.clone()));
|
||||
|
||||
let status_bar = cx.add_view(|cx| StatusBar::new(&pane, cx));
|
||||
let mut current_user = params.user_store.read(cx).watch_current_user().clone();
|
||||
@ -1047,7 +1054,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||
let pane = cx.add_view(|_| Pane::new());
|
||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||
let pane_id = pane.id();
|
||||
cx.observe(&pane, move |me, _, cx| {
|
||||
let active_entry = me.active_project_path(cx);
|
||||
@ -1061,6 +1068,7 @@ impl Workspace {
|
||||
.detach();
|
||||
self.panes.push(pane.clone());
|
||||
self.activate_pane(pane.clone(), cx);
|
||||
cx.emit(Event::PaneAdded(pane.clone()));
|
||||
pane
|
||||
}
|
||||
|
||||
@ -1916,7 +1924,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
impl Entity for Workspace {
|
||||
type Event = ();
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for Workspace {
|
||||
@ -1938,36 +1946,35 @@ impl View for Workspace {
|
||||
if let Some(element) =
|
||||
self.left_sidebar.render_active_item(&theme, cx)
|
||||
{
|
||||
content.add_child(Flexible::new(0.8, false, element).boxed());
|
||||
content
|
||||
.add_child(FlexItem::new(element).flex(0.8, false).boxed());
|
||||
}
|
||||
content.add_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flexible::new(
|
||||
1.,
|
||||
true,
|
||||
self.center.render(
|
||||
&theme,
|
||||
&self.follower_states_by_leader,
|
||||
self.project.read(cx).collaborators(),
|
||||
),
|
||||
)
|
||||
FlexItem::new(self.center.render(
|
||||
&theme,
|
||||
&self.follower_states_by_leader,
|
||||
self.project.read(cx).collaborators(),
|
||||
))
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.status_bar).boxed())
|
||||
.flexible(1., true)
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
);
|
||||
if let Some(element) =
|
||||
self.right_sidebar.render_active_item(&theme, cx)
|
||||
{
|
||||
content.add_child(Flexible::new(0.8, false, element).boxed());
|
||||
content
|
||||
.add_child(FlexItem::new(element).flex(0.8, false).boxed());
|
||||
}
|
||||
content.add_child(self.right_sidebar.render(&theme, cx));
|
||||
content.boxed()
|
||||
})
|
||||
.with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
|
||||
.flexible(1.0, true)
|
||||
.flex(1.0, true)
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
|
@ -29,6 +29,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
breadcrumbs = { path = "../breadcrumbs" }
|
||||
chat_panel = { path = "../chat_panel" }
|
||||
collections = { path = "../collections" }
|
||||
client = { path = "../client" }
|
||||
|
@ -85,7 +85,15 @@ diagnostic_message = "$text.2"
|
||||
lsp_message = "$text.2"
|
||||
|
||||
[workspace.toolbar]
|
||||
height = 44
|
||||
background = "$surface.1"
|
||||
border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false }
|
||||
height = 34
|
||||
item_spacing = 8
|
||||
padding = { left = 16, right = 8, top = 4, bottom = 4 }
|
||||
|
||||
[breadcrumbs]
|
||||
extends = "$text.1"
|
||||
padding = { left = 6 }
|
||||
|
||||
[panel]
|
||||
padding = { top = 12, left = 12, bottom = 12, right = 12 }
|
||||
@ -354,7 +362,6 @@ tab_summary_spacing = 10
|
||||
|
||||
[search]
|
||||
match_background = "$state.highlighted_line"
|
||||
background = "$surface.1"
|
||||
results_status = { extends = "$text.0", size = 18 }
|
||||
tab_icon_width = 14
|
||||
tab_icon_spacing = 4
|
||||
@ -384,15 +391,16 @@ extends = "$search.option_button"
|
||||
background = "$surface.2"
|
||||
|
||||
[search.match_index]
|
||||
extends = "$text.1"
|
||||
extends = "$text.2"
|
||||
padding = 6
|
||||
|
||||
[search.editor]
|
||||
max_width = 400
|
||||
min_width = 200
|
||||
max_width = 500
|
||||
background = "$surface.0"
|
||||
corner_radius = 6
|
||||
padding = { left = 13, right = 13, top = 3, bottom = 3 }
|
||||
margin = { top = 5, bottom = 5, left = 5, right = 5 }
|
||||
padding = { left = 14, right = 14, top = 3, bottom = 3 }
|
||||
margin = { right = 5 }
|
||||
text = "$text.0"
|
||||
placeholder_text = "$text.2"
|
||||
selection = "$selection.host"
|
||||
|
@ -4,6 +4,7 @@ pub mod menus;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
use chat_panel::ChatPanel;
|
||||
pub use client;
|
||||
pub use contacts_panel;
|
||||
@ -21,6 +22,7 @@ pub use lsp;
|
||||
use project::Project;
|
||||
pub use project::{self, fs};
|
||||
use project_panel::ProjectPanel;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
pub use workspace;
|
||||
use workspace::{AppState, Settings, Workspace, WorkspaceParams};
|
||||
@ -104,6 +106,21 @@ pub fn build_workspace(
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Workspace {
|
||||
cx.subscribe(&cx.handle(), |_, _, event, cx| {
|
||||
let workspace::Event::PaneAdded(pane) = event;
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
})
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
let workspace_params = WorkspaceParams {
|
||||
project,
|
||||
client: app_state.client.clone(),
|
||||
|
Loading…
Reference in New Issue
Block a user