Merge pull request #705 from zed-industries/breadcrumbs

Introduce breadcrumbs
This commit is contained in:
Antonio Scandurra 2022-04-01 11:02:54 +02:00 committed by GitHub
commit bdd95a82d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1208 additions and 568 deletions

14
Cargo.lock generated
View File

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

View File

@ -0,0 +1,22 @@
[package]
name = "breadcrumbs"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/breadcrumbs.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
search = { path = "../search" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@ -0,0 +1,146 @@
use editor::{Anchor, Editor};
use gpui::{
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use language::{BufferSnapshot, OutlineItem};
use search::ProjectSearchView;
use std::borrow::Cow;
use theme::SyntaxTheme;
use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
pub enum Event {
UpdateLocation,
}
pub struct Breadcrumbs {
editor: Option<ViewHandle<Editor>>,
project_search: Option<ViewHandle<ProjectSearchView>>,
subscriptions: Vec<Subscription>,
}
impl Breadcrumbs {
pub fn new() -> Self {
Self {
editor: Default::default(),
subscriptions: Default::default(),
project_search: Default::default(),
}
}
fn active_symbols(
&self,
theme: &SyntaxTheme,
cx: &AppContext,
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
let editor = self.editor.as_ref()?.read(cx);
let cursor = editor.newest_anchor_selection().head();
let (buffer, symbols) = editor
.buffer()
.read(cx)
.read(cx)
.symbols_containing(cursor, Some(theme))?;
Some((buffer, symbols))
}
}
impl Entity for Breadcrumbs {
type Event = Event;
}
impl View for Breadcrumbs {
fn ui_name() -> &'static str {
"Breadcrumbs"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let (buffer, symbols) =
if let Some((buffer, symbols)) = self.active_symbols(&theme.editor.syntax, cx) {
(buffer, symbols)
} else {
return Empty::new().boxed();
};
let filename = if let Some(path) = buffer.path() {
path.to_string_lossy()
} else {
Cow::Borrowed("untitled")
};
Flex::row()
.with_child(Label::new(filename.to_string(), theme.breadcrumbs.text.clone()).boxed())
.with_children(symbols.into_iter().flat_map(|symbol| {
[
Label::new("".to_string(), theme.breadcrumbs.text.clone()).boxed(),
Text::new(symbol.text, theme.breadcrumbs.text.clone())
.with_highlights(symbol.highlight_ranges)
.boxed(),
]
}))
.contained()
.with_style(theme.breadcrumbs.container)
.aligned()
.left()
.boxed()
}
}
impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.subscriptions.clear();
self.editor = None;
self.project_search = None;
if let Some(item) = active_pane_item {
if let Some(editor) = item.act_as::<Editor>(cx) {
self.subscriptions
.push(cx.subscribe(&editor, |_, _, event, cx| match event {
editor::Event::BufferEdited => cx.notify(),
editor::Event::SelectionsChanged { local } if *local => cx.notify(),
_ => {}
}));
self.editor = Some(editor);
if let Some(project_search) = item.downcast::<ProjectSearchView>() {
self.subscriptions
.push(cx.subscribe(&project_search, |_, _, _, cx| {
cx.emit(Event::UpdateLocation);
}));
self.project_search = Some(project_search.clone());
if project_search.read(cx).has_matches() {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
} else {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
} else {
ToolbarItemLocation::Hidden
}
} else {
ToolbarItemLocation::Hidden
}
}
fn location_for_event(
&self,
_: &Event,
current_location: ToolbarItemLocation,
cx: &AppContext,
) -> ToolbarItemLocation {
if let Some(project_search) = self.project_search.as_ref() {
if project_search.read(cx).has_matches() {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
} else {
current_location
}
}
}

View File

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

View File

@ -212,7 +212,7 @@ impl ContactsPanel {
}));
}
})
.flexible(1., true)
.flex(1., true)
.boxed()
})
.constrained()

View File

@ -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, &());

View File

