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