diff --git a/Cargo.lock b/Cargo.lock index 9c36923964..3db82b3cb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml new file mode 100644 index 0000000000..2e74fd2090 --- /dev/null +++ b/crates/breadcrumbs/Cargo.toml @@ -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"] } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs new file mode 100644 index 0000000000..ce32fb2272 --- /dev/null +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -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>, + project_search: Option>, + subscriptions: Vec, +} + +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>)> { + 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) -> ElementBox { + let theme = cx.global::().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, + ) -> 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::(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::() { + 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 + } + } +} diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 452f041c7b..a7c9123894 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -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 { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b8b5b3a361..06c6b8f1bb 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -212,7 +212,7 @@ impl ContactsPanel { })); } }) - .flexible(1., true) + .flex(1., true) .boxed() }) .constrained() diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index af98f3d589..ad3fc3202d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2264,6 +2264,33 @@ impl MultiBufferSnapshot { )) } + pub fn symbols_containing( + &self, + offset: T, + theme: Option<&SyntaxTheme>, + ) -> Option<(BufferSnapshot, Vec>)> { + 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::>(); cursor.seek(&Some(excerpt_id), Bias::Left, &()); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 9f0137ef62..56fd255d82 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -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(), ) diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index f31a80a831..cc725776b9 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -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)) } diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 6830923953..79112863b5 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -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() } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 6b884289a2..2ec307bbc3 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -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::() { - 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 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::() { - *total_flex.get_or_insert(0.) += metadata.flex; + if let Some(flex) = child + .metadata::() + .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::() { + 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 = (); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 535798083e..0c1ce7c228 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1674,6 +1674,32 @@ impl BufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { + self.outline_items_containing(0..self.len(), theme) + .map(Outline::new) + } + + pub fn symbols_containing( + &self, + position: T, + theme: Option<&SyntaxTheme>, + ) -> Option>> { + 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, + theme: Option<&SyntaxTheme>, + ) -> Option>> { 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::>(); - - if items.is_empty() { - None - } else { - Some(Outline::new(items)) - } + Some(items) } pub fn enclosing_bracket_ranges( diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 0460d122b7..174018b2cf 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -10,7 +10,7 @@ pub struct Outline { path_candidate_prefixes: Vec, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct OutlineItem { pub depth: usize, pub range: Range, diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 98ecf63a46..3eb87cefb6 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -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)> { + 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 { diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index a626ff89c8..c33cb60b3e 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -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() diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 34d5306d99..74e7d90d68 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -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() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index da9ee0664b..b5f8eedf80 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -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, active_editor: Option>, active_match_index: Option, @@ -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) { @@ -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>, + item: Option<&dyn ItemHandle>, cx: &mut ViewContext, - ) -> 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.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::(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 { +impl BufferSearchBar { + pub fn new(cx: &mut ViewContext) -> 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.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::(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) -> 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::(&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.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.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::()) - { - search_bar.update(cx, |search_bar, _| search_bar.dismissed = false); - let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); - let display_map = editor - .update(cx, |editor, cx| editor.snapshot(cx)) - .display_snapshot; - let selection = editor - .read(cx) - .newest_selection_with_snapshot::(&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) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) { + return; } - }); - } - - fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { - if pane.toolbar::().is_some() { - pane.dismiss_toolbar(cx); } + cx.propagate_action(); } fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { @@ -346,7 +385,7 @@ impl SearchBar { } fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar::() { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { 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 }); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d78fcb12b7..65bb07ae46 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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, WeakViewHandle, } -struct ProjectSearchView { +pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, @@ -75,6 +69,11 @@ struct ProjectSearchView { active_match_index: Option, } +pub struct ProjectSearchBar { + active_project_search: Option>, + subscription: Option, +} + 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) -> 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::().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) { @@ -402,45 +394,12 @@ impl ProjectSearchView { } } - fn search(&mut self, _: &Search, cx: &mut ViewContext) { + fn search(&mut self, cx: &mut ViewContext) { 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) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - 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) -> Option { 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, - ) { - 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) { + fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { 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) { - 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) { - 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.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) -> ElementBox { - let theme = cx.global::().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) { + 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) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + 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, + ) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + 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, - ) -> ElementBox { - let is_active = self.is_option_enabled(option); - MouseEventHandler::new::(option as usize, cx, |state, cx| { - let theme = &cx.global::().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) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + 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) { + 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, + ) { + 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, + ) -> ElementBox { + let is_active = self.is_option_enabled(option, cx); + MouseEventHandler::new::(option as usize, cx, |state, cx| { + let theme = &cx.global::().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) -> ElementBox { + if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + let theme = cx.global::().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, + ) -> ToolbarItemLocation { + cx.notify(); + self.subscription = None; + self.active_project_search = None; + if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + 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| { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 9fb4cda8e9..38d3a5fce8 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -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); diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index e642aa45d3..00ec288168 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -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, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d10c282e35..8fa15a9235 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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, } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 725319be41..d61cac1c44 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -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) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index df30d48dbe..d48a5711a3 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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>, active_item_index: usize, nav_history: Rc>, - toolbars: HashMap>, - active_toolbar_type: Option, - active_toolbar_visible: bool, -} - -pub trait Toolbar: View { - fn active_item_changed( - &mut self, - item: Option>, - cx: &mut ViewContext, - ) -> bool; - fn on_dismiss(&mut self, cx: &mut ViewContext); -} - -trait ToolbarHandle { - fn active_item_changed( - &self, - item: Option>, - cx: &mut MutableAppContext, - ) -> bool; - fn on_dismiss(&self, cx: &mut MutableAppContext); - fn to_any(&self) -> AnyViewHandle; + toolbar: ViewHandle, } pub struct ItemNavHistory { @@ -158,14 +132,12 @@ pub struct NavigationEntry { } impl Pane { - pub fn new() -> Self { + pub fn new(cx: &mut ViewContext) -> 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(&mut self, cx: &mut ViewContext, build_toolbar: F) - where - F: FnOnce(&mut ViewContext) -> V, - V: Toolbar, - { - let type_id = TypeId::of::(); - 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 { + &self.toolbar } - pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext) { - 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(&self) -> Option> { - self.toolbars - .get(&TypeId::of::()) - .and_then(|toolbar| toolbar.to_any().downcast()) - } - - pub fn active_toolbar(&self) -> Option { - 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) { - 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) { + 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) -> 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 ToolbarHandle for ViewHandle { - fn active_item_changed( - &self, - item: Option>, - 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(history: Rc>, item: &ViewHandle) -> Self { Self { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index afffec5074..258d644148 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -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() } diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 46713424e6..7a7ad4e272 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -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) { diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 4d00591787..a91dd645a0 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -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() diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs new file mode 100644 index 0000000000..8212b25082 --- /dev/null +++ b/crates/workspace/src/toolbar.rs @@ -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, + ) -> 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>, + items: Vec<(Box, ToolbarItemLocation)>, +} + +impl Entity for Toolbar { + type Event = (); +} + +impl View for Toolbar { + fn ui_name() -> &'static str { + "Toolbar" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().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(&mut self, item: ViewHandle, cx: &mut ViewContext) + 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.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(&self) -> Option> { + self.items + .iter() + .find_map(|(item, _)| item.to_any().downcast()) + } +} + +impl ToolbarItemViewHandle for ViewHandle { + 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 for &dyn ToolbarItemViewHandle { + fn into(self) -> AnyViewHandle { + self.to_any() + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 17b0c4b518..9929cd9a51 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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), +} + pub struct Workspace { weak_self: WeakViewHandle, client: Arc, @@ -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) -> ViewHandle { - 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() diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fc9946b778..ede24aae71 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -29,6 +29,7 @@ test-support = [ ] [dependencies] +breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } collections = { path = "../collections" } client = { path = "../client" } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 7bd0c59045..7f235cbf48 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -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" diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1302d54067..c94f8a0e81 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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, cx: &mut ViewContext, ) -> 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(),