@ -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,9 +170,6 @@ impl FileFinder {
// .boxed(),
// )
.with_child(
Flexible::new(
1.0,
false,
Flex::column()
.with_child(
Label::new(file_name.to_string(), style.label.clone())
@ -180,8 +181,7 @@ impl FileFinder {
.with_highlights(full_path_positions)
.boxed(),
)
.boxed(),
)
.flex(1., false)
.boxed(),
)
.boxed(),

View File

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

View File

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

View File

@ -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,11 +44,11 @@ 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 {
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 {
@ -73,6 +73,7 @@ impl Flex {
}
}
}
}
}
impl Extend<ElementBox> for Flex {
@ -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 = ();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>) {
@ -72,31 +81,14 @@ impl View for SearchBar {
} else {
theme.search.editor.input.container
};
Flex::row()
.with_child(
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()
.left()
.flex(1., true)
.boxed(),
)
.with_children(self.active_editor.as_ref().and_then(|editor| {
@ -116,19 +108,44 @@ impl View for SearchBar {
)
}))
.contained()
.with_style(theme.search.container)
.with_style(editor_container)
.aligned()
.constrained()
.with_height(theme.workspace.toolbar.height)
.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_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)
.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
}
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)
});
ToolbarItemLocation::Hidden
}
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,62 +325,14 @@ 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();
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;
}
} 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 dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
if pane.toolbar::<SearchBar>().is_some() {
pane.dismiss_toolbar(cx);
}
}
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
if let Some(active_editor) = self.active_editor.as_ref() {
@ -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
});

View File

