From 257601b3c124a727db848a78e284a357586e828c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Mar 2022 14:04:58 -0700 Subject: [PATCH 01/21] Add buffer method for getting the symbols containing a position --- crates/language/src/buffer.rs | 33 ++++++-- crates/language/src/outline.rs | 2 +- crates/language/src/tests.rs | 149 ++++++++++++++++++++++++++------- 3 files changed, 146 insertions(+), 38 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a4fee82805..37dc026abf 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1667,6 +1667,31 @@ 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 - 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 @@ -1674,6 +1699,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(), @@ -1766,12 +1792,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 6c9980b334..1cad180f64 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| { @@ -851,6 +909,35 @@ 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 + type: (_) @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 { From d296bb21a836d740f6a86d73decb49a1f425215b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 10:24:42 +0200 Subject: [PATCH 02/21] Emit `Event::PaneAdded` in `Workspace` when a new pane is created --- crates/workspace/src/workspace.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 17b0c4b518..4d9f0ca3da 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -650,6 +650,10 @@ impl WorkspaceParams { } } +pub enum Event { + PaneAdded(ViewHandle), +} + pub struct Workspace { weak_self: WeakViewHandle, client: Arc, @@ -1061,6 +1065,7 @@ impl Workspace { .detach(); self.panes.push(pane.clone()); self.activate_pane(pane.clone(), cx); + cx.emit(Event::PaneAdded(pane.clone())); pane } @@ -1916,7 +1921,7 @@ impl Workspace { } impl Entity for Workspace { - type Event = (); + type Event = Event; } impl View for Workspace { From 9df2dacd85285ec57f3ef2b1c437d2a0768751c4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 11:48:21 +0200 Subject: [PATCH 03/21] Restructure `Pane` to have a single `Toolbar` with multiple items --- crates/search/src/buffer_search.rs | 263 ++++++++++++++-------------- crates/search/src/project_search.rs | 9 +- crates/search/src/search.rs | 6 +- crates/theme/src/theme.rs | 13 +- crates/workspace/src/pane.rs | 135 +++----------- crates/workspace/src/toolbar.rs | 131 ++++++++++++++ crates/workspace/src/workspace.rs | 7 +- crates/zed/assets/themes/_base.toml | 6 +- crates/zed/src/zed.rs | 12 ++ 9 files changed, 318 insertions(+), 264 deletions(-) create mode 100644 crates/workspace/src/toolbar.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index da9ee0664b..d1560274c4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -8,7 +8,7 @@ use gpui::{ use language::OffsetRangeExt; use project::search::SearchQuery; use std::ops::Range; -use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace}; +use workspace::{ItemHandle, Pane, Settings, ToolbarItemView}; action!(Deploy, bool); action!(Dismiss); @@ -38,7 +38,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(SearchBar::select_match_on_pane); } -struct SearchBar { +pub struct SearchBar { query_editor: ViewHandle, active_editor: Option>, active_match_index: Option, @@ -66,69 +66,69 @@ impl View for SearchBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let editor_container = if self.query_contains_error { - theme.search.invalid_editor + if self.dismissed { + Empty::new().boxed() } 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_search_option("Case", SearchOption::CaseSensitive, cx)) - .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) - .contained() - .with_style(theme.search.option_button_group) - .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()) + let theme = cx.global::().theme.clone(); + let editor_container = if self.query_contains_error { + theme.search.invalid_editor + } else { + theme.search.editor.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) .contained() - .with_style(theme.search.match_index.container) + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.search.max_editor_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_search_option( + "Case", + SearchOption::CaseSensitive, + cx, + )) + .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.search.option_button_group) .aligned() .boxed(), ) - })) - .contained() - .with_style(theme.search.container) - .constrained() - .with_height(theme.workspace.toolbar.height) - .named("search bar") + .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(), + ) + })) + .named("search bar") + } } } -impl Toolbar for SearchBar { - fn active_item_changed( - &mut self, - item: Option>, - cx: &mut ViewContext, - ) -> bool { +impl ToolbarItemView for SearchBar { + fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { self.active_editor_subscription.take(); self.active_editor.take(); self.pending_search.take(); @@ -139,29 +139,19 @@ 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; + self.dismissed = false; + return; } } - false - } - 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) - }); - } - } + self.dismiss(&Dismiss, cx); } } impl SearchBar { - 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) - }); + pub fn new(cx: &mut ViewContext) -> Self { + let query_editor = + cx.add_view(|cx| Editor::auto_height(2, Some(|theme| theme.search.editor.clone()), cx)); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); @@ -176,10 +166,73 @@ 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.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(); + 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 +291,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 +351,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)); } } @@ -541,7 +546,7 @@ 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); + search_bar.set_active_pane_item(Some(&editor), cx); search_bar }); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d78fcb12b7..cf74a21000 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -336,8 +336,7 @@ impl ProjectSearchView { .detach(); let query_editor = cx.add_view(|cx| { - let mut editor = - Editor::single_line(Some(|theme| theme.search.editor.input.clone()), cx); + let mut editor = Editor::single_line(Some(|theme| theme.search.editor.clone()), cx); editor.set_text(query_text, cx); editor }); @@ -569,7 +568,7 @@ impl ProjectSearchView { let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { - theme.search.editor.input.container + theme.search.editor.container }; Flex::row() .with_child( @@ -578,7 +577,7 @@ impl ProjectSearchView { .with_style(editor_container) .aligned() .constrained() - .with_max_width(theme.search.editor.max_width) + .with_max_width(theme.search.max_editor_width) .boxed(), ) .with_child( @@ -615,7 +614,7 @@ impl ProjectSearchView { }) }) .contained() - .with_style(theme.search.container) + .with_style(theme.workspace.toolbar.container) .constrained() .with_height(theme.workspace.toolbar.height) .named("project search") diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 9fb4cda8e9..b2543fe261 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,11 +1,11 @@ +pub use buffer_search::SearchBar; +use editor::{Anchor, MultiBufferSnapshot}; +use gpui::{action, MutableAppContext}; use std::{ cmp::{self, Ordering}, ops::Range, }; -use editor::{Anchor, MultiBufferSnapshot}; -use gpui::{action, MutableAppContext}; - mod buffer_search; mod project_search; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d10c282e35..bccf44dfff 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -94,14 +94,18 @@ 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)] pub struct Search { #[serde(flatten)] pub container: ContainerStyle, - pub editor: FindEditor, + pub max_editor_width: f32, + pub editor: FieldEditor, pub invalid_editor: ContainerStyle, pub option_button_group: ContainerStyle, pub option_button: ContainedText, @@ -115,13 +119,6 @@ pub struct Search { pub tab_icon_spacing: f32, } -#[derive(Clone, Deserialize, Default)] -pub struct FindEditor { - #[serde(flatten)] - pub input: FieldEditor, - pub max_width: f32, -} - #[derive(Deserialize, Default)] pub struct Sidebar { #[serde(flatten)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index df30d48dbe..ec9319a396 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 { @@ -713,11 +640,7 @@ 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(&self.toolbar).boxed()) .with_child(ChildView::new(active_item).flexible(1., true).boxed()) .boxed() } else { @@ -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/toolbar.rs b/crates/workspace/src/toolbar.rs new file mode 100644 index 0000000000..a709f4bd05 --- /dev/null +++ b/crates/workspace/src/toolbar.rs @@ -0,0 +1,131 @@ +use crate::{ItemHandle, Settings}; +use gpui::{ + elements::*, AnyViewHandle, 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, + ); +} + +trait ToolbarItemViewHandle { + fn to_any(&self) -> AnyViewHandle; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut MutableAppContext, + ); +} + +pub struct Toolbar { + active_pane_item: Option>, + left_items: Vec>, + right_items: Vec>, +} + +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; + Flex::row() + .with_children(self.left_items.iter().map(|i| { + ChildView::new(i.as_ref()) + .aligned() + .contained() + .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) + .boxed() + })) + .contained() + .with_style(theme.container) + .constrained() + .with_height(theme.height) + .boxed() + } +} + +impl Toolbar { + pub fn new() -> Self { + Self { + active_pane_item: None, + left_items: Default::default(), + right_items: Default::default(), + } + } + + pub fn add_left_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + ToolbarItemView, + { + item.set_active_pane_item(self.active_pane_item.as_deref(), cx); + self.left_items.push(Box::new(item)); + cx.notify(); + } + + pub fn add_right_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + ToolbarItemView, + { + item.set_active_pane_item(self.active_pane_item.as_deref(), cx); + self.right_items.push(Box::new(item)); + cx.notify(); + } + + pub fn set_active_pane_item( + &mut self, + item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + self.active_pane_item = item.map(|item| item.boxed_clone()); + for tool in self.left_items.iter().chain(&self.right_items) { + tool.set_active_pane_item(item, cx); + } + } + + pub fn item_of_type(&self) -> Option> { + self.left_items + .iter() + .chain(&self.right_items) + .find_map(|tool| tool.to_any().downcast()) + } +} + +impl ToolbarItemViewHandle for ViewHandle { + fn to_any(&self) -> AnyViewHandle { + self.into() + } + + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut MutableAppContext, + ) { + 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 4d9f0ca3da..d3a644d852 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::ToolbarItemView; use util::ResultExt; type ProjectItemBuilders = HashMap< @@ -720,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); @@ -733,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(); @@ -1051,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); diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 7bd0c59045..54afc3d5a1 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -85,7 +85,10 @@ diagnostic_message = "$text.2" lsp_message = "$text.2" [workspace.toolbar] +background = "$surface.1" +border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false } height = 44 +item_spacing = 8 [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } @@ -353,8 +356,8 @@ tab_icon_spacing = 4 tab_summary_spacing = 10 [search] +max_editor_width = 400 match_background = "$state.highlighted_line" -background = "$surface.1" results_status = { extends = "$text.0", size = 18 } tab_icon_width = 14 tab_icon_spacing = 4 @@ -388,7 +391,6 @@ extends = "$text.1" padding = 6 [search.editor] -max_width = 400 background = "$surface.0" corner_radius = 6 padding = { left = 13, right = 13, top = 3, bottom = 3 } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1302d54067..ca3f18e233 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -21,6 +21,7 @@ pub use lsp; use project::Project; pub use project::{self, fs}; use project_panel::ProjectPanel; +use search::SearchBar; use std::{path::PathBuf, sync::Arc}; pub use workspace; use workspace::{AppState, Settings, Workspace, WorkspaceParams}; @@ -104,6 +105,17 @@ 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 search_bar = cx.add_view(|cx| SearchBar::new(cx)); + toolbar.add_right_item(search_bar, cx); + }) + }); + }) + .detach(); + let workspace_params = WorkspaceParams { project, client: app_state.client.clone(), From a86118cfe2d5b4699d7396ec861f920a36cc5538 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 11:59:34 +0200 Subject: [PATCH 04/21] Avoid matching duplicate `impl` outline items in tests --- crates/language/src/tests.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 1c15f1e598..3eb87cefb6 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -961,11 +961,8 @@ fn rust_lang() -> Language { name: (_) @name) @item (impl_item "impl" @context - type: (_) @name) @item - (impl_item - "impl" @context - trait: (_) @name - "for" @context + trait: (_)? @name + "for"? @context type: (_) @name) @item (function_item "fn" @context From 42a7e573bc8aaf0afb9c3edfaa1fe09bb0f2398e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 12:17:37 +0200 Subject: [PATCH 05/21] Add padding to toolbar --- crates/zed/assets/themes/_base.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 54afc3d5a1..be5d0ead08 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -89,6 +89,8 @@ background = "$surface.1" border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false } height = 44 item_spacing = 8 +padding.left = 8 +padding.right = 8 [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } From d7a39a2116043e363fe2f72f20aecd25a36dc244 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 13:47:48 +0200 Subject: [PATCH 06/21] Honor `SearchBar` being dismissed when changing the active item --- crates/search/src/buffer_search.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d1560274c4..36dbbbe7e6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -66,7 +66,7 @@ impl View for SearchBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - if self.dismissed { + if self.dismissed || self.active_editor.is_none() { Empty::new().boxed() } else { let theme = cx.global::().theme.clone(); @@ -129,6 +129,7 @@ impl View for SearchBar { impl ToolbarItemView for SearchBar { fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + cx.notify(); self.active_editor_subscription.take(); self.active_editor.take(); self.pending_search.take(); @@ -139,12 +140,9 @@ impl ToolbarItemView for SearchBar { Some(cx.subscribe(&editor, Self::on_active_editor_event)); self.active_editor = Some(editor); self.update_matches(false, cx); - self.dismissed = false; return; } } - - self.dismiss(&Dismiss, cx); } } From bfa5dd52ddc1a57f79ea76d785abc106421fb1eb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 13:49:09 +0200 Subject: [PATCH 07/21] Don't underflow when calling `symbols_containing_offset(0)` --- crates/language/src/buffer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 36ef7d4a01..0c1ce7c228 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1684,7 +1684,8 @@ impl BufferSnapshot { theme: Option<&SyntaxTheme>, ) -> Option>> { let position = position.to_offset(&self); - let mut items = self.outline_items_containing(position - 1..position + 1, theme)?; + 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); From 099250c6915a3d81fa8e5781b6fdfba8b2c84880 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 13:49:55 +0200 Subject: [PATCH 08/21] Introduce `MultiBuffer::symbols_containing` --- crates/editor/src/multi_buffer.rs | 27 +++++++++++++++++++++++++++ crates/text/src/anchor.rs | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) 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/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, From 13f42550c94ce977342383c5196e6f0d2c1993a7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 14:01:15 +0200 Subject: [PATCH 09/21] Show breadcrumbs in the toolbar --- Cargo.lock | 13 ++++ crates/breadcrumbs/Cargo.toml | 21 ++++++ crates/breadcrumbs/src/breadcrumbs.rs | 99 +++++++++++++++++++++++++++ crates/theme/src/theme.rs | 6 ++ crates/zed/Cargo.toml | 1 + crates/zed/assets/themes/_base.toml | 3 + crates/zed/src/zed.rs | 4 ++ 7 files changed, 147 insertions(+) create mode 100644 crates/breadcrumbs/Cargo.toml create mode 100644 crates/breadcrumbs/src/breadcrumbs.rs diff --git a/Cargo.lock b/Cargo.lock index 9c36923964..d119b987e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,6 +719,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "breadcrumbs" +version = "0.1.0" +dependencies = [ + "collections", + "editor", + "gpui", + "language", + "theme", + "workspace", +] + [[package]] name = "brotli" version = "3.3.0" @@ -5963,6 +5975,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..2e6e07ca19 --- /dev/null +++ b/crates/breadcrumbs/Cargo.toml @@ -0,0 +1,21 @@ +[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" } +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..89f6202a54 --- /dev/null +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -0,0 +1,99 @@ +use editor::{Anchor, Editor}; +use gpui::{ + elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle, +}; +use language::{BufferSnapshot, OutlineItem}; +use std::borrow::Cow; +use theme::SyntaxTheme; +use workspace::{ItemHandle, Settings, ToolbarItemView}; + +pub struct Breadcrumbs { + editor: Option>, + editor_subscription: Option, +} + +impl Breadcrumbs { + pub fn new() -> Self { + Self { + editor: Default::default(), + editor_subscription: 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))?; + if buffer.path().is_none() && symbols.is_empty() { + None + } else { + Some((buffer, symbols)) + } + } +} + +impl Entity for Breadcrumbs { + type 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(), + ] + })) + .boxed() + } +} + +impl ToolbarItemView for Breadcrumbs { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + self.editor_subscription = None; + self.editor = None; + if let Some(editor) = active_pane_item.and_then(|i| i.act_as::(cx)) { + self.editor_subscription = Some(cx.subscribe(&editor, |_, _, event, cx| match event { + editor::Event::BufferEdited => cx.notify(), + editor::Event::SelectionsChanged { local } if *local => cx.notify(), + _ => {} + })); + self.editor = Some(editor); + } + cx.notify(); + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bccf44dfff..703079523e 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: Breadcrumbs, } #[derive(Deserialize, Default)] @@ -271,6 +272,11 @@ pub struct ProjectDiagnostics { pub tab_summary_spacing: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct Breadcrumbs { + pub text: TextStyle, +} + #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, 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 be5d0ead08..1852c0d87f 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -92,6 +92,9 @@ item_spacing = 8 padding.left = 8 padding.right = 8 +[breadcrumbs] +text = "$text.1" + [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ca3f18e233..bfa5b3024d 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; @@ -109,6 +110,9 @@ pub fn build_workspace( 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_left_item(breadcrumbs, cx); + let search_bar = cx.add_view(|cx| SearchBar::new(cx)); toolbar.add_right_item(search_bar, cx); }) From a6bdb6dc5da5252d7d626cbdda7dce563054f013 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 15:53:36 +0200 Subject: [PATCH 10/21] Embed match index inside of search query editor --- crates/search/src/buffer_search.rs | 53 ++++++++++++++++------------- crates/search/src/project_search.rs | 51 ++++++++++++++------------- crates/zed/assets/themes/_base.toml | 2 +- 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 36dbbbe7e6..ad4736f387 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -77,7 +77,28 @@ impl View for SearchBar { }; Flex::row() .with_child( - ChildView::new(&self.query_editor) + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .flexible(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() @@ -85,6 +106,13 @@ impl View for SearchBar { .with_max_width(theme.search.max_editor_width) .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_search_option( @@ -99,29 +127,6 @@ 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(), - ) - })) .named("search bar") } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index cf74a21000..1415b16da0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -572,7 +572,26 @@ impl ProjectSearchView { }; Flex::row() .with_child( - ChildView::new(&self.query_editor) + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .flexible(1., true) + .boxed(), + ) + .with_children(self.active_match_index.map(|match_ix| { + 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(editor_container) .aligned() @@ -580,6 +599,13 @@ impl ProjectSearchView { .with_max_width(theme.search.max_editor_width) .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)) @@ -590,29 +616,6 @@ impl ProjectSearchView { .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.workspace.toolbar.container) .constrained() diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 1852c0d87f..73ffaa8898 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -392,7 +392,7 @@ extends = "$search.option_button" background = "$surface.2" [search.match_index] -extends = "$text.1" +extends = "$text.2" padding = 6 [search.editor] From a11665ecc7c5c2e6fa4edf8ac8dbba6434bd27b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 17:04:39 +0200 Subject: [PATCH 11/21] Render project search query editor in toolbar --- crates/search/src/buffer_search.rs | 42 +-- crates/search/src/project_search.rs | 465 ++++++++++++++++------------ crates/search/src/search.rs | 7 +- crates/zed/assets/themes/_base.toml | 2 +- crates/zed/src/zed.rs | 8 +- 5 files changed, 307 insertions(+), 217 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ad4736f387..af6e6116c5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -19,26 +19,30 @@ 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); } -pub struct SearchBar { +pub struct BufferSearchBar { query_editor: ViewHandle, active_editor: Option>, active_match_index: Option, @@ -52,13 +56,13 @@ pub struct SearchBar { dismissed: bool, } -impl Entity for SearchBar { +impl Entity for BufferSearchBar { type Event = (); } -impl View for SearchBar { +impl View for BufferSearchBar { fn ui_name() -> &'static str { - "SearchBar" + "BufferSearchBar" } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -132,7 +136,7 @@ impl View for SearchBar { } } -impl ToolbarItemView for SearchBar { +impl ToolbarItemView for BufferSearchBar { fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { cx.notify(); self.active_editor_subscription.take(); @@ -151,7 +155,7 @@ impl ToolbarItemView for SearchBar { } } -impl SearchBar { +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.clone()), cx)); @@ -295,7 +299,7 @@ impl SearchBar { } fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + 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; } @@ -354,7 +358,7 @@ impl SearchBar { } fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + 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)); } } @@ -548,7 +552,7 @@ mod tests { }); let search_bar = cx.add_view(Default::default(), |cx| { - let mut search_bar = SearchBar::new(cx); + let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar }); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1415b16da0..2d3bbc5f27 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,7 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{Item, ItemNavHistory, Settings, Workspace}; +use workspace::{Item, ItemNavHistory, Pane, Settings, ToolbarItemView, Workspace}; action!(Deploy); action!(Search); @@ -31,29 +31,21 @@ struct ActiveSearches(HashMap, WeakViewHandle, } +pub struct ProjectSearchBar { + active_project_search: Option>, + subscription: Option, +} + impl Entity for ProjectSearch { type Event = (); } @@ -154,7 +151,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() { "" @@ -173,12 +170,7 @@ impl View for ProjectSearchView { ChildView::new(&self.results_editor) .flexible(1., true) .boxed() - }; - - Flex::column() - .with_child(self.render_query_editor(cx)) - .with_child(results) - .boxed() + } } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -401,45 +393,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 { @@ -460,22 +419,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); @@ -494,26 +438,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); @@ -562,97 +486,123 @@ impl ProjectSearchView { cx.notify(); } } +} - 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 +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.container - }; - Flex::row() - .with_child( - Flex::row() - .with_child( - ChildView::new(&self.query_editor) - .flexible(1., true) - .boxed(), - ) - .with_children(self.active_match_index.map(|match_ix| { - 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(editor_container) - .aligned() - .constrained() - .with_max_width(theme.search.max_editor_width) - .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.workspace.toolbar.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(); } } @@ -679,6 +629,139 @@ 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.container + }; + Flex::row() + .with_child( + Flex::row() + .with_child( + ChildView::new(&search.query_editor) + .flexible(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_max_width(theme.search.max_editor_width) + .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.workspace.toolbar.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .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.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); + } + cx.notify(); + } } #[cfg(test)] @@ -728,7 +811,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| { @@ -765,7 +848,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| { @@ -776,7 +859,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| { @@ -787,7 +870,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| { @@ -798,7 +881,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| { @@ -809,7 +892,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 b2543fe261..e1ef1357ce 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,13 +1,14 @@ -pub use buffer_search::SearchBar; +pub use buffer_search::BufferSearchBar; use editor::{Anchor, MultiBufferSnapshot}; use gpui::{action, MutableAppContext}; +pub use project_search::ProjectSearchBar; use std::{ cmp::{self, Ordering}, ops::Range, }; -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/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 73ffaa8898..06e9c4f3b3 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -361,7 +361,7 @@ tab_icon_spacing = 4 tab_summary_spacing = 10 [search] -max_editor_width = 400 +max_editor_width = 250 match_background = "$state.highlighted_line" results_status = { extends = "$text.0", size = 18 } tab_icon_width = 14 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bfa5b3024d..84cf163b76 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -22,7 +22,7 @@ pub use lsp; use project::Project; pub use project::{self, fs}; use project_panel::ProjectPanel; -use search::SearchBar; +use search::{BufferSearchBar, ProjectSearchBar}; use std::{path::PathBuf, sync::Arc}; pub use workspace; use workspace::{AppState, Settings, Workspace, WorkspaceParams}; @@ -113,8 +113,10 @@ pub fn build_workspace( let breadcrumbs = cx.add_view(|_| Breadcrumbs::new()); toolbar.add_left_item(breadcrumbs, cx); - let search_bar = cx.add_view(|cx| SearchBar::new(cx)); - toolbar.add_right_item(search_bar, cx); + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_right_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_right_item(project_search_bar, cx); }) }); }) From fb46615c9faa78212d987cfe2ab04493aad34691 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Mar 2022 21:16:12 -0700 Subject: [PATCH 12/21] Use a fancier angle bracket in the breadcrumbs --- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 89f6202a54..19042b3e40 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -68,7 +68,7 @@ impl View for Breadcrumbs { .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(), + Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed(), Text::new(symbol.text, theme.breadcrumbs.text.clone()) .with_highlights(symbol.highlight_ranges) .boxed(), From 94097a56a1277d2dea4d0bf752c87047c1c22ea8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Mar 2022 13:18:31 +0200 Subject: [PATCH 13/21] Fix buffer search unit tests --- crates/search/src/buffer_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index af6e6116c5..9e2ed67a99 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -554,6 +554,7 @@ mod tests { let search_bar = cx.add_view(Default::default(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(false, cx); search_bar }); From 621e67bca7f2408334e0040922e28b093bd3f2fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Mar 2022 13:35:17 +0200 Subject: [PATCH 14/21] Revert deletion of `FindEditor` in theme --- crates/search/src/buffer_search.rs | 9 +++++---- crates/search/src/project_search.rs | 7 ++++--- crates/theme/src/theme.rs | 10 ++++++++-- crates/zed/assets/themes/_base.toml | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 9e2ed67a99..caf25cbec1 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -77,7 +77,7 @@ impl View for BufferSearchBar { let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { - theme.search.editor.container + theme.search.editor.input.container }; Flex::row() .with_child( @@ -107,7 +107,7 @@ impl View for BufferSearchBar { .with_style(editor_container) .aligned() .constrained() - .with_max_width(theme.search.max_editor_width) + .with_max_width(theme.search.editor.max_width) .boxed(), ) .with_child( @@ -157,8 +157,9 @@ impl ToolbarItemView for BufferSearchBar { 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.clone()), cx)); + let query_editor = cx.add_view(|cx| { + Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx) + }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2d3bbc5f27..293c33ae6a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -328,7 +328,8 @@ impl ProjectSearchView { .detach(); let query_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line(Some(|theme| theme.search.editor.clone()), cx); + let mut editor = + Editor::single_line(Some(|theme| theme.search.editor.input.clone()), cx); editor.set_text(query_text, cx); editor }); @@ -685,7 +686,7 @@ impl View for ProjectSearchBar { let editor_container = if search.query_contains_error { theme.search.invalid_editor } else { - theme.search.editor.container + theme.search.editor.input.container }; Flex::row() .with_child( @@ -713,7 +714,7 @@ impl View for ProjectSearchBar { .with_style(editor_container) .aligned() .constrained() - .with_max_width(theme.search.max_editor_width) + .with_max_width(theme.search.editor.max_width) .boxed(), ) .with_child( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 703079523e..54b85d65c2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -105,8 +105,7 @@ pub struct Toolbar { pub struct Search { #[serde(flatten)] pub container: ContainerStyle, - pub max_editor_width: f32, - pub editor: FieldEditor, + pub editor: FindEditor, pub invalid_editor: ContainerStyle, pub option_button_group: ContainerStyle, pub option_button: ContainedText, @@ -120,6 +119,13 @@ pub struct Search { pub tab_icon_spacing: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct FindEditor { + #[serde(flatten)] + pub input: FieldEditor, + pub max_width: f32, +} + #[derive(Deserialize, Default)] pub struct Sidebar { #[serde(flatten)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 06e9c4f3b3..4f598e6ac6 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -361,7 +361,6 @@ tab_icon_spacing = 4 tab_summary_spacing = 10 [search] -max_editor_width = 250 match_background = "$state.highlighted_line" results_status = { extends = "$text.0", size = 18 } tab_icon_width = 14 @@ -396,6 +395,7 @@ extends = "$text.2" padding = 6 [search.editor] +max_width = 250 background = "$surface.0" corner_radius = 6 padding = { left = 13, right = 13, top = 3, bottom = 3 } From 0453dd1101cd1f8499013f9f99ac7a3bb91c0cc4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Mar 2022 16:38:00 +0200 Subject: [PATCH 15/21] Allow flex items to float to the end of the flex axis Co-Authored-By: Nathan Sobo --- crates/chat_panel/src/chat_panel.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 2 +- crates/file_finder/src/file_finder.rs | 36 +++--- crates/gpui/src/elements.rs | 11 +- crates/gpui/src/elements/flex.rs | 118 +++++++++++------- crates/outline/src/outline.rs | 6 +- crates/project_symbols/src/project_symbols.rs | 6 +- crates/search/src/buffer_search.rs | 6 +- crates/search/src/project_search.rs | 12 +- crates/theme_selector/src/theme_selector.rs | 6 +- crates/workspace/src/pane.rs | 4 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/sidebar.rs | 2 +- crates/workspace/src/status_bar.rs | 2 +- crates/workspace/src/toolbar.rs | 2 +- crates/workspace/src/workspace.rs | 25 ++-- 16 files changed, 138 insertions(+), 104 deletions(-) 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/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/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/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 caf25cbec1..90d2876092 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -82,11 +82,7 @@ impl View for BufferSearchBar { Flex::row() .with_child( Flex::row() - .with_child( - ChildView::new(&self.query_editor) - .flexible(1., true) - .boxed(), - ) + .with_child(ChildView::new(&self.query_editor).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 { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 293c33ae6a..009c67ba93 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -164,12 +164,10 @@ 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() + ChildView::new(&self.results_editor).flex(1., true).boxed() } } @@ -691,11 +689,7 @@ impl View for ProjectSearchBar { Flex::row() .with_child( Flex::row() - .with_child( - ChildView::new(&search.query_editor) - .flexible(1., true) - .boxed(), - ) + .with_child(ChildView::new(&search.query_editor).flex(1., true).boxed()) .with_children(search.active_match_index.map(|match_ix| { Label::new( format!( 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 ec9319a396..d48a5711a3 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -612,7 +612,7 @@ impl Pane { Empty::new() .contained() .with_border(theme.workspace.tab.container.border) - .flexible(0., true) + .flex(0., true) .named("filler"), ); @@ -641,7 +641,7 @@ impl View for Pane { Flex::column() .with_child(self.render_tabs(cx)) .with_child(ChildView::new(&self.toolbar).boxed()) - .with_child(ChildView::new(active_item).flexible(1., true).boxed()) + .with_child(ChildView::new(active_item).flex(1., true).boxed()) .boxed() } else { Empty::new().boxed() 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 index a709f4bd05..ed3370c315 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -46,12 +46,12 @@ impl View for Toolbar { .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/workspace.rs b/crates/workspace/src/workspace.rs index d3a644d852..2a6a3c6355 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1946,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() From 8bfac63e0d4a7bd44d3799870cbb41bcf80f2859 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 31 Mar 2022 10:36:39 -0600 Subject: [PATCH 16/21] Render the search UI on a separate row from the breadcrumbs - In project search, render it above the breadcrumbs - In buffer search, render it below Co-Authored-By: Antonio Scandurra Co-Authored-By: Max Brunsfeld --- Cargo.lock | 1 + crates/breadcrumbs/Cargo.toml | 1 + crates/breadcrumbs/src/breadcrumbs.rs | 33 +++++-- crates/search/src/buffer_search.rs | 136 ++++++++++++++------------ crates/search/src/project_search.rs | 15 ++- crates/search/src/search.rs | 2 +- crates/workspace/src/toolbar.rs | 135 +++++++++++++++++-------- crates/workspace/src/workspace.rs | 2 +- crates/zed/src/zed.rs | 7 +- 9 files changed, 207 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d119b987e5..3db82b3cb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -727,6 +727,7 @@ dependencies = [ "editor", "gpui", "language", + "search", "theme", "workspace", ] diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 2e6e07ca19..2e74fd2090 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -12,6 +12,7 @@ collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +search = { path = "../search" } theme = { path = "../theme" } workspace = { path = "../workspace" } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 19042b3e40..5bbba7b973 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -3,9 +3,10 @@ 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, ToolbarItemView}; +use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView}; pub struct Breadcrumbs { editor: Option>, @@ -83,17 +84,29 @@ impl ToolbarItemView for Breadcrumbs { &mut self, active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, - ) { + ) -> ToolbarItemLocation { + cx.notify(); self.editor_subscription = None; self.editor = None; - if let Some(editor) = active_pane_item.and_then(|i| i.act_as::(cx)) { - self.editor_subscription = Some(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(item) = active_pane_item { + if let Some(editor) = item.act_as::(cx) { + self.editor_subscription = + Some(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 item.downcast::().is_some() { + ToolbarItemLocation::Secondary + } else { + ToolbarItemLocation::PrimaryLeft + } + } else { + ToolbarItemLocation::Hidden + } + } else { + ToolbarItemLocation::Hidden } - cx.notify(); } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 90d2876092..d7b5eec672 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -8,13 +8,17 @@ use gpui::{ use language::OffsetRangeExt; use project::search::SearchQuery; use std::ops::Range; -use workspace::{ItemHandle, Pane, Settings, ToolbarItemView}; +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")), @@ -57,7 +61,7 @@ pub struct BufferSearchBar { } impl Entity for BufferSearchBar { - type Event = (); + type Event = Event; } impl View for BufferSearchBar { @@ -70,70 +74,66 @@ impl View for BufferSearchBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - if self.dismissed || self.active_editor.is_none() { - Empty::new().boxed() + let theme = cx.global::().theme.clone(); + let editor_container = if self.query_contains_error { + theme.search.invalid_editor } else { - let theme = cx.global::().theme.clone(); - let editor_container = if self.query_contains_error { - theme.search.invalid_editor - } else { - theme.search.editor.input.container - }; - Flex::row() - .with_child( - Flex::row() - .with_child(ChildView::new(&self.query_editor).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() - }; + theme.search.editor.input.container + }; + Flex::row() + .with_child( + Flex::row() + .with_child(ChildView::new(&self.query_editor).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_max_width(theme.search.editor.max_width) - .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_search_option( - "Case", - SearchOption::CaseSensitive, - cx, - )) - .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) - .contained() - .with_style(theme.search.option_button_group) - .aligned() - .boxed(), - ) - .named("search bar") - } + 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_max_width(theme.search.editor.max_width) + .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_search_option("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.search.option_button_group) + .aligned() + .boxed(), + ) + .named("search bar") } } impl ToolbarItemView for BufferSearchBar { - fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + fn set_active_pane_item( + &mut self, + item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { cx.notify(); self.active_editor_subscription.take(); self.active_editor.take(); @@ -145,9 +145,21 @@ impl ToolbarItemView for BufferSearchBar { Some(cx.subscribe(&editor, Self::on_active_editor_event)); self.active_editor = Some(editor); self.update_matches(false, cx); - return; + if !self.dismissed { + return ToolbarItemLocation::Secondary; + } } } + + ToolbarItemLocation::Hidden + } + + fn location_for_event(&self, _: &Self::Event, _: ToolbarItemLocation) -> ToolbarItemLocation { + if self.active_editor.is_some() && !self.dismissed { + ToolbarItemLocation::Secondary + } else { + ToolbarItemLocation::Hidden + } } } @@ -186,6 +198,7 @@ impl BufferSearchBar { if let Some(active_editor) = self.active_editor.as_ref() { cx.focus(active_editor); } + cx.emit(Event::UpdateLocation); cx.notify(); } @@ -234,6 +247,7 @@ impl BufferSearchBar { self.dismissed = false; cx.notify(); + cx.emit(Event::UpdateLocation); true } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 009c67ba93..7026ac3473 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -16,7 +16,9 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{Item, ItemNavHistory, Pane, Settings, ToolbarItemView, Workspace}; +use workspace::{ + Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace, +}; action!(Deploy); action!(Search); @@ -56,7 +58,7 @@ struct ProjectSearch { active_query: Option, } -struct ProjectSearchView { +pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, @@ -136,7 +138,7 @@ impl ProjectSearch { } } -enum ViewEvent { +pub enum ViewEvent { UpdateTab, } @@ -748,14 +750,17 @@ impl ToolbarItemView for ProjectSearchBar { &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 + } else { + ToolbarItemLocation::Hidden } - cx.notify(); } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index e1ef1357ce..38d3a5fce8 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,7 +1,7 @@ pub use buffer_search::BufferSearchBar; use editor::{Anchor, MultiBufferSnapshot}; use gpui::{action, MutableAppContext}; -pub use project_search::ProjectSearchBar; +pub use project_search::{ProjectSearchBar, ProjectSearchView}; use std::{ cmp::{self, Ordering}, ops::Range, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index ed3370c315..6580818e0f 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -9,22 +9,38 @@ pub trait ToolbarItemView: View { &mut self, active_pane_item: Option<&dyn crate::ItemHandle>, cx: &mut ViewContext, - ); + ) -> ToolbarItemLocation; + + fn location_for_event( + &self, + _event: &Self::Event, + current_location: ToolbarItemLocation, + ) -> 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, Eq)] +pub enum ToolbarItemLocation { + Hidden, + PrimaryLeft, + PrimaryRight, + Secondary, } pub struct Toolbar { active_pane_item: Option>, - left_items: Vec>, - right_items: Vec>, + items: Vec<(Box, ToolbarItemLocation)>, } impl Entity for Toolbar { @@ -38,26 +54,50 @@ impl View for Toolbar { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &cx.global::().theme.workspace.toolbar; - Flex::row() - .with_children(self.left_items.iter().map(|i| { - ChildView::new(i.as_ref()) - .aligned() - .contained() - .with_margin_right(theme.item_spacing) - .boxed() - })) - .with_children(self.right_items.iter().map(|i| { - ChildView::new(i.as_ref()) - .aligned() - .contained() - .with_margin_left(theme.item_spacing) - .flex_float() + + 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 => primary_left_items.push(item), + ToolbarItemLocation::PrimaryRight => primary_right_items.push(item), + ToolbarItemLocation::Secondary => secondary_item = Some(item), + } + } + + Flex::column() + .with_child( + Flex::row() + .with_children(primary_left_items.iter().map(|i| { + ChildView::new(i.as_ref()) + .aligned() + .contained() + .with_margin_right(theme.item_spacing) + .boxed() + })) + .with_children(primary_right_items.iter().map(|i| { + ChildView::new(i.as_ref()) + .aligned() + .contained() + .with_margin_left(theme.item_spacing) + .flex_float() + .boxed() + })) + .constrained() + .with_height(theme.height) + .boxed(), + ) + .with_children(secondary_item.map(|item| { + ChildView::new(item.as_ref()) + .constrained() + .with_height(theme.height) .boxed() })) .contained() .with_style(theme.container) - .constrained() - .with_height(theme.height) .boxed() } } @@ -66,49 +106,58 @@ impl Toolbar { pub fn new() -> Self { Self { active_pane_item: None, - left_items: Default::default(), - right_items: Default::default(), + items: Default::default(), } } - pub fn add_left_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + pub fn add_item(&mut self, item: ViewHandle, cx: &mut ViewContext) where T: 'static + ToolbarItemView, { - item.set_active_pane_item(self.active_pane_item.as_deref(), cx); - self.left_items.push(Box::new(item)); - cx.notify(); - } - - pub fn add_right_item(&mut self, item: ViewHandle, cx: &mut ViewContext) - where - T: 'static + ToolbarItemView, - { - item.set_active_pane_item(self.active_pane_item.as_deref(), cx); - self.right_items.push(Box::new(item)); + 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); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + }) + .detach(); + self.items.push((Box::new(item), dbg!(location))); cx.notify(); } pub fn set_active_pane_item( &mut self, - item: Option<&dyn ItemHandle>, + pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { - self.active_pane_item = item.map(|item| item.boxed_clone()); - for tool in self.left_items.iter().chain(&self.right_items) { - tool.set_active_pane_item(item, cx); + 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.left_items + self.items .iter() - .chain(&self.right_items) - .find_map(|tool| tool.to_any().downcast()) + .find_map(|(item, _)| item.to_any().downcast()) } } impl ToolbarItemViewHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + fn to_any(&self) -> AnyViewHandle { self.into() } @@ -117,10 +166,10 @@ impl ToolbarItemViewHandle for ViewHandle { &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) - }); + }) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2a6a3c6355..9929cd9a51 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -48,7 +48,7 @@ use std::{ }, }; use theme::{Theme, ThemeRegistry}; -pub use toolbar::ToolbarItemView; +pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; type ProjectItemBuilders = HashMap< diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 84cf163b76..c94f8a0e81 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -111,12 +111,11 @@ pub fn build_workspace( pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let breadcrumbs = cx.add_view(|_| Breadcrumbs::new()); - toolbar.add_left_item(breadcrumbs, cx); - + toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); - toolbar.add_right_item(buffer_search_bar, cx); + toolbar.add_item(buffer_search_bar, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - toolbar.add_right_item(project_search_bar, cx); + toolbar.add_item(project_search_bar, cx); }) }); }) From 903810f22ef6047adfe0772fd559f21ea8eff6d5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 31 Mar 2022 11:44:16 -0600 Subject: [PATCH 17/21] Style search in buffer below breadcrumbs We still have issues with project search styling. Co-Authored-By: Antonio Scandurra Co-Authored-By: Max Brunsfeld --- crates/breadcrumbs/src/breadcrumbs.rs | 2 ++ crates/gpui/src/color.rs | 4 ++++ crates/search/src/buffer_search.rs | 12 +++++++++++- crates/theme/src/theme.rs | 8 ++------ crates/workspace/src/toolbar.rs | 2 +- crates/zed/assets/themes/_base.toml | 17 ++++++++++------- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 5bbba7b973..9fd67034f4 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -75,6 +75,8 @@ impl View for Breadcrumbs { .boxed(), ] })) + .contained() + .with_style(theme.breadcrumbs.container) .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/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d7b5eec672..87f145d4fc 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -83,7 +83,13 @@ impl View for BufferSearchBar { Flex::row() .with_child( Flex::row() - .with_child(ChildView::new(&self.query_editor).flex(1., true).boxed()) + .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 { @@ -104,7 +110,9 @@ impl View for BufferSearchBar { .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( @@ -124,6 +132,8 @@ impl View for BufferSearchBar { .aligned() .boxed(), ) + .contained() + .with_style(theme.search.container) .named("search bar") } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 54b85d65c2..8fa15a9235 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -26,7 +26,7 @@ pub struct Theme { pub editor: Editor, pub search: Search, pub project_diagnostics: ProjectDiagnostics, - pub breadcrumbs: Breadcrumbs, + pub breadcrumbs: ContainedText, } #[derive(Deserialize, Default)] @@ -123,6 +123,7 @@ pub struct Search { pub struct FindEditor { #[serde(flatten)] pub input: FieldEditor, + pub min_width: f32, pub max_width: f32, } @@ -278,11 +279,6 @@ pub struct ProjectDiagnostics { pub tab_summary_spacing: f32, } -#[derive(Clone, Deserialize, Default)] -pub struct Breadcrumbs { - pub text: TextStyle, -} - #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 6580818e0f..f15d1e1c75 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -127,7 +127,7 @@ impl Toolbar { } }) .detach(); - self.items.push((Box::new(item), dbg!(location))); + self.items.push((Box::new(item), location)); cx.notify(); } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 4f598e6ac6..d5b25cbe1a 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -87,13 +87,15 @@ lsp_message = "$text.2" [workspace.toolbar] background = "$surface.1" border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false } -height = 44 +height = 34 item_spacing = 8 -padding.left = 8 +padding.left = 16 padding.right = 8 +padding.bottom = 4 [breadcrumbs] -text = "$text.1" +extends = "$text.1" +padding = { left = 6 } [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } @@ -395,15 +397,16 @@ extends = "$text.2" padding = 6 [search.editor] -max_width = 250 +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" -border = { width = 1, color = "$border.0" } +border = { width = 1, color = "$border.0", overlay = true } [search.invalid_editor] extends = "$search.editor" From 9f939bd007cac48213c21f32462e9a11bf09ee8a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Apr 2022 09:40:49 +0200 Subject: [PATCH 18/21] Fix styling of project search bar --- crates/breadcrumbs/src/breadcrumbs.rs | 2 ++ crates/search/src/project_search.rs | 16 ++++++++++++---- crates/workspace/src/toolbar.rs | 2 ++ crates/zed/assets/themes/_base.toml | 6 ++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 9fd67034f4..94b8a4459e 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -77,6 +77,8 @@ impl View for Breadcrumbs { })) .contained() .with_style(theme.breadcrumbs.container) + .aligned() + .left() .boxed() } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7026ac3473..7fb7036576 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -691,7 +691,13 @@ impl View for ProjectSearchBar { Flex::row() .with_child( Flex::row() - .with_child(ChildView::new(&search.query_editor).flex(1., true).boxed()) + .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!( @@ -710,7 +716,9 @@ impl View for ProjectSearchBar { .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( @@ -735,9 +743,9 @@ impl View for ProjectSearchBar { .boxed(), ) .contained() - .with_style(theme.workspace.toolbar.container) - .constrained() - .with_height(theme.workspace.toolbar.height) + .with_style(theme.search.container) + .aligned() + .left() .named("project search") } else { Empty::new().boxed() diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index f15d1e1c75..06a4f6440b 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -76,6 +76,7 @@ impl View for Toolbar { .aligned() .contained() .with_margin_right(theme.item_spacing) + .flex(1., false) .boxed() })) .with_children(primary_right_items.iter().map(|i| { @@ -84,6 +85,7 @@ impl View for Toolbar { .contained() .with_margin_left(theme.item_spacing) .flex_float() + .flex(1., false) .boxed() })) .constrained() diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index d5b25cbe1a..7f235cbf48 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -89,9 +89,7 @@ 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 -padding.right = 8 -padding.bottom = 4 +padding = { left = 16, right = 8, top = 4, bottom = 4 } [breadcrumbs] extends = "$text.1" @@ -406,7 +404,7 @@ margin = { right = 5 } text = "$text.0" placeholder_text = "$text.2" selection = "$selection.host" -border = { width = 1, color = "$border.0", overlay = true } +border = { width = 1, color = "$border.0" } [search.invalid_editor] extends = "$search.editor" From 7f9ff47089b7e352912126951e7af684f67fb513 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Apr 2022 10:00:21 +0200 Subject: [PATCH 19/21] Hide breadcrumbs when project search has no results --- crates/breadcrumbs/src/breadcrumbs.rs | 50 ++++++++++++++++++++++----- crates/search/src/buffer_search.rs | 12 +++++-- crates/search/src/project_search.rs | 4 +++ crates/workspace/src/toolbar.rs | 9 +++-- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 94b8a4459e..85723d9399 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -8,16 +8,22 @@ use std::borrow::Cow; use theme::SyntaxTheme; use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView}; +pub enum Event { + UpdateLocation, +} + pub struct Breadcrumbs { editor: Option>, - editor_subscription: Option, + project_search: Option>, + subscriptions: Vec, } impl Breadcrumbs { pub fn new() -> Self { Self { editor: Default::default(), - editor_subscription: Default::default(), + subscriptions: Default::default(), + project_search: Default::default(), } } @@ -42,7 +48,7 @@ impl Breadcrumbs { } impl Entity for Breadcrumbs { - type Event = (); + type Event = Event; } impl View for Breadcrumbs { @@ -90,19 +96,30 @@ impl ToolbarItemView for Breadcrumbs { cx: &mut ViewContext, ) -> ToolbarItemLocation { cx.notify(); - self.editor_subscription = None; + 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.editor_subscription = - Some(cx.subscribe(&editor, |_, _, event, cx| match event { + 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 item.downcast::().is_some() { - ToolbarItemLocation::Secondary + 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 } @@ -113,4 +130,21 @@ impl ToolbarItemView for Breadcrumbs { 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/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 87f145d4fc..b5f8eedf80 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2,8 +2,9 @@ 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; @@ -164,7 +165,12 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } - fn location_for_event(&self, _: &Self::Event, _: ToolbarItemLocation) -> ToolbarItemLocation { + fn location_for_event( + &self, + _: &Self::Event, + _: ToolbarItemLocation, + _: &AppContext, + ) -> ToolbarItemLocation { if self.active_editor.is_some() && !self.dismissed { ToolbarItemLocation::Secondary } else { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7fb7036576..0226e2c844 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -487,6 +487,10 @@ impl ProjectSearchView { cx.notify(); } } + + pub fn has_matches(&self) -> bool { + self.active_match_index.is_some() + } } impl ProjectSearchBar { diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 06a4f6440b..8bcca1bde3 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -1,7 +1,7 @@ use crate::{ItemHandle, Settings}; use gpui::{ - elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, View, - ViewContext, ViewHandle, + elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext, + View, ViewContext, ViewHandle, }; pub trait ToolbarItemView: View { @@ -15,6 +15,7 @@ pub trait ToolbarItemView: View { &self, _event: &Self::Event, current_location: ToolbarItemLocation, + _cx: &AppContext, ) -> ToolbarItemLocation { current_location } @@ -121,7 +122,9 @@ impl Toolbar { 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); + let new_location = item + .read(cx) + .location_for_event(event, *current_location, cx); if new_location != *current_location { *current_location = new_location; cx.notify(); From 6d4c748d82ae4d7c6151102ed5f3c9b7a2836f6e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Apr 2022 10:15:37 +0200 Subject: [PATCH 20/21] Show "untitled" in breadcrumbs when the buffer has no path --- crates/breadcrumbs/src/breadcrumbs.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 85723d9399..e575d27e50 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -39,11 +39,7 @@ impl Breadcrumbs { .read(cx) .read(cx) .symbols_containing(cursor, Some(theme))?; - if buffer.path().is_none() && symbols.is_empty() { - None - } else { - Some((buffer, symbols)) - } + Some((buffer, symbols)) } } From cd5389b4d894819de88d2973a27655c608a2edaf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Apr 2022 10:55:38 +0200 Subject: [PATCH 21/21] Let toolbar items specify `flex` when they have a primary location --- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- crates/search/src/project_search.rs | 4 +- crates/workspace/src/toolbar.rs | 68 +++++++++++++++------------ 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index e575d27e50..ce32fb2272 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -117,7 +117,7 @@ impl ToolbarItemView for Breadcrumbs { ToolbarItemLocation::Hidden } } else { - ToolbarItemLocation::PrimaryLeft + ToolbarItemLocation::PrimaryLeft { flex: None } } } else { ToolbarItemLocation::Hidden diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0226e2c844..65bb07ae46 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -769,7 +769,9 @@ impl ToolbarItemView for ProjectSearchBar { 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 + ToolbarItemLocation::PrimaryLeft { + flex: Some((1., false)), + } } else { ToolbarItemLocation::Hidden } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 8bcca1bde3..8212b25082 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -31,11 +31,11 @@ trait ToolbarItemViewHandle { ) -> ToolbarItemLocation; } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum ToolbarItemLocation { Hidden, - PrimaryLeft, - PrimaryRight, + PrimaryLeft { flex: Option<(f32, bool)> }, + PrimaryRight { flex: Option<(f32, bool)> }, Secondary, } @@ -61,44 +61,52 @@ impl View for Toolbar { let mut secondary_item = None; for (item, position) in &self.items { - match position { + match *position { ToolbarItemLocation::Hidden => {} - ToolbarItemLocation::PrimaryLeft => primary_left_items.push(item), - ToolbarItemLocation::PrimaryRight => primary_right_items.push(item), - ToolbarItemLocation::Secondary => secondary_item = Some(item), + 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.iter().map(|i| { - ChildView::new(i.as_ref()) - .aligned() - .contained() - .with_margin_right(theme.item_spacing) - .flex(1., false) - .boxed() - })) - .with_children(primary_right_items.iter().map(|i| { - ChildView::new(i.as_ref()) - .aligned() - .contained() - .with_margin_left(theme.item_spacing) - .flex_float() - .flex(1., false) - .boxed() - })) + .with_children(primary_left_items) + .with_children(primary_right_items) .constrained() .with_height(theme.height) .boxed(), ) - .with_children(secondary_item.map(|item| { - ChildView::new(item.as_ref()) - .constrained() - .with_height(theme.height) - .boxed() - })) + .with_children(secondary_item) .contained() .with_style(theme.container) .boxed()