@ -6,8 +6,8 @@ use collections::HashMap;
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakModelHandle, WeakViewHandle,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
};
use project::{search::SearchQuery, Project};
use std::{
@ -16,7 +16,9 @@ use std::{
path::PathBuf,
};
use util::ResultExt as _;
use workspace::{Item, ItemNavHistory, Settings, Workspace};
use workspace::{
Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
};
action!(Deploy);
action!(Search);
@ -31,29 +33,21 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
pub fn init(cx: &mut MutableAppContext) {
cx.set_global(ActiveSearches::default());
cx.add_bindings([
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-shift-F", ToggleFocus, Some("Pane")),
Binding::new("cmd-f", ToggleFocus, Some("Pane")),
Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
Binding::new("enter", Search, Some("ProjectSearchView")),
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")),
Binding::new(
"cmd-g",
SelectMatch(Direction::Next),
Some("ProjectSearchView"),
),
Binding::new(
"cmd-shift-G",
SelectMatch(Direction::Prev),
Some("ProjectSearchView"),
),
Binding::new("enter", Search, Some("ProjectSearchBar")),
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
]);
cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchView::search);
cx.add_action(ProjectSearchView::search_in_new);
cx.add_action(ProjectSearchView::toggle_search_option);
cx.add_action(ProjectSearchView::select_match);
cx.add_action(ProjectSearchView::toggle_focus);
cx.capture_action(ProjectSearchView::tab);
cx.add_action(ProjectSearchBar::search);
cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::toggle_search_option);
cx.add_action(ProjectSearchBar::select_match);
cx.add_action(ProjectSearchBar::toggle_focus);
cx.capture_action(ProjectSearchBar::tab);
}
struct ProjectSearch {
@ -64,7 +58,7 @@ struct ProjectSearch {
active_query: Option<SearchQuery>,
}
struct ProjectSearchView {
pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>,
@ -75,6 +69,11 @@ struct ProjectSearchView {
active_match_index: Option<usize>,
}
pub struct ProjectSearchBar {
active_project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>,
}
impl Entity for ProjectSearch {
type Event = ();
}
@ -139,7 +138,7 @@ impl ProjectSearch {
}
}
enum ViewEvent {
pub enum ViewEvent {
UpdateTab,
}
@ -154,7 +153,7 @@ impl View for ProjectSearchView {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx);
let results = if model.match_ranges.is_empty() {
if model.match_ranges.is_empty() {
let theme = &cx.global::<Settings>().theme;
let text = if self.query_editor.read(cx).text(cx).is_empty() {
""
@ -167,18 +166,11 @@ impl View for ProjectSearchView {
.aligned()
.contained()
.with_background_color(theme.editor.background)
.flexible(1., true)
.flex(1., true)
.boxed()
} else {
ChildView::new(&self.results_editor)
.flexible(1., true)
.boxed()
};
Flex::column()
.with_child(self.render_query_editor(cx))
.with_child(results)
.boxed()
ChildView::new(&self.results_editor).flex(1., true).boxed()
}
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@ -402,45 +394,12 @@ impl ProjectSearchView {
}
}
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
fn search(&mut self, cx: &mut ViewContext<Self>) {
if let Some(query) = self.build_search_query(cx) {
self.model.update(cx, |model, cx| model.search(query, cx));
}
}
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
if let Some(search_view) = workspace
.active_item(cx)
.and_then(|item| item.downcast::<ProjectSearchView>())
{
let new_query = search_view.update(cx, |search_view, cx| {
let new_query = search_view.build_search_query(cx);
if new_query.is_some() {
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
search_view.query_editor.update(cx, |editor, cx| {
editor.set_text(old_query.as_str(), cx);
});
search_view.regex = old_query.is_regex();
search_view.whole_word = old_query.whole_word();
search_view.case_sensitive = old_query.case_sensitive();
}
}
new_query
});
if let Some(new_query) = new_query {
let model = cx.add_model(|cx| {
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
model.search(new_query, cx);
model
});
workspace.add_item(
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
cx,
);
}
}
}
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx);
if self.regex {
@ -461,22 +420,7 @@ impl ProjectSearchView {
}
}
fn toggle_search_option(
&mut self,
ToggleSearchOption(option): &ToggleSearchOption,
cx: &mut ViewContext<Self>,
) {
let value = match option {
SearchOption::WholeWord => &mut self.whole_word,
SearchOption::CaseSensitive => &mut self.case_sensitive,
SearchOption::Regex => &mut self.regex,
};
*value = !*value;
self.search(&Search, cx);
cx.notify();
}
fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
let model = self.model.read(cx);
let results_editor = self.results_editor.read(cx);
@ -495,26 +439,6 @@ impl ProjectSearchView {
}
}
fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
self.focus_query_editor(cx);
}
}
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
cx.propagate_action()
}
}
fn focus_query_editor(&self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&SelectAll, cx);
@ -564,93 +488,126 @@ impl ProjectSearchView {
}
}
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
pub fn has_matches(&self) -> bool {
self.active_match_index.is_some()
}
}
impl ProjectSearchBar {
pub fn new() -> Self {
Self {
active_project_search: Default::default(),
subscription: Default::default(),
}
}
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| search_view.search(cx));
}
}
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
if let Some(search_view) = workspace
.active_item(cx)
.and_then(|item| item.downcast::<ProjectSearchView>())
{
let new_query = search_view.update(cx, |search_view, cx| {
let new_query = search_view.build_search_query(cx);
if new_query.is_some() {
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
search_view.query_editor.update(cx, |editor, cx| {
editor.set_text(old_query.as_str(), cx);
});
search_view.regex = old_query.is_regex();
search_view.whole_word = old_query.whole_word();
search_view.case_sensitive = old_query.case_sensitive();
}
}
new_query
});
if let Some(new_query) = new_query {
let model = cx.add_model(|cx| {
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
model.search(new_query, cx);
model
});
workspace.add_item(
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
cx,
);
}
}
}
fn select_match(
pane: &mut Pane,
&SelectMatch(direction): &SelectMatch,
cx: &mut ViewContext<Pane>,
) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
search_view.select_match(direction, cx);
});
} else {
theme.search.editor.input.container
};
Flex::row()
.with_child(
ChildView::new(&self.query_editor)
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_max_width(theme.search.editor.max_width)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx))
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.with_children({
self.active_match_index.into_iter().flat_map(|match_ix| {
[
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
Label::new(
format!(
"{}/{}",
match_ix + 1,
self.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
]
})
})
.contained()
.with_style(theme.search.container)
.constrained()
.with_height(theme.workspace.toolbar.height)
.named("project search")
cx.propagate_action();
}
}
fn render_option_button(
&self,
icon: &str,
option: SearchOption,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_active = self.is_option_enabled(option);
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme.search;
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
(true, false) => &theme.active_option_button,
(true, true) => &theme.active_hovered_option_button,
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx) {
if !search_view.model.read(cx).match_ranges.is_empty() {
search_view.focus_results_editor(cx);
}
} else {
search_view.focus_query_editor(cx);
}
});
} else {
cx.propagate_action();
}
}
fn is_option_enabled(&self, option: SearchOption) -> bool {
match option {
SearchOption::WholeWord => self.whole_word,
SearchOption::CaseSensitive => self.case_sensitive,
SearchOption::Regex => self.regex,
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx) {
if !search_view.model.read(cx).match_ranges.is_empty() {
search_view.focus_results_editor(cx);
}
} else {
cx.propagate_action();
}
});
} else {
cx.propagate_action();
}
}
fn toggle_search_option(
&mut self,
ToggleSearchOption(option): &ToggleSearchOption,
cx: &mut ViewContext<Self>,
) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
let value = match option {
SearchOption::WholeWord => &mut search_view.whole_word,
SearchOption::CaseSensitive => &mut search_view.case_sensitive,
SearchOption::Regex => &mut search_view.regex,
};
*value = !*value;
search_view.search(cx);
});
cx.notify();
}
}
@ -677,6 +634,148 @@ impl ProjectSearchView {
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
fn render_option_button(
&self,
icon: &str,
option: SearchOption,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::new::<ProjectSearchBar, _, _>(option as usize, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme.search;
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
(true, false) => &theme.active_option_button,
(true, true) => &theme.active_hovered_option_button,
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
match option {
SearchOption::WholeWord => search.whole_word,
SearchOption::CaseSensitive => search.case_sensitive,
SearchOption::Regex => search.regex,
}
} else {
false
}
}
}
impl Entity for ProjectSearchBar {
type Event = ();
}
impl View for ProjectSearchBar {
fn ui_name() -> &'static str {
"ProjectSearchBar"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if search.query_contains_error {
theme.search.invalid_editor
} else {
theme.search.editor.input.container
};
Flex::row()
.with_child(
Flex::row()
.with_child(
ChildView::new(&search.query_editor)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.with_children(search.active_match_index.map(|match_ix| {
Label::new(
format!(
"{}/{}",
match_ix + 1,
search.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed()
}))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button(
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.search.container)
.aligned()
.left()
.named("project search")
} else {
Empty::new().boxed()
}
}
}
impl ToolbarItemView for ProjectSearchBar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.subscription = None;
self.active_project_search = None;
if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
self.active_project_search = Some(search);
ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
}
} else {
ToolbarItemLocation::Hidden
}
}
}
#[cfg(test)]
@ -726,7 +825,7 @@ mod tests {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
search_view.search(&Search, cx);
search_view.search(cx);
});
search_view.next_notification(&cx).await;
search_view.update(cx, |search_view, cx| {
@ -763,7 +862,7 @@ mod tests {
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
search_view.select_match(Direction::Next, cx);
});
search_view.update(cx, |search_view, cx| {
@ -774,7 +873,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
search_view.select_match(Direction::Next, cx);
});
search_view.update(cx, |search_view, cx| {
@ -785,7 +884,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
search_view.select_match(Direction::Next, cx);
});
search_view.update(cx, |search_view, cx| {
@ -796,7 +895,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(&SelectMatch(Direction::Prev), cx);
search_view.select_match(Direction::Prev, cx);
});
search_view.update(cx, |search_view, cx| {
@ -807,7 +906,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(&SelectMatch(Direction::Prev), cx);
search_view.select_match(Direction::Prev, cx);
});
search_view.update(cx, |search_view, cx| {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
use super::{ItemHandle, SplitDirection};
use crate::{Item, Settings, WeakItemHandle, Workspace};
use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
use collections::{HashMap, VecDeque};
use gpui::{
action,
@ -7,16 +7,11 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f},
keymap::Binding,
platform::{CursorStyle, NavigationDirection},
AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use project::{ProjectEntryId, ProjectPath};
use std::{
any::{Any, TypeId},
cell::RefCell,
cmp, mem,
rc::Rc,
};
use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
use util::ResultExt;
action!(Split, SplitDirection);
@ -101,28 +96,7 @@ pub struct Pane {
items: Vec<Box<dyn ItemHandle>>,
active_item_index: usize,
nav_history: Rc<RefCell<NavHistory>>,
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
active_toolbar_type: Option<TypeId>,
active_toolbar_visible: bool,
}
pub trait Toolbar: View {
fn active_item_changed(
&mut self,
item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> bool;
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
}
trait ToolbarHandle {
fn active_item_changed(
&self,
item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool;
fn on_dismiss(&self, cx: &mut MutableAppContext);
fn to_any(&self) -> AnyViewHandle;
toolbar: ViewHandle<Toolbar>,
}
pub struct ItemNavHistory {
@ -158,14 +132,12 @@ pub struct NavigationEntry {
}
impl Pane {
pub fn new() -> Self {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
items: Vec::new(),
active_item_index: 0,
nav_history: Default::default(),
toolbars: Default::default(),
active_toolbar_type: Default::default(),
active_toolbar_visible: false,
toolbar: cx.add_view(|_| Toolbar::new()),
}
}
@ -402,7 +374,7 @@ impl Pane {
self.items[prev_active_item_ix].deactivated(cx);
cx.emit(Event::ActivateItem { local });
}
self.update_active_toolbar(cx);
self.update_toolbar(cx);
if local {
self.focus_active_item(cx);
self.activate(cx);
@ -487,7 +459,7 @@ impl Pane {
self.focus_active_item(cx);
self.activate(cx);
}
self.update_active_toolbar(cx);
self.update_toolbar(cx);
cx.notify();
}
@ -502,63 +474,18 @@ impl Pane {
cx.emit(Event::Split(direction));
}
pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: Toolbar,
{
let type_id = TypeId::of::<V>();
if self.active_toolbar_type != Some(type_id) {
self.dismiss_toolbar(cx);
let active_item = self.active_item();
self.toolbars
.entry(type_id)
.or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
self.active_toolbar_type = Some(type_id);
self.active_toolbar_visible =
self.toolbars[&type_id].active_item_changed(active_item, cx);
cx.notify();
}
pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
&self.toolbar
}
pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
self.toolbars
.get_mut(&active_toolbar_type)
.unwrap()
.on_dismiss(cx);
self.active_toolbar_visible = false;
self.focus_active_item(cx);
cx.notify();
}
}
pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
self.toolbars
.get(&TypeId::of::<T>())
.and_then(|toolbar| toolbar.to_any().downcast())
}
pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
let type_id = self.active_toolbar_type?;
let toolbar = self.toolbars.get(&type_id)?;
if self.active_toolbar_visible {
Some(toolbar.to_any())
} else {
None
}
}
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self.items.get(self.active_item_index);
for (toolbar_type_id, toolbar) in &self.toolbars {
let visible = toolbar.active_item_changed(active_item.cloned(), cx);
if Some(*toolbar_type_id) == self.active_toolbar_type {
self.active_toolbar_visible = visible;
}
}
fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self
.items
.get(self.active_item_index)
.map(|item| item.as_ref());
self.toolbar.update(cx, |toolbar, cx| {
toolbar.set_active_pane_item(active_item, cx);
});
}
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
@ -685,7 +612,7 @@ impl Pane {
Empty::new()
.contained()
.with_border(theme.workspace.tab.container.border)
.flexible(0., true)
.flex(0., true)
.named("filler"),
);
@ -713,12 +640,8 @@ impl View for Pane {
EventHandler::new(if let Some(active_item) = self.active_item() {
Flex::column()
.with_child(self.render_tabs(cx))
.with_children(
self.active_toolbar()
.as_ref()
.map(|view| ChildView::new(view).boxed()),
)
.with_child(ChildView::new(active_item).flexible(1., true).boxed())
.with_child(ChildView::new(&self.toolbar).boxed())
.with_child(ChildView::new(active_item).flex(1., true).boxed())
.boxed()
} else {
Empty::new().boxed()
@ -740,24 +663,6 @@ impl View for Pane {
}
}
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
fn active_item_changed(
&self,
item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool {
self.update(cx, |this, cx| this.active_item_changed(item, cx))
}
fn on_dismiss(&self, cx: &mut MutableAppContext) {
self.update(cx, |this, cx| this.on_dismiss(cx));
}
fn to_any(&self) -> AnyViewHandle {
self.into()
}
}
impl ItemNavHistory {
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
Self {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,193 @@
use crate::{ItemHandle, Settings};
use gpui::{
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
View, ViewContext, ViewHandle,
};
pub trait ToolbarItemView: View {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation;
fn location_for_event(
&self,
_event: &Self::Event,
current_location: ToolbarItemLocation,
_cx: &AppContext,
) -> ToolbarItemLocation {
current_location
}
}
trait ToolbarItemViewHandle {
fn id(&self) -> usize;
fn to_any(&self) -> AnyViewHandle;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext,
) -> ToolbarItemLocation;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ToolbarItemLocation {
Hidden,
PrimaryLeft { flex: Option<(f32, bool)> },
PrimaryRight { flex: Option<(f32, bool)> },
Secondary,
}
pub struct Toolbar {
active_pane_item: Option<Box<dyn ItemHandle>>,
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
impl Entity for Toolbar {
type Event = ();
}
impl View for Toolbar {
fn ui_name() -> &'static str {
"Toolbar"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.workspace.toolbar;
let mut primary_left_items = Vec::new();
let mut primary_right_items = Vec::new();
let mut secondary_item = None;
for (item, position) in &self.items {
match *position {
ToolbarItemLocation::Hidden => {}
ToolbarItemLocation::PrimaryLeft { flex } => {
let left_item = ChildView::new(item.as_ref())
.aligned()
.contained()
.with_margin_right(theme.item_spacing);
if let Some((flex, expanded)) = flex {
primary_left_items.push(left_item.flex(flex, expanded).boxed());
} else {
primary_left_items.push(left_item.boxed());
}
}
ToolbarItemLocation::PrimaryRight { flex } => {
let right_item = ChildView::new(item.as_ref())
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.flex_float();
if let Some((flex, expanded)) = flex {
primary_right_items.push(right_item.flex(flex, expanded).boxed());
} else {
primary_right_items.push(right_item.boxed());
}
}
ToolbarItemLocation::Secondary => {
secondary_item = Some(
ChildView::new(item.as_ref())
.constrained()
.with_height(theme.height)
.boxed(),
);
}
}
}
Flex::column()
.with_child(
Flex::row()
.with_children(primary_left_items)
.with_children(primary_right_items)
.constrained()
.with_height(theme.height)
.boxed(),
)
.with_children(secondary_item)
.contained()
.with_style(theme.container)
.boxed()
}
}
impl Toolbar {
pub fn new() -> Self {
Self {
active_pane_item: None,
items: Default::default(),
}
}
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + ToolbarItemView,
{
let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
cx.subscribe(&item, |this, item, event, cx| {
if let Some((_, current_location)) =
this.items.iter_mut().find(|(i, _)| i.id() == item.id())
{
let new_location = item
.read(cx)
.location_for_event(event, *current_location, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
}
})
.detach();
self.items.push((Box::new(item), location));
cx.notify();
}
pub fn set_active_pane_item(
&mut self,
pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
for (toolbar_item, current_location) in self.items.iter_mut() {
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
}
}
pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
self.items
.iter()
.find_map(|(item, _)| item.to_any().downcast())
}
}
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
fn id(&self) -> usize {
self.id()
}
fn to_any(&self) -> AnyViewHandle {
self.into()
}
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext,
) -> ToolbarItemLocation {
self.update(cx, |this, cx| {
this.set_active_pane_item(active_pane_item, cx)
})
}
}
impl Into<AnyViewHandle> for &dyn ToolbarItemViewHandle {
fn into(self) -> AnyViewHandle {
self.to_any()
}
}

View File

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

View File

@ -29,6 +29,7 @@ test-support = [
]
[dependencies]
breadcrumbs = { path = "../breadcrumbs" }
chat_panel = { path = "../chat_panel" }
collections = { path = "../collections" }
client = { path = "../client" }

View File

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

View File

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