From e7d4c385d50d48ad33d052b32420c577e9dad042 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 27 Jan 2022 17:52:36 +0100 Subject: [PATCH 01/35] Take an `Into` in `ChildView::new` Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/chat_panel/src/chat_panel.rs | 4 +- crates/diagnostics/src/diagnostics.rs | 2 +- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/gpui/src/presenter.rs | 10 ++-- crates/outline/src/outline.rs | 2 +- crates/theme_selector/src/theme_selector.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/pane_group.rs | 65 ++++++++++++--------- crates/workspace/src/sidebar.rs | 2 +- crates/workspace/src/status_bar.rs | 20 ++++--- crates/workspace/src/workspace.rs | 12 ++-- 12 files changed, 69 insertions(+), 56 deletions(-) diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 993a0cec0e..b155d9fc32 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -217,7 +217,7 @@ impl ChatPanel { let theme = &self.settings.borrow().theme; Flex::column() .with_child( - Container::new(ChildView::new(self.channel_select.id()).boxed()) + Container::new(ChildView::new(&self.channel_select).boxed()) .with_style(theme.chat_panel.channel_select.container) .boxed(), ) @@ -282,7 +282,7 @@ impl ChatPanel { fn render_input_box(&self) -> ElementBox { let theme = &self.settings.borrow().theme; - Container::new(ChildView::new(self.input_editor.id()).boxed()) + Container::new(ChildView::new(&self.input_editor).boxed()) .with_style(theme.chat_panel.input_editor.container) .boxed() } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d99713308a..ac197a7456 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -111,7 +111,7 @@ impl View for ProjectDiagnosticsEditor { .with_style(theme.container) .boxed() } else { - ChildView::new(self.editor.id()).boxed() + ChildView::new(&self.editor).boxed() } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3bf489df91..e950dc0fcb 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -76,7 +76,7 @@ impl View for FileFinder { Container::new( Flex::new(Axis::Vertical) .with_child( - Container::new(ChildView::new(self.query_editor.id()).boxed()) + Container::new(ChildView::new(&self.query_editor).boxed()) .with_style(settings.theme.selector.input_editor.container) .boxed(), ) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 68d7424ae4..5a87aa6bc1 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -181,7 +181,7 @@ impl View for GoToLine { Container::new( Flex::new(Axis::Vertical) .with_child( - Container::new(ChildView::new(self.line_editor.id()).boxed()) + Container::new(ChildView::new(&self.line_editor).boxed()) .with_style(theme.input_editor.container) .boxed(), ) diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 354f0a0f82..2666a329f0 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -6,8 +6,8 @@ use crate::{ json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, - Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel, - ReadView, Scene, View, ViewHandle, + Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, + ReadModel, ReadView, Scene, View, ViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -462,8 +462,10 @@ pub struct ChildView { } impl ChildView { - pub fn new(view_id: usize) -> Self { - Self { view_id } + pub fn new(view: impl Into) -> Self { + Self { + view_id: view.into().id(), + } } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 151e3ec0bb..b6921ce69f 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -80,7 +80,7 @@ impl View for OutlineView { Flex::new(Axis::Vertical) .with_child( - Container::new(ChildView::new(self.query_editor.id()).boxed()) + Container::new(ChildView::new(&self.query_editor).boxed()) .with_style(settings.theme.selector.input_editor.container) .boxed(), ) diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index f359bd85dd..5e536394e0 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -299,7 +299,7 @@ impl View for ThemeSelector { ConstrainedBox::new( Container::new( Flex::new(Axis::Vertical) - .with_child(ChildView::new(self.query_editor.id()).boxed()) + .with_child(ChildView::new(&self.query_editor).boxed()) .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed()) .boxed(), ) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 739d07aab1..deb19f47b5 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -516,7 +516,7 @@ impl View for Pane { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) - .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed()) + .with_child(ChildView::new(active_item).flexible(1., true).boxed()) .named("pane") } else { Empty::new().named("pane") diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index a2b3803b85..2b56a023fc 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,43 +1,45 @@ use anyhow::{anyhow, Result}; -use gpui::{elements::*, Axis}; +use gpui::{elements::*, Axis, ViewHandle}; use theme::Theme; +use crate::Pane; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { root: Member, } impl PaneGroup { - pub fn new(pane_id: usize) -> Self { + pub fn new(pane: ViewHandle) -> Self { Self { - root: Member::Pane(pane_id), + root: Member::Pane(pane), } } pub fn split( &mut self, - old_pane_id: usize, - new_pane_id: usize, + old_pane: &ViewHandle, + new_pane: &ViewHandle, direction: SplitDirection, ) -> Result<()> { match &mut self.root { - Member::Pane(pane_id) => { - if *pane_id == old_pane_id { - self.root = Member::new_axis(old_pane_id, new_pane_id, direction); + Member::Pane(pane) => { + if pane == old_pane { + self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); Ok(()) } else { Err(anyhow!("Pane not found")) } } - Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction), + Member::Axis(axis) => axis.split(old_pane, new_pane, direction), } } - pub fn remove(&mut self, pane_id: usize) -> Result { + pub fn remove(&mut self, pane: &ViewHandle) -> Result { match &mut self.root { Member::Pane(_) => Ok(false), Member::Axis(axis) => { - if let Some(last_pane) = axis.remove(pane_id)? { + if let Some(last_pane) = axis.remove(pane)? { self.root = last_pane; } Ok(true) @@ -53,11 +55,15 @@ impl PaneGroup { #[derive(Clone, Debug, Eq, PartialEq)] enum Member { Axis(PaneAxis), - Pane(usize), + Pane(ViewHandle), } impl Member { - fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self { + fn new_axis( + old_pane: ViewHandle, + new_pane: ViewHandle, + direction: SplitDirection, + ) -> Self { use Axis::*; use SplitDirection::*; @@ -67,16 +73,16 @@ impl Member { }; let members = match direction { - Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)], - Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)], + Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)], + Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)], }; Member::Axis(PaneAxis { axis, members }) } - pub fn render<'a>(&self, theme: &Theme) -> ElementBox { + pub fn render(&self, theme: &Theme) -> ElementBox { match self { - Member::Pane(view_id) => ChildView::new(*view_id).boxed(), + Member::Pane(pane) => ChildView::new(pane).boxed(), Member::Axis(axis) => axis.render(theme), } } @@ -91,8 +97,8 @@ struct PaneAxis { impl PaneAxis { fn split( &mut self, - old_pane_id: usize, - new_pane_id: usize, + old_pane: &ViewHandle, + new_pane: &ViewHandle, direction: SplitDirection, ) -> Result<()> { use SplitDirection::*; @@ -100,23 +106,24 @@ impl PaneAxis { for (idx, member) in self.members.iter_mut().enumerate() { match member { Member::Axis(axis) => { - if axis.split(old_pane_id, new_pane_id, direction).is_ok() { + if axis.split(old_pane, new_pane, direction).is_ok() { return Ok(()); } } - Member::Pane(pane_id) => { - if *pane_id == old_pane_id { + Member::Pane(pane) => { + if pane == old_pane { if direction.matches_axis(self.axis) { match direction { Up | Left => { - self.members.insert(idx, Member::Pane(new_pane_id)); + self.members.insert(idx, Member::Pane(new_pane.clone())); } Down | Right => { - self.members.insert(idx + 1, Member::Pane(new_pane_id)); + self.members.insert(idx + 1, Member::Pane(new_pane.clone())); } } } else { - *member = Member::new_axis(old_pane_id, new_pane_id, direction); + *member = + Member::new_axis(old_pane.clone(), new_pane.clone(), direction); } return Ok(()); } @@ -126,13 +133,13 @@ impl PaneAxis { Err(anyhow!("Pane not found")) } - fn remove(&mut self, pane_id_to_remove: usize) -> Result> { + fn remove(&mut self, pane_to_remove: &ViewHandle) -> Result> { let mut found_pane = false; let mut remove_member = None; for (idx, member) in self.members.iter_mut().enumerate() { match member { Member::Axis(axis) => { - if let Ok(last_pane) = axis.remove(pane_id_to_remove) { + if let Ok(last_pane) = axis.remove(pane_to_remove) { if let Some(last_pane) = last_pane { *member = last_pane; } @@ -140,8 +147,8 @@ impl PaneAxis { break; } } - Member::Pane(pane_id) => { - if *pane_id == pane_id_to_remove { + Member::Pane(pane) => { + if pane == pane_to_remove { found_pane = true; remove_member = Some(idx); break; diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 8d3d0f6370..7c0cb5c911 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -136,7 +136,7 @@ impl Sidebar { container.add_child( Hook::new( - ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) + ConstrainedBox::new(ChildView::new(active_item).boxed()) .with_max_width(*self.width.borrow()) .boxed(), ) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 162394ed00..2d26c33a8a 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,7 +1,7 @@ use crate::{ItemViewHandle, Pane, Settings}; use gpui::{ - elements::*, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, - ViewContext, ViewHandle, + elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, + View, ViewContext, ViewHandle, }; use postage::watch; @@ -14,7 +14,7 @@ pub trait StatusItemView: View { } trait StatusItemViewHandle { - fn id(&self) -> usize; + fn to_any(&self) -> AnyViewHandle; fn set_active_pane_item( &self, active_pane_item: Option<&dyn ItemViewHandle>, @@ -45,13 +45,13 @@ impl View for StatusBar { .with_children( self.left_items .iter() - .map(|i| ChildView::new(i.id()).aligned().boxed()), + .map(|i| ChildView::new(i.as_ref()).aligned().boxed()), ) .with_child(Empty::new().flexible(1., true).boxed()) .with_children( self.right_items .iter() - .map(|i| ChildView::new(i.id()).aligned().boxed()), + .map(|i| ChildView::new(i.as_ref()).aligned().boxed()), ) .contained() .with_style(theme.container) @@ -111,8 +111,8 @@ impl StatusBar { } impl StatusItemViewHandle for ViewHandle { - fn id(&self) -> usize { - self.id() + fn to_any(&self) -> AnyViewHandle { + self.into() } fn set_active_pane_item( @@ -125,3 +125,9 @@ impl StatusItemViewHandle for ViewHandle { }); } } + +impl Into for &dyn StatusItemViewHandle { + fn into(self) -> AnyViewHandle { + self.to_any() + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 48dcb4907b..86d5271224 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -599,7 +599,7 @@ impl Workspace { Workspace { modal: None, weak_self: cx.weak_handle(), - center: PaneGroup::new(pane.id()), + center: PaneGroup::new(pane.clone()), panes: vec![pane.clone()], active_pane: pane.clone(), status_bar, @@ -1048,15 +1048,13 @@ impl Workspace { new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } - self.center - .split(pane.id(), new_pane.id(), direction) - .unwrap(); + self.center.split(&pane, &new_pane, direction).unwrap(); cx.notify(); new_pane } fn remove_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { - if self.center.remove(pane.id()).unwrap() { + if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); } @@ -1287,7 +1285,7 @@ impl View for Workspace { Flexible::new(1., true, self.center.render(&settings.theme)) .boxed(), ) - .with_child(ChildView::new(self.status_bar.id()).boxed()) + .with_child(ChildView::new(&self.status_bar).boxed()) .flexible(1., true) .boxed(), ); @@ -1298,7 +1296,7 @@ impl View for Workspace { content.add_child(self.right_sidebar.render(&settings, cx)); content.boxed() }) - .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) + .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed())) .flexible(1.0, true) .boxed(), ) From bebde782fa33bdb0c450ea39f07dc3d300adf23f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 27 Jan 2022 17:39:58 +0100 Subject: [PATCH 02/35] Deploy `FindBar` when hitting `cmd-f` Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- Cargo.lock | 10 ++++++++ crates/find/Cargo.toml | 12 ++++++++++ crates/find/src/find.rs | 45 ++++++++++++++++++++++++++++++++++++ crates/workspace/src/pane.rs | 32 +++++++++++++++++++++++-- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 crates/find/Cargo.toml create mode 100644 crates/find/src/find.rs diff --git a/Cargo.lock b/Cargo.lock index 9417057bba..fea2808632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1719,6 +1719,15 @@ dependencies = [ "workspace", ] +[[package]] +name = "find" +version = "0.1.0" +dependencies = [ + "editor", + "gpui", + "workspace", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -5725,6 +5734,7 @@ dependencies = [ "editor", "env_logger", "file_finder", + "find", "fsevent", "futures", "fuzzy", diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml new file mode 100644 index 0000000000..94ac64fcf7 --- /dev/null +++ b/crates/find/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "find" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/find.rs" + +[dependencies] +editor = { path = "../editor" } +gpui = { path = "../gpui" } +workspace = { path = "../workspace" } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs new file mode 100644 index 0000000000..b8949703ff --- /dev/null +++ b/crates/find/src/find.rs @@ -0,0 +1,45 @@ +use gpui::{ + action, color::Color, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, + View, ViewContext, +}; +use workspace::Workspace; + +action!(Deploy); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([Binding::new( + "cmd-f", + Deploy, + Some("Editor && mode == full"), + )]); + cx.add_action(FindBar::deploy); +} + +struct FindBar; + +impl Entity for FindBar { + type Event = (); +} + +impl View for FindBar { + fn ui_name() -> &'static str { + "FindBar" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new() + .contained() + .with_background_color(Color::red()) + .constrained() + .with_height(30.) + .boxed() + } +} + +impl FindBar { + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + workspace + .active_pane() + .update(cx, |pane, cx| pane.show_toolbar(cx, |_| FindBar)); + } +} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index deb19f47b5..95fb7c863b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -7,11 +7,17 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle, - Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle, + AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, + ViewHandle, }; use postage::watch; use project::ProjectPath; -use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + cmp, mem, + rc::Rc, +}; use util::ResultExt; action!(Split, SplitDirection); @@ -75,6 +81,8 @@ pub struct Pane { active_item_index: usize, settings: watch::Receiver, nav_history: Rc>, + toolbars: HashMap, + active_toolbar: Option, } // #[derive(Debug, Eq, PartialEq)] @@ -120,6 +128,8 @@ impl Pane { active_item_index: 0, settings, nav_history: Default::default(), + toolbars: Default::default(), + active_toolbar: Default::default(), } } @@ -365,6 +375,19 @@ impl Pane { cx.emit(Event::Split(direction)); } + pub fn show_toolbar(&mut self, cx: &mut ViewContext, build_toolbar: F) + where + F: FnOnce(&mut ViewContext) -> V, + V: View, + { + let handle = self + .toolbars + .entry(TypeId::of::()) + .or_insert_with(|| cx.add_view(build_toolbar).into()); + self.active_toolbar = Some(handle.clone()); + cx.notify(); + } + fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; @@ -516,6 +539,11 @@ impl View for Pane { 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()) .named("pane") } else { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 21ad632bd2..5ab0262288 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -36,6 +36,7 @@ contacts_panel = { path = "../contacts_panel" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } +find = { path = "../find" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { path = "../go_to_line" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 59804cf87c..dd65825555 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -62,6 +62,7 @@ fn main() { outline::init(cx); project_panel::init(cx); diagnostics::init(cx); + find::init(cx); cx.spawn({ let client = client.clone(); |cx| async move { From 05e20ca72b0992398b08f5652f5dfdcf553613f6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jan 2022 12:58:21 -0800 Subject: [PATCH 03/35] Inform toolbars when active item changes Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/find/Cargo.toml | 1 + crates/find/src/find.rs | 71 ++++++++++++++++++++++---- crates/workspace/src/pane.rs | 98 +++++++++++++++++++++++++++++------- 4 files changed, 144 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fea2808632..45fe7ad8e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1725,6 +1725,7 @@ version = "0.1.0" dependencies = [ "editor", "gpui", + "postage", "workspace", ] diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 94ac64fcf7..08805a67d3 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -10,3 +10,4 @@ path = "src/find.rs" editor = { path = "../editor" } gpui = { path = "../gpui" } workspace = { path = "../workspace" } +postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index b8949703ff..6c43725a4a 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,8 +1,11 @@ +use editor::{Editor, EditorSettings}; use gpui::{ - action, color::Color, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, - View, ViewContext, + action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, View, + ViewContext, ViewHandle, }; -use workspace::Workspace; +use postage::watch; +use std::sync::Arc; +use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); @@ -15,7 +18,11 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(FindBar::deploy); } -struct FindBar; +struct FindBar { + settings: watch::Receiver, + query_editor: ViewHandle, + active_editor: Option>, +} impl Entity for FindBar { type Event = (); @@ -27,19 +34,65 @@ impl View for FindBar { } fn render(&mut self, _: &mut RenderContext) -> ElementBox { - Empty::new() + ChildView::new(&self.query_editor) .contained() - .with_background_color(Color::red()) - .constrained() - .with_height(30.) + .with_style(self.settings.borrow().theme.selector.input_editor.container) .boxed() } } +impl Toolbar for FindBar { + fn active_item_changed( + &mut self, + item: Option>, + cx: &mut ViewContext, + ) -> bool { + self.active_editor = item.and_then(|item| item.act_as::(cx)); + self.active_editor.is_some() + } +} + impl FindBar { + fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::single_line( + { + let settings = settings.clone(); + Arc::new(move |_| { + let settings = settings.borrow(); + EditorSettings { + style: settings.theme.selector.input_editor.as_editor(), + tab_size: settings.tab_size, + soft_wrap: editor::SoftWrap::None, + } + }) + }, + cx, + ) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + + Self { + query_editor, + active_editor: None, + settings, + } + } + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let settings = workspace.settings(); + workspace.active_pane().update(cx, |pane, cx| { + pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + if let Some(toolbar) = pane.active_toolbar() { + cx.focus(toolbar); + } + }); + } + + fn cancel(workspace: &mut Workspace, _: &Cancel, cx: &mut ViewContext) { workspace .active_pane() - .update(cx, |pane, cx| pane.show_toolbar(cx, |_| FindBar)); + .update(cx, |pane, cx| pane.hide_toolbar(cx)); } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 95fb7c863b..5cbd9fe559 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -81,14 +81,27 @@ pub struct Pane { active_item_index: usize, settings: watch::Receiver, nav_history: Rc>, - toolbars: HashMap, - active_toolbar: Option, + toolbars: HashMap>, + active_toolbar_type: Option, + active_toolbar_visible: bool, } -// #[derive(Debug, Eq, PartialEq)] -// pub struct State { -// pub tabs: Vec, -// } +pub trait Toolbar: View { + fn active_item_changed( + &mut self, + item: Option>, + cx: &mut ViewContext, + ) -> bool; +} + +trait ToolbarHandle { + fn active_item_changed( + &self, + item: Option>, + cx: &mut MutableAppContext, + ) -> bool; + fn to_any(&self) -> AnyViewHandle; +} pub struct ItemNavHistory { history: Rc>, @@ -129,7 +142,8 @@ impl Pane { settings, nav_history: Default::default(), toolbars: Default::default(), - active_toolbar: Default::default(), + active_toolbar_type: Default::default(), + active_toolbar_visible: false, } } @@ -301,6 +315,7 @@ impl Pane { if prev_active_item_ix != self.active_item_index { self.item_views[prev_active_item_ix].1.deactivated(cx); } + self.update_active_toolbar(cx); self.focus_active_item(cx); cx.notify(); } @@ -354,15 +369,18 @@ impl Pane { true } }); - self.active_item_index = cmp::min( - self.active_item_index, - self.item_views.len().saturating_sub(1), + self.activate_item( + cmp::min( + self.active_item_index, + self.item_views.len().saturating_sub(1), + ), + cx, ); if self.item_views.is_empty() { + self.update_active_toolbar(cx); cx.emit(Event::Remove); } - cx.notify(); } fn focus_active_item(&mut self, cx: &mut ViewContext) { @@ -378,16 +396,46 @@ impl Pane { pub fn show_toolbar(&mut self, cx: &mut ViewContext, build_toolbar: F) where F: FnOnce(&mut ViewContext) -> V, - V: View, + V: Toolbar, { - let handle = self - .toolbars - .entry(TypeId::of::()) - .or_insert_with(|| cx.add_view(build_toolbar).into()); - self.active_toolbar = Some(handle.clone()); + let type_id = TypeId::of::(); + 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 hide_toolbar(&mut self, cx: &mut ViewContext) { + self.active_toolbar_type = None; + self.active_toolbar_visible = false; + self.focus_active_item(cx); + cx.notify(); + } + + pub fn active_toolbar(&self) -> Option { + let type_id = self.active_toolbar_type?; + let toolbar = self.toolbars.get(&type_id)?; + if self.active_toolbar_visible { + Some(toolbar.to_any()) + } else { + None + } + } + + fn update_active_toolbar(&mut self, cx: &mut ViewContext) { + if let Some(type_id) = self.active_toolbar_type { + if let Some(toolbar) = self.toolbars.get(&type_id) { + self.active_toolbar_visible = toolbar.active_item_changed( + Some(self.item_views[self.active_item_index].1.clone()), + cx, + ); + } + } + } + fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; @@ -540,7 +588,7 @@ impl View for Pane { Flex::column() .with_child(self.render_tabs(cx)) .with_children( - self.active_toolbar + self.active_toolbar() .as_ref() .map(|view| ChildView::new(view).boxed()), ) @@ -556,6 +604,20 @@ impl View for Pane { } } +impl ToolbarHandle for ViewHandle { + fn active_item_changed( + &self, + item: Option>, + cx: &mut MutableAppContext, + ) -> bool { + self.update(cx, |this, cx| this.active_item_changed(item, cx)) + } + + fn to_any(&self) -> AnyViewHandle { + self.into() + } +} + impl ItemNavHistory { pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { Self { From d8e4464a89ef2f28f363ce792abbd670fbdd56f5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jan 2022 13:00:51 -0800 Subject: [PATCH 04/35] WIP - Run substring search when typing in find bar Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/find/Cargo.toml | 1 + crates/find/src/find.rs | 44 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45fe7ad8e0..49533117c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1723,6 +1723,7 @@ dependencies = [ name = "find" version = "0.1.0" dependencies = [ + "aho-corasick", "editor", "gpui", "postage", diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 08805a67d3..16a9b1f827 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" path = "src/find.rs" [dependencies] +aho-corasick = "0.7" editor = { path = "../editor" } gpui = { path = "../gpui" } workspace = { path = "../workspace" } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 6c43725a4a..929e9ce67b 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,3 +1,4 @@ +use aho_corasick::AhoCorasick; use editor::{Editor, EditorSettings}; use gpui::{ action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, View, @@ -8,14 +9,15 @@ use std::sync::Arc; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); +action!(Cancel); pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([Binding::new( - "cmd-f", - Deploy, - Some("Editor && mode == full"), - )]); + cx.add_bindings([ + Binding::new("cmd-f", Deploy, Some("Editor && mode == full")), + Binding::new("escape", Cancel, Some("FindBar")), + ]); cx.add_action(FindBar::deploy); + cx.add_action(FindBar::cancel); } struct FindBar { @@ -33,6 +35,10 @@ impl View for FindBar { "FindBar" } + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); + } + fn render(&mut self, _: &mut RenderContext) -> ElementBox { ChildView::new(&self.query_editor) .contained() @@ -95,4 +101,32 @@ impl FindBar { .active_pane() .update(cx, |pane, cx| pane.hide_toolbar(cx)); } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + _: &editor::Event, + cx: &mut ViewContext, + ) { + if let Some(editor) = &self.active_editor { + let search = self.query_editor.read(cx).text(cx); + if search.is_empty() { + return; + } + let search = AhoCorasick::new_auto_configured(&[search]); + editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut ranges = search + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) + .map(|mat| { + let mat = mat.unwrap(); + mat.start()..mat.end() + }) + .peekable(); + if ranges.peek().is_some() { + editor.select_ranges(ranges, None, cx); + } + }); + } + } } From 34ed73474971a43b1a6fdd65a7914395740bae3b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jan 2022 15:19:28 -0800 Subject: [PATCH 05/35] Add highlighted_ranges API to editor Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 137 ++++++++++++++++++++++- crates/editor/src/element.rs | 207 +++++++++++++++++++++++------------ 2 files changed, 273 insertions(+), 71 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 27a8c79ba3..c57bbf0aed 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,12 +9,13 @@ mod test; use aho_corasick::AhoCorasick; use clock::ReplicaId; -use collections::{HashMap, HashSet}; +use collections::{BTreeMap, HashMap, HashSet}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ action, + color::Color, elements::*, fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, @@ -37,7 +38,8 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ - cmp, + any::TypeId, + cmp::{self, Ordering}, iter::{self, FromIterator}, mem, ops::{Deref, Range, RangeInclusive, Sub}, @@ -382,6 +384,7 @@ pub struct Editor { vertical_scroll_margin: f32, placeholder_text: Option>, highlighted_rows: Option>, + highlighted_ranges: BTreeMap>)>, nav_history: Option, } @@ -522,6 +525,7 @@ impl Editor { vertical_scroll_margin: 3.0, placeholder_text: None, highlighted_rows: None, + highlighted_ranges: Default::default(), nav_history: None, }; let selection = Selection { @@ -3721,6 +3725,58 @@ impl Editor { self.highlighted_rows.clone() } + pub fn highlight_ranges( + &mut self, + ranges: Vec>, + color: Color, + cx: &mut ViewContext, + ) { + self.highlighted_ranges + .insert(TypeId::of::(), (color, ranges)); + cx.notify(); + } + + pub fn clear_highlighted_ranges(&mut self, cx: &mut ViewContext) { + self.highlighted_ranges.remove(&TypeId::of::()); + cx.notify(); + } + + pub fn highlighted_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Color, Range)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (color, ranges) in self.highlighted_ranges.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((*color, start..end)) + } + } + results + } + fn next_blink_epoch(&mut self) -> usize { self.blink_epoch += 1; self.blink_epoch @@ -6555,6 +6611,83 @@ mod tests { }); } + #[gpui::test] + fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + let settings = EditorSettings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = buffer.read(cx).snapshot(cx); + + let anchor_range = |range: Range| { + buffer.anchor_after(range.start)..buffer.anchor_after(range.end) + }; + + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + Color::red(), + cx, + ); + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + Color::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + ), + &[ + ( + Color::red(), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + ), + ( + Color::red(), + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + ), + ( + Color::green(), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5) + ), + ( + Color::green(), + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6) + ), + ] + ); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + ), + &[( + Color::red(), + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + ),] + ); + }); + } + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f1af1b4ce8..1b1ae46491 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -312,64 +312,45 @@ impl EditorElement { let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen let max_glyph_width = layout.em_width; let scroll_left = scroll_position.x() * max_glyph_width; + let content_origin = bounds.origin() + layout.text_offset; cx.scene.push_layer(Some(bounds)); - // Draw selections - let corner_radius = 2.5; + for (color, range) in &layout.highlighted_ranges { + self.paint_highlighted_range( + range.clone(), + start_row, + end_row, + *color, + 0., + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + } + let mut cursors = SmallVec::<[Cursor; 32]>::new(); - - let content_origin = bounds.origin() + layout.text_offset; - for (replica_id, selections) in &layout.selections { let style = style.replica_selection_style(*replica_id); + let corner_radius = 0.15 * layout.line_height; for selection in selections { - if selection.start != selection.end { - let row_range = if selection.end.column() == 0 { - cmp::max(selection.start.row(), start_row) - ..cmp::min(selection.end.row(), end_row) - } else { - cmp::max(selection.start.row(), start_row) - ..cmp::min(selection.end.row() + 1, end_row) - }; - - let selection = Selection { - color: style.selection, - line_height: layout.line_height, - start_y: content_origin.y() + row_range.start as f32 * layout.line_height - - scroll_top, - lines: row_range - .into_iter() - .map(|row| { - let line_layout = &layout.line_layouts[(row - start_row) as usize]; - SelectionLine { - start_x: if row == selection.start.row() { - content_origin.x() - + line_layout - .x_for_index(selection.start.column() as usize) - - scroll_left - } else { - content_origin.x() - scroll_left - }, - end_x: if row == selection.end.row() { - content_origin.x() - + line_layout - .x_for_index(selection.end.column() as usize) - - scroll_left - } else { - content_origin.x() - + line_layout.width() - + corner_radius * 2.0 - - scroll_left - }, - } - }) - .collect(), - }; - - selection.paint(bounds, cx.scene); - } + self.paint_highlighted_range( + selection.start..selection.end, + start_row, + end_row, + style.selection, + corner_radius, + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); if view.show_local_cursors() || *replica_id != local_replica_id { let cursor_position = selection.head(); @@ -412,6 +393,62 @@ impl EditorElement { cx.scene.pop_layer(); } + fn paint_highlighted_range( + &self, + range: Range, + start_row: u32, + end_row: u32, + color: Color, + corner_radius: f32, + layout: &LayoutState, + content_origin: Vector2F, + scroll_top: f32, + scroll_left: f32, + bounds: RectF, + cx: &mut PaintContext, + ) { + if range.start != range.end { + let row_range = if range.end.column() == 0 { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) + } else { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) + }; + + let selection = HighlightedRange { + color, + line_height: layout.line_height, + corner_radius, + start_y: content_origin.y() + row_range.start as f32 * layout.line_height + - scroll_top, + lines: row_range + .into_iter() + .map(|row| { + let line_layout = &layout.line_layouts[(row - start_row) as usize]; + HighlightedRangeLine { + start_x: if row == range.start.row() { + content_origin.x() + + line_layout.x_for_index(range.start.column() as usize) + - scroll_left + } else { + content_origin.x() - scroll_left + }, + end_x: if row == range.end.row() { + content_origin.x() + + line_layout.x_for_index(range.end.column() as usize) + - scroll_left + } else { + content_origin.x() + line_layout.width() + corner_radius * 2.0 + - scroll_left + }, + } + }) + .collect(), + }; + + selection.paint(bounds, cx.scene); + } + } + fn paint_blocks( &mut self, bounds: RectF, @@ -715,10 +752,16 @@ impl Element for EditorElement { let mut selections = HashMap::default(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; + let mut highlighted_ranges = Vec::new(); self.update_view(cx.app, |view, cx| { - highlighted_rows = view.highlighted_rows(); let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx)); + highlighted_rows = view.highlighted_rows(); + highlighted_ranges = view.highlighted_ranges_in_range( + start_anchor.clone()..end_anchor.clone(), + &display_map, + ); + let local_selections = view .local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map); for selection in &local_selections { @@ -837,6 +880,7 @@ impl Element for EditorElement { snapshot, active_rows, highlighted_rows, + highlighted_ranges, line_layouts, line_number_layouts, blocks, @@ -950,6 +994,7 @@ pub struct LayoutState { line_height: f32, em_width: f32, em_advance: f32, + highlighted_ranges: Vec<(Color, Range)>, selections: HashMap>>, text_offset: Vector2F, } @@ -1036,20 +1081,21 @@ impl Cursor { } #[derive(Debug)] -struct Selection { +struct HighlightedRange { start_y: f32, line_height: f32, - lines: Vec, + lines: Vec, color: Color, + corner_radius: f32, } #[derive(Debug)] -struct SelectionLine { +struct HighlightedRangeLine { start_x: f32, end_x: f32, } -impl Selection { +impl HighlightedRange { fn paint(&self, bounds: RectF, scene: &mut Scene) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene); @@ -1064,26 +1110,31 @@ impl Selection { } } - fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], bounds: RectF, scene: &mut Scene) { + fn paint_lines( + &self, + start_y: f32, + lines: &[HighlightedRangeLine], + bounds: RectF, + scene: &mut Scene, + ) { if lines.is_empty() { return; } let mut path = PathBuilder::new(); - let corner_radius = 0.15 * self.line_height; let first_line = lines.first().unwrap(); let last_line = lines.last().unwrap(); let first_top_left = vec2f(first_line.start_x, start_y); let first_top_right = vec2f(first_line.end_x, start_y); - let curve_height = vec2f(0., corner_radius); + let curve_height = vec2f(0., self.corner_radius); let curve_width = |start_x: f32, end_x: f32| { let max = (end_x - start_x) / 2.; - let width = if max < corner_radius { + let width = if max < self.corner_radius { max } else { - corner_radius + self.corner_radius }; vec2f(width, 0.) @@ -1107,26 +1158,38 @@ impl Selection { Ordering::Less => { let curve_width = curve_width(next_top_right.x(), bottom_right.x()); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right - curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } path.line_to(next_top_right + curve_width); - path.curve_to(next_top_right + curve_height, next_top_right); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } } Ordering::Greater => { let curve_width = curve_width(bottom_right.x(), next_top_right.x()); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right + curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right + curve_width, bottom_right); + } path.line_to(next_top_right - curve_width); - path.curve_to(next_top_right + curve_height, next_top_right); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } } } } else { let curve_width = curve_width(line.start_x, line.end_x); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right - curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } let bottom_left = vec2f(line.start_x, bottom_right.y()); path.line_to(bottom_left + curve_width); - path.curve_to(bottom_left - curve_height, bottom_left); + if self.corner_radius > 0. { + path.curve_to(bottom_left - curve_height, bottom_left); + } } } @@ -1134,14 +1197,20 @@ impl Selection { let curve_width = curve_width(last_line.start_x, first_line.start_x); let second_top_left = vec2f(last_line.start_x, start_y + self.line_height); path.line_to(second_top_left + curve_height); - path.curve_to(second_top_left + curve_width, second_top_left); + if self.corner_radius > 0. { + path.curve_to(second_top_left + curve_width, second_top_left); + } let first_bottom_left = vec2f(first_line.start_x, second_top_left.y()); path.line_to(first_bottom_left - curve_width); - path.curve_to(first_bottom_left - curve_height, first_bottom_left); + if self.corner_radius > 0. { + path.curve_to(first_bottom_left - curve_height, first_bottom_left); + } } path.line_to(first_top_left + curve_height); - path.curve_to(first_top_left + top_curve_width, first_top_left); + if self.corner_radius > 0. { + path.curve_to(first_top_left + top_curve_width, first_top_left); + } path.line_to(first_top_right - top_curve_width); scene.push_path(path.build(self.color, Some(bounds))); From 3abd7bc8dddc740645a28150569da13b82c330a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jan 2022 15:19:52 -0800 Subject: [PATCH 06/35] Highlight find matches Co-Authored-By: Nathan Sobo --- crates/find/src/find.rs | 21 +++++++++++---------- crates/theme/src/theme.rs | 6 ++++++ crates/zed/assets/themes/_base.toml | 5 ++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 929e9ce67b..19b105ccf8 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -110,22 +110,23 @@ impl FindBar { ) { if let Some(editor) = &self.active_editor { let search = self.query_editor.read(cx).text(cx); - if search.is_empty() { - return; - } - let search = AhoCorasick::new_auto_configured(&[search]); + let theme = &self.settings.borrow().theme.find; editor.update(cx, |editor, cx| { + if search.is_empty() { + editor.clear_highlighted_ranges::(cx); + return; + } + + let search = AhoCorasick::new_auto_configured(&[search]); let buffer = editor.buffer().read(cx).snapshot(cx); - let mut ranges = search + let ranges = search .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) .map(|mat| { let mat = mat.unwrap(); - mat.start()..mat.end() + buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()) }) - .peekable(); - if ranges.peek().is_some() { - editor.select_ranges(ranges, None, cx); - } + .collect(); + editor.highlight_ranges::(ranges, theme.match_background, cx); }); } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c3d37b950c..9452204617 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -24,6 +24,7 @@ pub struct Theme { pub project_panel: ProjectPanel, pub selector: Selector, pub editor: EditorStyle, + pub find: Find, pub project_diagnostics: ProjectDiagnostics, } @@ -87,6 +88,11 @@ pub struct Tab { pub icon_conflict: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct Find { + pub match_background: Color, +} + #[derive(Deserialize, Default)] pub struct Sidebar { #[serde(flatten)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index b818b514a7..e92715793c 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -185,7 +185,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -318,3 +318,6 @@ status_bar_item = { extends = "$text.2", margin.right = 10 } tab_icon_width = 13 tab_icon_spacing = 4 tab_summary_spacing = 10 + +[find] +match_background = "$state.highlighted_line" From 4f0ffdcdaf39409c4dbcb4d39fd7f05c85f818a0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jan 2022 15:38:10 -0800 Subject: [PATCH 07/35] Avoid panic when closing the last tab in a pane, due to calling `activate_item` --- crates/workspace/src/pane.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5cbd9fe559..a112213795 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -312,7 +312,9 @@ impl Pane { pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { if index < self.item_views.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); - if prev_active_item_ix != self.active_item_index { + if prev_active_item_ix != self.active_item_index + && prev_active_item_ix < self.item_views.len() + { self.item_views[prev_active_item_ix].1.deactivated(cx); } self.update_active_toolbar(cx); From da35df0ccaee11cba501b0055e83abe25d7aab40 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Jan 2022 16:16:51 -0800 Subject: [PATCH 08/35] WIP --- Cargo.lock | 1 + crates/find/Cargo.toml | 1 + crates/find/src/find.rs | 88 +++++++++++++++++++++++++++-- crates/theme/src/theme.rs | 6 ++ crates/zed/assets/themes/_base.toml | 28 +++++++++ 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49533117c1..cce55da25d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,6 +1727,7 @@ dependencies = [ "editor", "gpui", "postage", + "theme", "workspace", ] diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 16a9b1f827..2984f013ec 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -10,5 +10,6 @@ path = "src/find.rs" aho-corasick = "0.7" editor = { path = "../editor" } gpui = { path = "../gpui" } +theme = { path = "../theme" } workspace = { path = "../workspace" } postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 19b105ccf8..41dcbe38b2 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,4 +1,4 @@ -use aho_corasick::AhoCorasick; +use aho_corasick::AhoCorasickBuilder; use editor::{Editor, EditorSettings}; use gpui::{ action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, View, @@ -10,6 +10,14 @@ use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); action!(Cancel); +action!(ToggleMode, SearchMode); + +#[derive(Clone, Copy)] +pub enum SearchMode { + WholeWord, + CaseSensitive, + Regex, +} pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ @@ -18,12 +26,16 @@ pub fn init(cx: &mut MutableAppContext) { ]); cx.add_action(FindBar::deploy); cx.add_action(FindBar::cancel); + cx.add_action(FindBar::toggle_mode); } struct FindBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, + case_sensitive_mode: bool, + whole_word_mode: bool, + regex_mode: bool, } impl Entity for FindBar { @@ -39,10 +51,25 @@ impl View for FindBar { cx.focus(&self.query_editor); } - fn render(&mut self, _: &mut RenderContext) -> ElementBox { - ChildView::new(&self.query_editor) + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + Flex::column() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(theme.query.container) + .boxed(), + ) + .with_child( + Flex::column() + .with_child(self.render_mode_button("Aa", SearchMode::CaseSensitive, theme, cx)) + .with_child(self.render_mode_button("|ab|", SearchMode::WholeWord, theme, cx)) + .with_child(self.render_mode_button(".*", SearchMode::Regex, theme, cx)) + .contained() + .with_style(theme.mode_button_group) + .boxed(), + ) .contained() - .with_style(self.settings.borrow().theme.selector.input_editor.container) .boxed() } } @@ -67,7 +94,7 @@ impl FindBar { Arc::new(move |_| { let settings = settings.borrow(); EditorSettings { - style: settings.theme.selector.input_editor.as_editor(), + style: settings.theme.find.query.as_editor(), tab_size: settings.tab_size, soft_wrap: editor::SoftWrap::None, } @@ -82,10 +109,37 @@ impl FindBar { Self { query_editor, active_editor: None, + case_sensitive_mode: false, + whole_word_mode: false, + regex_mode: false, settings, } } + fn render_mode_button( + &self, + icon: &str, + mode: SearchMode, + theme: &theme::Find, + cx: &mut RenderContext, + ) -> ElementBox { + let is_active = self.is_mode_enabled(mode); + MouseEventHandler::new::(0, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.mode_button, + (false, true) => &theme.hovered_mode_button, + (true, false) => &theme.active_mode_button, + (true, true) => &theme.active_hovered_mode_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .boxed() + } + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { @@ -102,6 +156,25 @@ impl FindBar { .update(cx, |pane, cx| pane.hide_toolbar(cx)); } + fn is_mode_enabled(&self, mode: SearchMode) -> bool { + match mode { + SearchMode::WholeWord => self.whole_word_mode, + SearchMode::CaseSensitive => self.case_sensitive_mode, + SearchMode::Regex => self.regex_mode, + } + } + + fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { + eprintln!("TOGGLE MODE"); + let value = match mode { + SearchMode::WholeWord => &mut self.whole_word_mode, + SearchMode::CaseSensitive => &mut self.case_sensitive_mode, + SearchMode::Regex => &mut self.regex_mode, + }; + *value = !*value; + cx.notify(); + } + fn on_query_editor_event( &mut self, _: ViewHandle, @@ -117,7 +190,10 @@ impl FindBar { return; } - let search = AhoCorasick::new_auto_configured(&[search]); + let search = AhoCorasickBuilder::new() + .auto_configure(&[&search]) + .ascii_case_insensitive(!self.case_sensitive_mode) + .build(&[&search]); let buffer = editor.buffer().read(cx).snapshot(cx); let ranges = search .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9452204617..0ea5ebee40 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -90,6 +90,12 @@ pub struct Tab { #[derive(Clone, Deserialize, Default)] pub struct Find { + pub query: InputEditorStyle, + pub mode_button_group: ContainerStyle, + pub mode_button: ContainedText, + pub active_mode_button: ContainedText, + pub hovered_mode_button: ContainedText, + pub active_hovered_mode_button: ContainedText, pub match_background: Color, } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index e92715793c..0f8326e838 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -321,3 +321,31 @@ tab_summary_spacing = 10 [find] match_background = "$state.highlighted_line" + +[find.mode_button] +extends = "$text.1" + +[find.mode_button_group] +corner_radius = 6 +border = { width = 1, color = "$border.0" } + +[find.active_mode_button] +extends = "$find.mode_button" +background = "$surface.2" + +[find.hovered_mode_button] +extends = "$find.mode_button" +background = "$surface.2" + +[find.active_hovered_mode_button] +extends = "$find.mode_button" +background = "$surface.2" + +[find.query] +background = "$surface.1" +corner_radius = 6 +padding = { left = 16, right = 16, top = 7, bottom = 7 } +text = "$text.0" +placeholder_text = "$text.2" +selection = "$selection.host" +border = { width = 1, color = "$border.0" } From df1810a3b001d46503733014d9c509e7e5b53007 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 27 Jan 2022 18:59:44 -0700 Subject: [PATCH 09/35] A bit more progress styling find Fix the pinwheel when hovering mode buttons. --- crates/find/src/find.rs | 13 ++++++++----- crates/theme/src/theme.rs | 11 ++++++++++- crates/zed/assets/themes/_base.toml | 7 +++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 41dcbe38b2..2b862fc075 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -53,15 +53,17 @@ impl View for FindBar { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme.find; - Flex::column() + Flex::row() .with_child( ChildView::new(&self.query_editor) .contained() - .with_style(theme.query.container) + .with_style(theme.editor.input.container) + .constrained() + .with_max_width(theme.editor.max_width) .boxed(), ) .with_child( - Flex::column() + Flex::row() .with_child(self.render_mode_button("Aa", SearchMode::CaseSensitive, theme, cx)) .with_child(self.render_mode_button("|ab|", SearchMode::WholeWord, theme, cx)) .with_child(self.render_mode_button(".*", SearchMode::Regex, theme, cx)) @@ -70,6 +72,7 @@ impl View for FindBar { .boxed(), ) .contained() + .with_style(theme.container) .boxed() } } @@ -94,7 +97,7 @@ impl FindBar { Arc::new(move |_| { let settings = settings.borrow(); EditorSettings { - style: settings.theme.find.query.as_editor(), + style: settings.theme.find.editor.input.as_editor(), tab_size: settings.tab_size, soft_wrap: editor::SoftWrap::None, } @@ -124,7 +127,7 @@ impl FindBar { cx: &mut RenderContext, ) -> ElementBox { let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(0, cx, |state, _| { + MouseEventHandler::new::(mode as usize, cx, |state, _| { let style = match (is_active, state.hovered) { (false, false) => &theme.mode_button, (false, true) => &theme.hovered_mode_button, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 0ea5ebee40..e4274653e8 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -90,7 +90,9 @@ pub struct Tab { #[derive(Clone, Deserialize, Default)] pub struct Find { - pub query: InputEditorStyle, + #[serde(flatten)] + pub container: ContainerStyle, + pub editor: FindEditor, pub mode_button_group: ContainerStyle, pub mode_button: ContainedText, pub active_mode_button: ContainedText, @@ -99,6 +101,13 @@ pub struct Find { pub match_background: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct FindEditor { + #[serde(flatten)] + pub input: InputEditorStyle, + pub max_width: f32, +} + #[derive(Deserialize, Default)] pub struct Sidebar { #[serde(flatten)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 0f8326e838..b52e80e400 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -321,6 +321,7 @@ tab_summary_spacing = 10 [find] match_background = "$state.highlighted_line" +background = "$surface.1" [find.mode_button] extends = "$text.1" @@ -341,10 +342,12 @@ background = "$surface.2" extends = "$find.mode_button" background = "$surface.2" -[find.query] -background = "$surface.1" +[find.editor] +max_width = 400 +background = "$surface.0" corner_radius = 6 padding = { left = 16, right = 16, top = 7, bottom = 7 } +margin = { top = 5, bottom = 5, left = 5, right = 5 } text = "$text.0" placeholder_text = "$text.2" selection = "$selection.host" From b980b1105348c7ff827337f52e90c3446e6eca72 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 11:23:14 +0100 Subject: [PATCH 10/35] Implement whole word mode --- crates/editor/src/editor.rs | 20 ++++++++++++++++++++ crates/editor/src/movement.rs | 22 +--------------------- crates/find/src/find.rs | 25 ++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c57bbf0aed..dc662969dc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -438,6 +438,14 @@ pub struct NavigationData { offset: usize, } +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub enum CharKind { + Newline, + Punctuation, + Whitespace, + Word, +} + impl Editor { pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext) -> Self { let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); @@ -4215,6 +4223,18 @@ pub fn settings_builder( }) } +pub fn char_kind(c: char) -> CharKind { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 466b6e9323..9a800f9abb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,5 +1,5 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::ToPoint; +use crate::{char_kind, CharKind, ToPoint}; use anyhow::Result; use std::{cmp, ops::Range}; @@ -215,26 +215,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { - CharKind::Whitespace - } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 2b862fc075..cea47a4a77 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,5 +1,5 @@ use aho_corasick::AhoCorasickBuilder; -use editor::{Editor, EditorSettings}; +use editor::{char_kind, Editor, EditorSettings}; use gpui::{ action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, @@ -200,9 +200,28 @@ impl FindBar { let buffer = editor.buffer().read(cx).snapshot(cx); let ranges = search .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) - .map(|mat| { + .filter_map(|mat| { let mat = mat.unwrap(); - buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()) + + if self.whole_word_mode { + let prev_kind = + buffer.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = + char_kind(buffer.chars_at(mat.start()).next().unwrap()); + let end_kind = + char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) != prev_kind && Some(end_kind) != next_kind { + Some( + buffer.anchor_after(mat.start()) + ..buffer.anchor_before(mat.end()), + ) + } else { + None + } + } else { + Some(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())) + } }) .collect(); editor.highlight_ranges::(ranges, theme.match_background, cx); From b2ded5bca8cfb4703d86cc8e752d6adebfe6235f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 11:50:13 +0100 Subject: [PATCH 11/35] Optimize some common operations when `MultiBuffer` is a singleton --- crates/editor/src/multi_buffer.rs | 59 ++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4cedfa80d2..3d9b7ceb39 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -76,6 +76,7 @@ struct BufferState { #[derive(Clone, Default)] pub struct MultiBufferSnapshot { + singleton: bool, excerpts: SumTree, parse_count: usize, diagnostics_update_count: usize, @@ -163,6 +164,7 @@ impl MultiBuffer { }, cx, ); + this.snapshot.borrow_mut().singleton = true; this } @@ -1079,11 +1081,9 @@ impl MultiBufferSnapshot { .eq(needle.bytes()) } - fn as_singleton(&self) -> Option<&BufferSnapshot> { - let mut excerpts = self.excerpts.iter(); - let buffer = excerpts.next().map(|excerpt| &excerpt.buffer); - if excerpts.next().is_none() { - buffer + fn as_singleton(&self) -> Option<&Excerpt> { + if self.singleton { + self.excerpts.iter().next() } else { None } @@ -1098,6 +1098,10 @@ impl MultiBufferSnapshot { } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.clip_offset(offset, bias); + } + let mut cursor = self.excerpts.cursor::(); cursor.seek(&offset, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { @@ -1113,6 +1117,10 @@ impl MultiBufferSnapshot { } pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.clip_point(point, bias); + } + let mut cursor = self.excerpts.cursor::(); cursor.seek(&point, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { @@ -1128,6 +1136,10 @@ impl MultiBufferSnapshot { } pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.clip_point_utf16(point, bias); + } + let mut cursor = self.excerpts.cursor::(); cursor.seek(&point, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { @@ -1193,6 +1205,10 @@ impl MultiBufferSnapshot { } pub fn offset_to_point(&self, offset: usize) -> Point { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.offset_to_point(offset); + } + let mut cursor = self.excerpts.cursor::<(usize, Point)>(); cursor.seek(&offset, Bias::Right, &()); if let Some(excerpt) = cursor.item() { @@ -1210,6 +1226,10 @@ impl MultiBufferSnapshot { } pub fn point_to_offset(&self, point: Point) -> usize { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.point_to_offset(point); + } + let mut cursor = self.excerpts.cursor::<(Point, usize)>(); cursor.seek(&point, Bias::Right, &()); if let Some(excerpt) = cursor.item() { @@ -1227,6 +1247,10 @@ impl MultiBufferSnapshot { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.point_utf16_to_offset(point); + } + let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); cursor.seek(&point, Bias::Right, &()); if let Some(excerpt) = cursor.item() { @@ -1520,6 +1544,14 @@ impl MultiBufferSnapshot { pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); + if let Some(excerpt) = self.as_singleton() { + return Anchor { + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id.clone(), + text_anchor: excerpt.buffer.anchor_at(offset, bias), + }; + } + let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>(); cursor.seek(&offset, Bias::Right, &()); if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left { @@ -1675,7 +1707,7 @@ impl MultiBufferSnapshot { { self.as_singleton() .into_iter() - .flat_map(move |buffer| buffer.diagnostic_group(group_id)) + .flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id)) } pub fn diagnostics_in_range<'a, T, O>( @@ -1686,8 +1718,10 @@ impl MultiBufferSnapshot { T: 'a + ToOffset, O: 'a + text::FromAnchor, { - self.as_singleton().into_iter().flat_map(move |buffer| { - buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) + self.as_singleton().into_iter().flat_map(move |excerpt| { + excerpt + .buffer + .diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) }) } @@ -1730,17 +1764,16 @@ impl MultiBufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let buffer = self.as_singleton()?; - let outline = buffer.outline(theme)?; - let excerpt_id = &self.excerpts.iter().next().unwrap().id; + let excerpt = self.as_singleton()?; + let outline = excerpt.buffer.outline(theme)?; Some(Outline::new( outline .items .into_iter() .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), + 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, From 860e37d50ffb50e1f5f4f198604aeea6367598b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 12:15:55 +0100 Subject: [PATCH 12/35] Move finding results in the background --- Cargo.lock | 1 + crates/find/Cargo.toml | 1 + crates/find/src/find.rs | 90 +++++++++++++++++++++++++++-------------- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cce55da25d..a876093e52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,6 +1727,7 @@ dependencies = [ "editor", "gpui", "postage", + "smol", "theme", "workspace", ] diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 2984f013ec..cba620f688 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -13,3 +13,4 @@ gpui = { path = "../gpui" } theme = { path = "../theme" } workspace = { path = "../workspace" } postage = { version = "0.4.1", features = ["futures-traits"] } +smol = { version = "1.2" } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index cea47a4a77..ee70eca204 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,10 +1,11 @@ use aho_corasick::AhoCorasickBuilder; use editor::{char_kind, Editor, EditorSettings}; use gpui::{ - action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, View, + action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, }; use postage::watch; +use smol::future::yield_now; use std::sync::Arc; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; @@ -33,6 +34,7 @@ struct FindBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, + pending_search: Option>, case_sensitive_mode: bool, whole_word_mode: bool, regex_mode: bool, @@ -116,6 +118,7 @@ impl FindBar { whole_word_mode: false, regex_mode: false, settings, + pending_search: None, } } @@ -184,26 +187,38 @@ impl FindBar { _: &editor::Event, cx: &mut ViewContext, ) { - if let Some(editor) = &self.active_editor { - let search = self.query_editor.read(cx).text(cx); - let theme = &self.settings.borrow().theme.find; - editor.update(cx, |editor, cx| { - if search.is_empty() { - editor.clear_highlighted_ranges::(cx); - return; - } + self.update_matches(cx); + } + + fn update_matches(&mut self, cx: &mut ViewContext) { + let search = self.query_editor.read(cx).text(cx); + self.pending_search.take(); + if let Some(editor) = self.active_editor.as_ref() { + if search.is_empty() { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } else { + let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); + let case_sensitive_mode = self.case_sensitive_mode; + let whole_word_mode = self.whole_word_mode; + let ranges = cx.background().spawn(async move { + const YIELD_INTERVAL: usize = 20000; + + let search = AhoCorasickBuilder::new() + .auto_configure(&[&search]) + .ascii_case_insensitive(!case_sensitive_mode) + .build(&[&search]); + let mut ranges = Vec::new(); + for (ix, mat) in search + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } - let search = AhoCorasickBuilder::new() - .auto_configure(&[&search]) - .ascii_case_insensitive(!self.case_sensitive_mode) - .build(&[&search]); - let buffer = editor.buffer().read(cx).snapshot(cx); - let ranges = search - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) - .filter_map(|mat| { let mat = mat.unwrap(); - if self.whole_word_mode { + if whole_word_mode { let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); let start_kind = @@ -211,21 +226,34 @@ impl FindBar { let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) != prev_kind && Some(end_kind) != next_kind { - Some( - buffer.anchor_after(mat.start()) - ..buffer.anchor_before(mat.end()), - ) - } else { - None + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; } - } else { - Some(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())) } - }) - .collect(); - editor.highlight_ranges::(ranges, theme.match_background, cx); - }); + + ranges.push( + buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()), + ); + } + + ranges + }); + + let editor = editor.downgrade(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let ranges = ranges.await; + if let Some((this, editor)) = + cx.read(|cx| this.upgrade(cx).zip(editor.upgrade(cx))) + { + this.update(&mut cx, |this, cx| { + let theme = &this.settings.borrow().theme.find; + editor.update(cx, |editor, cx| { + editor.highlight_ranges::(ranges, theme.match_background, cx) + }); + }); + } + })); + } } } } From 5c862bfe983d58db2530b7a3b5abf85df710554a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 15:19:58 +0100 Subject: [PATCH 13/35] Maintain search results as query and active editor changes Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/find/Cargo.toml | 3 ++- crates/find/src/find.rs | 57 ++++++++++++++++++++++++++++++++++++----- crates/gpui/src/app.rs | 16 ++++++++++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a876093e52..889b620198 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1724,6 +1724,7 @@ name = "find" version = "0.1.0" dependencies = [ "aho-corasick", + "collections", "editor", "gpui", "postage", diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index cba620f688..8efce7024e 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -7,10 +7,11 @@ edition = "2021" path = "src/find.rs" [dependencies] -aho-corasick = "0.7" +collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } theme = { path = "../theme" } workspace = { path = "../workspace" } +aho-corasick = "0.7" postage = { version = "0.4.1", features = ["futures-traits"] } smol = { version = "1.2" } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index ee70eca204..a144019f28 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,8 +1,9 @@ use aho_corasick::AhoCorasickBuilder; +use collections::HashSet; use editor::{char_kind, Editor, EditorSettings}; use gpui::{ - action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Task, View, - ViewContext, ViewHandle, + action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Subscription, + Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use postage::watch; use smol::future::yield_now; @@ -34,6 +35,8 @@ struct FindBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, + active_editor_subscription: Option, + highlighted_editors: HashSet>, pending_search: Option>, case_sensitive_mode: bool, whole_word_mode: bool, @@ -85,8 +88,19 @@ impl Toolbar for FindBar { item: Option>, cx: &mut ViewContext, ) -> bool { - self.active_editor = item.and_then(|item| item.act_as::(cx)); - self.active_editor.is_some() + self.active_editor_subscription.take(); + self.active_editor.take(); + self.pending_search.take(); + + if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { + self.active_editor_subscription = + Some(cx.subscribe(&editor, Self::on_active_editor_event)); + self.active_editor = Some(editor); + self.update_matches(cx); + true + } else { + false + } } } @@ -114,6 +128,8 @@ impl FindBar { Self { query_editor, active_editor: None, + active_editor_subscription: None, + highlighted_editors: Default::default(), case_sensitive_mode: false, whole_word_mode: false, regex_mode: false, @@ -171,23 +187,49 @@ impl FindBar { } fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { - eprintln!("TOGGLE MODE"); let value = match mode { SearchMode::WholeWord => &mut self.whole_word_mode, SearchMode::CaseSensitive => &mut self.case_sensitive_mode, SearchMode::Regex => &mut self.regex_mode, }; *value = !*value; + self.update_matches(cx); cx.notify(); } fn on_query_editor_event( &mut self, _: ViewHandle, - _: &editor::Event, + event: &editor::Event, cx: &mut ViewContext, ) { - self.update_matches(cx); + match event { + editor::Event::Edited => { + for editor in self.highlighted_editors.drain() { + if let Some(editor) = editor.upgrade(cx) { + if Some(&editor) != self.active_editor.as_ref() { + editor.update(cx, |editor, cx| { + editor.clear_highlighted_ranges::(cx) + }); + } + } + } + self.update_matches(cx); + } + _ => {} + } + } + + fn on_active_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(cx), + _ => {} + } } fn update_matches(&mut self, cx: &mut ViewContext) { @@ -247,6 +289,7 @@ impl FindBar { { this.update(&mut cx, |this, cx| { let theme = &this.settings.borrow().theme.find; + this.highlighted_editors.insert(editor.downgrade()); editor.update(cx, |editor, cx| { editor.highlight_ranges::(ranges, theme.match_background, cx) }); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 31efb4991e..3a2124de95 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3246,6 +3246,7 @@ impl Drop for AnyModelHandle { self.ref_counts.lock().dec_model(self.model_id); } } + pub struct WeakViewHandle { window_id: usize, view_id: usize, @@ -3288,6 +3289,21 @@ impl Clone for WeakViewHandle { } } +impl PartialEq for WeakViewHandle { + fn eq(&self, other: &Self) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + +impl Eq for WeakViewHandle {} + +impl Hash for WeakViewHandle { + fn hash(&self, state: &mut H) { + self.window_id.hash(state); + self.view_id.hash(state); + } +} + #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct ElementStateId(usize, usize); From 5b9d791269b6b3b1f2e89a7c420231bb0f622a5a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 16:15:18 +0100 Subject: [PATCH 14/35] Implement regex search with multiline support Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 + crates/editor/src/editor.rs | 5 +- crates/editor/src/element.rs | 9 +- crates/find/Cargo.toml | 2 + crates/find/src/find.rs | 175 +++++++++++++++++++--------- crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 6 +- 7 files changed, 138 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 889b620198..e00df6c0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1724,10 +1724,12 @@ name = "find" version = "0.1.0" dependencies = [ "aho-corasick", + "anyhow", "collections", "editor", "gpui", "postage", + "regex", "smol", "theme", "workspace", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dc662969dc..146985bcc2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -29,10 +29,11 @@ use language::{ AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; +use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint, + Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot, + ToOffset, ToPoint, }; -use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot}; use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1b1ae46491..ecff2b93d1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -323,6 +323,7 @@ impl EditorElement { end_row, *color, 0., + 0.15 * layout.line_height, layout, content_origin, scroll_top, @@ -344,6 +345,7 @@ impl EditorElement { end_row, style.selection, corner_radius, + corner_radius * 2., layout, content_origin, scroll_top, @@ -400,6 +402,7 @@ impl EditorElement { end_row: u32, color: Color, corner_radius: f32, + line_end_overshoot: f32, layout: &LayoutState, content_origin: Vector2F, scroll_top: f32, @@ -414,7 +417,7 @@ impl EditorElement { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) }; - let selection = HighlightedRange { + let highlighted_range = HighlightedRange { color, line_height: layout.line_height, corner_radius, @@ -437,7 +440,7 @@ impl EditorElement { + line_layout.x_for_index(range.end.column() as usize) - scroll_left } else { - content_origin.x() + line_layout.width() + corner_radius * 2.0 + content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left }, } @@ -445,7 +448,7 @@ impl EditorElement { .collect(), }; - selection.paint(bounds, cx.scene); + highlighted_range.paint(bounds, cx.scene); } } diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 8efce7024e..eb3d99ce34 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -13,5 +13,7 @@ gpui = { path = "../gpui" } theme = { path = "../theme" } workspace = { path = "../workspace" } aho-corasick = "0.7" +anyhow = "1.0" postage = { version = "0.4.1", features = ["futures-traits"] } +regex = "1.5" smol = { version = "1.2" } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index a144019f28..90412fb99b 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,13 +1,15 @@ use aho_corasick::AhoCorasickBuilder; +use anyhow::Result; use collections::HashSet; -use editor::{char_kind, Editor, EditorSettings}; +use editor::{char_kind, Anchor, Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{ action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use postage::watch; +use regex::RegexBuilder; use smol::future::yield_now; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); @@ -41,6 +43,7 @@ struct FindBar { case_sensitive_mode: bool, whole_word_mode: bool, regex_mode: bool, + query_contains_error: bool, } impl Entity for FindBar { @@ -58,11 +61,16 @@ impl View for FindBar { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme.find; + let editor_container = if self.query_contains_error { + theme.invalid_editor + } else { + theme.editor.input.container + }; Flex::row() .with_child( ChildView::new(&self.query_editor) .contained() - .with_style(theme.editor.input.container) + .with_style(editor_container) .constrained() .with_max_width(theme.editor.max_width) .boxed(), @@ -135,6 +143,7 @@ impl FindBar { regex_mode: false, settings, pending_search: None, + query_contains_error: false, } } @@ -214,7 +223,9 @@ impl FindBar { } } } + self.query_contains_error = false; self.update_matches(cx); + cx.notify(); } _ => {} } @@ -233,70 +244,122 @@ impl FindBar { } fn update_matches(&mut self, cx: &mut ViewContext) { - let search = self.query_editor.read(cx).text(cx); + let query = self.query_editor.read(cx).text(cx); self.pending_search.take(); if let Some(editor) = self.active_editor.as_ref() { - if search.is_empty() { + if query.is_empty() { editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); } else { let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - let case_sensitive_mode = self.case_sensitive_mode; - let whole_word_mode = self.whole_word_mode; - let ranges = cx.background().spawn(async move { - const YIELD_INTERVAL: usize = 20000; - - let search = AhoCorasickBuilder::new() - .auto_configure(&[&search]) - .ascii_case_insensitive(!case_sensitive_mode) - .build(&[&search]); - let mut ranges = Vec::new(); - for (ix, mat) in search - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) - .enumerate() - { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - let mat = mat.unwrap(); - - if whole_word_mode { - let prev_kind = - buffer.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = - char_kind(buffer.chars_at(mat.start()).next().unwrap()); - let end_kind = - char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { - continue; - } - } - - ranges.push( - buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()), - ); - } - - ranges - }); + let case_sensitive = self.case_sensitive_mode; + let whole_word = self.whole_word_mode; + let ranges = if self.regex_mode { + cx.background() + .spawn(regex_search(buffer, query, case_sensitive, whole_word)) + } else { + cx.background().spawn(async move { + Ok(search(buffer, query, case_sensitive, whole_word).await) + }) + }; let editor = editor.downgrade(); - self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let ranges = ranges.await; - if let Some((this, editor)) = - cx.read(|cx| this.upgrade(cx).zip(editor.upgrade(cx))) - { - this.update(&mut cx, |this, cx| { - let theme = &this.settings.borrow().theme.find; - this.highlighted_editors.insert(editor.downgrade()); - editor.update(cx, |editor, cx| { - editor.highlight_ranges::(ranges, theme.match_background, cx) + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + match ranges.await { + Ok(ranges) => { + if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) { + this.update(&mut cx, |this, cx| { + let theme = &this.settings.borrow().theme.find; + this.highlighted_editors.insert(editor.downgrade()); + editor.update(cx, |editor, cx| { + editor.highlight_ranges::( + ranges, + theme.match_background, + cx, + ) + }); + }); + } + } + Err(_) => { + this.update(&mut cx, |this, cx| { + this.query_contains_error = true; + cx.notify(); }); - }); + } } })); } } } } + +const YIELD_INTERVAL: usize = 20000; + +async fn search( + buffer: MultiBufferSnapshot, + query: String, + case_sensitive: bool, + whole_word: bool, +) -> Vec> { + let mut ranges = Vec::new(); + + let search = AhoCorasickBuilder::new() + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); + for (ix, mat) in search + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + + if whole_word { + let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + + ranges +} + +async fn regex_search( + buffer: MultiBufferSnapshot, + mut query: String, + case_sensitive: bool, + whole_word: bool, +) -> Result>> { + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query; + } + + let mut ranges = Vec::new(); + + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(true) + .build()?; + for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + + Ok(ranges) +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e4274653e8..d9fab286b0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -93,6 +93,7 @@ pub struct Find { #[serde(flatten)] pub container: ContainerStyle, pub editor: FindEditor, + pub invalid_editor: ContainerStyle, pub mode_button_group: ContainerStyle, pub mode_button: ContainedText, pub active_mode_button: ContainedText, diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index b52e80e400..c88c4bf3d8 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -185,7 +185,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -352,3 +352,7 @@ text = "$text.0" placeholder_text = "$text.2" selection = "$selection.host" border = { width = 1, color = "$border.0" } + +[find.invalid_editor] +extends = "$find.editor" +border = { width = 1, color = "$status.bad" } From d2a64f217176f67818e0f6b964a78b0029153887 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 17:26:08 +0100 Subject: [PATCH 15/35] Optimize search when regex doesn't contain newlines Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/find/src/find.rs | 54 +++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 90412fb99b..f57c531987 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -115,7 +115,8 @@ impl Toolbar for FindBar { impl FindBar { fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { let query_editor = cx.add_view(|cx| { - Editor::single_line( + Editor::auto_height( + 2, { let settings = settings.clone(); Arc::new(move |_| { @@ -349,16 +350,49 @@ async fn regex_search( let mut ranges = Vec::new(); - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .multi_line(true) - .build()?; - for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } + if query.contains("\n") || query.contains("\\n") { + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(true) + .build()?; + for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + } else { + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .build()?; + + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in buffer + .chunks(0..buffer.len(), None) + .map(|c| c.text) + .chain(["\n"]) + .enumerate() + { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } } Ok(ranges) From 7e02d669e1e4c929c2970ae109dddf27011844b1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 28 Jan 2022 13:05:29 -0800 Subject: [PATCH 16/35] Add a simple unit test for find bar --- Cargo.lock | 1 + crates/editor/src/editor.rs | 30 ++++--- crates/editor/src/element.rs | 4 +- crates/find/Cargo.toml | 6 ++ crates/find/src/find.rs | 150 +++++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e00df6c0d0..a8e555b423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1732,6 +1732,7 @@ dependencies = [ "regex", "smol", "theme", + "unindent", "workspace", ] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 146985bcc2..5b04e129c4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3750,11 +3750,23 @@ impl Editor { cx.notify(); } + #[cfg(feature = "test-support")] + pub fn highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Vec<(Range, Color)> { + let snapshot = self.snapshot(cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + self.highlighted_ranges_in_range(start..end, &snapshot) + } + pub fn highlighted_ranges_in_range( &self, search_range: Range, display_snapshot: &DisplaySnapshot, - ) -> Vec<(Color, Range)> { + ) -> Vec<(Range, Color)> { let mut results = Vec::new(); let buffer = &display_snapshot.buffer_snapshot; for (color, ranges) in self.highlighted_ranges.values() { @@ -3780,7 +3792,7 @@ impl Editor { .end .to_point(buffer) .to_display_point(display_snapshot); - results.push((*color, start..end)) + results.push((start..end, *color)) } } results @@ -6679,20 +6691,20 @@ mod tests { ), &[ ( - Color::red(), DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), - ), - ( Color::red(), + ), + ( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), ), ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), Color::green(), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5) ), ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), Color::green(), - DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6) ), ] ); @@ -6702,9 +6714,9 @@ mod tests { &snapshot, ), &[( - Color::red(), DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - ),] + Color::red(), + )] ); }); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ecff2b93d1..8ef6656897 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -316,7 +316,7 @@ impl EditorElement { cx.scene.push_layer(Some(bounds)); - for (color, range) in &layout.highlighted_ranges { + for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( range.clone(), start_row, @@ -997,7 +997,7 @@ pub struct LayoutState { line_height: f32, em_width: f32, em_advance: f32, - highlighted_ranges: Vec<(Color, Range)>, + highlighted_ranges: Vec<(Range, Color)>, selections: HashMap>>, text_offset: Vector2F, } diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index eb3d99ce34..acab695d12 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -17,3 +17,9 @@ anyhow = "1.0" postage = { version = "0.4.1", features = ["futures-traits"] } regex = "1.5" smol = { version = "1.2" } + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } +unindent = "0.1" diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index f57c531987..db2d74a0a9 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -148,6 +148,16 @@ impl FindBar { } } + #[cfg(test)] + fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.read(cx).len(); + query_buffer.edit([0..len], query, cx); + }); + }); + } + fn render_mode_button( &self, icon: &str, @@ -397,3 +407,143 @@ async fn regex_search( Ok(ranges) } + +#[cfg(test)] +mod tests { + use super::*; + use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer}; + use gpui::{color::Color, TestAppContext}; + use std::sync::Arc; + use unindent::Unindent as _; + + #[gpui::test] + async fn test_find_simple(mut cx: TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); + theme.find.match_background = Color::red(); + let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); + + let buffer = cx.update(|cx| { + MultiBuffer::build_simple( + &r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + cx, + ) + }); + let editor = cx.add_view(Default::default(), |cx| { + Editor::new(buffer.clone(), Arc::new(EditorSettings::test), cx) + }); + + let find_bar = cx.add_view(Default::default(), |cx| { + let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx); + find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); + find_bar + }); + + // default: case-insensitive substring search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("us", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ), + ] + ); + }); + + // switch to case sensitive search + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.highlighted_ranges(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ),] + ); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("or", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + Color::red(), + ), + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + Color::red(), + ), + ( + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ( + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), + Color::red(), + ), + ] + ); + }); + + // switch to whole word search + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ] + ); + }); + } +} From 5c7cea5a3e7178c62e1ceceb3fa557d2db015136 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 28 Jan 2022 14:00:00 -0800 Subject: [PATCH 17/35] WIP - Navigate to prev/next search result --- crates/editor/src/editor.rs | 18 ++++-- crates/editor/src/items.rs | 2 +- crates/find/src/find.rs | 125 ++++++++++++++++++++++++++++++++---- 3 files changed, 127 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5b04e129c4..425d500d3d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -861,7 +861,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; - let newest_selection = self.newest_selection_internal().unwrap().clone(); + let newest_selection = self.newest_anchor_selection().unwrap().clone(); let start; let end; @@ -3358,10 +3358,10 @@ impl Editor { &self, snapshot: &MultiBufferSnapshot, ) -> Selection { - self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot) + self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot) } - pub fn newest_selection_internal(&self) -> Option<&Selection> { + pub fn newest_anchor_selection(&self) -> Option<&Selection> { self.pending_selection .as_ref() .map(|s| &s.selection) @@ -3377,7 +3377,7 @@ impl Editor { T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { let buffer = self.buffer.read(cx).snapshot(cx); - let old_cursor_position = self.newest_selection_internal().map(|s| s.head()); + let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. @@ -3511,6 +3511,7 @@ impl Editor { buffer.set_active_selections(&self.selections, cx) }); } + cx.emit(Event::SelectionsChanged); } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { @@ -3751,7 +3752,7 @@ impl Editor { } #[cfg(feature = "test-support")] - pub fn highlighted_ranges( + pub fn all_highlighted_ranges( &mut self, cx: &mut ViewContext, ) -> Vec<(Range, Color)> { @@ -3762,6 +3763,12 @@ impl Editor { self.highlighted_ranges_in_range(start..end, &snapshot) } + pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { + self.highlighted_ranges + .get(&TypeId::of::()) + .map(|(color, ranges)| (*color, ranges.as_slice())) + } + pub fn highlighted_ranges_in_range( &self, search_range: Range, @@ -4011,6 +4018,7 @@ pub enum Event { Dirtied, Saved, TitleChanged, + SelectionsChanged, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1dafec32c6..97ce056152 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -141,7 +141,7 @@ impl ItemView for Editor { } fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(selection) = self.newest_selection_internal() { + if let Some(selection) = self.newest_anchor_selection() { self.push_to_nav_history(selection.head(), None, cx); } } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index db2d74a0a9..434056b9a9 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -9,12 +9,19 @@ use gpui::{ use postage::watch; use regex::RegexBuilder; use smol::future::yield_now; -use std::{ops::Range, sync::Arc}; +use std::{cmp::Ordering, ops::Range, sync::Arc}; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); action!(Cancel); action!(ToggleMode, SearchMode); +action!(GoToMatch, Direction); + +#[derive(Clone, Copy)] +pub enum Direction { + Prev, + Next, +} #[derive(Clone, Copy)] pub enum SearchMode { @@ -31,12 +38,14 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(FindBar::deploy); cx.add_action(FindBar::cancel); cx.add_action(FindBar::toggle_mode); + cx.add_action(FindBar::go_to_match); } struct FindBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, + active_match_index: Option, active_editor_subscription: Option, highlighted_editors: HashSet>, pending_search: Option>, @@ -84,6 +93,12 @@ impl View for FindBar { .with_style(theme.mode_button_group) .boxed(), ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, theme, cx)) + .with_child(self.render_nav_button(">", Direction::Next, theme, cx)) + .boxed(), + ) .contained() .with_style(theme.container) .boxed() @@ -138,6 +153,7 @@ impl FindBar { query_editor, active_editor: None, active_editor_subscription: None, + active_match_index: None, highlighted_editors: Default::default(), case_sensitive_mode: false, whole_word_mode: false, @@ -166,7 +182,7 @@ impl FindBar { cx: &mut RenderContext, ) -> ElementBox { let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(mode as usize, cx, |state, _| { + MouseEventHandler::new::((cx.view_id(), mode as usize), cx, |state, _| { let style = match (is_active, state.hovered) { (false, false) => &theme.mode_button, (false, true) => &theme.hovered_mode_button, @@ -182,6 +198,32 @@ impl FindBar { .boxed() } + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + theme: &theme::Find, + cx: &mut RenderContext, + ) -> ElementBox { + MouseEventHandler::new::( + (cx.view_id(), 10 + direction as usize), + cx, + |state, _| { + let style = if state.hovered { + &theme.hovered_mode_button + } else { + &theme.mode_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }, + ) + .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .boxed() + } + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { @@ -217,6 +259,32 @@ impl FindBar { cx.notify(); } + fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { + if let Some(mut index) = self.active_match_index { + if let Some(editor) = self.active_editor.as_ref() { + editor.update(cx, |editor, cx| { + if let Some((_, ranges)) = editor.highlighted_ranges_for_type::() { + match direction { + Direction::Prev => { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + Direction::Next => { + index += 1; + if index >= ranges.len() { + index = 0; + } + } + } + } + }); + } + } + } + fn on_query_editor_event( &mut self, _: ViewHandle, @@ -244,12 +312,14 @@ impl FindBar { fn on_active_editor_event( &mut self, - _: ViewHandle, + editor: ViewHandle, event: &editor::Event, cx: &mut ViewContext, ) { match event { editor::Event::Edited => self.update_matches(cx), + editor::Event::SelectionsChanged => self.update_match_index(cx), + _ => {} } } @@ -279,15 +349,16 @@ impl FindBar { Ok(ranges) => { if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) { this.update(&mut cx, |this, cx| { - let theme = &this.settings.borrow().theme.find; this.highlighted_editors.insert(editor.downgrade()); editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.find; editor.highlight_ranges::( ranges, theme.match_background, cx, ) }); + this.update_match_index(cx); }); } } @@ -302,6 +373,29 @@ impl FindBar { } } } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + self.active_match_index = self.active_match_index(cx); + } + + fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { + let editor = self.active_editor.as_ref()?; + let editor = editor.read(cx); + let position = editor.newest_anchor_selection()?.head(); + let ranges = editor.highlighted_ranges_for_type::()?.1; + let buffer = editor.buffer().read(cx).read(cx); + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(i), + } + } } const YIELD_INTERVAL: usize = 20000; @@ -445,14 +539,15 @@ mod tests { find_bar }); - // default: case-insensitive substring search. + // Search for a string that appears with different casing. + // By default, search is case-insensitive. find_bar.update(&mut cx, |find_bar, cx| { find_bar.set_query("us", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[ ( DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), @@ -466,28 +561,30 @@ mod tests { ); }); - // switch to case sensitive search + // Switch to a case sensitive search. find_bar.update(&mut cx, |find_bar, cx| { find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), Color::red(), - ),] + )] ); }); + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. find_bar.update(&mut cx, |find_bar, cx| { find_bar.set_query("or", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[ ( DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), @@ -521,14 +618,14 @@ mod tests { ); }); - // switch to whole word search + // Switch to a whole word search. find_bar.update(&mut cx, |find_bar, cx| { find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[ ( DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), @@ -545,5 +642,9 @@ mod tests { ] ); }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + }); } } From 1d55872e7a48822643cca0c6d9087084959cdc8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 29 Jan 2022 14:38:58 +0100 Subject: [PATCH 18/35] Display active match and allow going to next or previous match We still need to write a unit test for this, as well as add a keybinding. --- crates/find/src/find.rs | 35 ++++++++++++++++++++++++----- crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 3 +++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 434056b9a9..fe91af58be 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,7 +1,7 @@ use aho_corasick::AhoCorasickBuilder; use anyhow::Result; use collections::HashSet; -use editor::{char_kind, Anchor, Editor, EditorSettings, MultiBufferSnapshot}; +use editor::{char_kind, Anchor, Autoscroll, Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{ action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -9,7 +9,11 @@ use gpui::{ use postage::watch; use regex::RegexBuilder; use smol::future::yield_now; -use std::{cmp::Ordering, ops::Range, sync::Arc}; +use std::{ + cmp::{self, Ordering}, + ops::Range, + sync::Arc, +}; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); @@ -99,6 +103,20 @@ impl View for FindBar { .with_child(self.render_nav_button(">", Direction::Next, theme, cx)) .boxed(), ) + .with_children(self.active_editor.as_ref().and_then(|editor| { + let (_, highlighted_ranges) = + editor.read(cx).highlighted_ranges_for_type::()?; + let match_ix = cmp::min(self.active_match_index? + 1, highlighted_ranges.len()); + Some( + Label::new( + format!("{} of {}", match_ix, highlighted_ranges.len()), + theme.match_index.text.clone(), + ) + .contained() + .with_style(theme.match_index.container) + .boxed(), + ) + })) .contained() .with_style(theme.container) .boxed() @@ -273,12 +291,16 @@ impl FindBar { } } Direction::Next => { - index += 1; - if index >= ranges.len() { + if index == ranges.len() - 1 { index = 0; + } else { + index += 1; } } } + + let range_to_select = ranges[index].clone(); + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); } }); } @@ -312,14 +334,13 @@ impl FindBar { fn on_active_editor_event( &mut self, - editor: ViewHandle, + _: ViewHandle, event: &editor::Event, cx: &mut ViewContext, ) { match event { editor::Event::Edited => self.update_matches(cx), editor::Event::SelectionsChanged => self.update_match_index(cx), - _ => {} } } @@ -329,6 +350,7 @@ impl FindBar { self.pending_search.take(); if let Some(editor) = self.active_editor.as_ref() { if query.is_empty() { + self.active_match_index.take(); editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); } else { let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); @@ -376,6 +398,7 @@ impl FindBar { fn update_match_index(&mut self, cx: &mut ViewContext) { self.active_match_index = self.active_match_index(cx); + cx.notify(); } fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d9fab286b0..37414eca2f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -100,6 +100,7 @@ pub struct Find { pub hovered_mode_button: ContainedText, pub active_hovered_mode_button: ContainedText, pub match_background: Color, + pub match_index: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index c88c4bf3d8..360d5d03e1 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -342,6 +342,9 @@ background = "$surface.2" extends = "$find.mode_button" background = "$surface.2" +[find.match_index] +extends = "$text.1" + [find.editor] max_width = 400 background = "$surface.0" From df4cc45790f9e6d067f6391a1a41ae16608a3a49 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 29 Jan 2022 16:16:48 +0100 Subject: [PATCH 19/35] Select next/prev find match based on whether we intersect active match Also, capture the next/prev selection logic in a unit test. --- crates/find/src/find.rs | 116 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index fe91af58be..dc89f95a5a 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -21,7 +21,7 @@ action!(Cancel); action!(ToggleMode, SearchMode); action!(GoToMatch, Direction); -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum Direction { Prev, Next, @@ -281,25 +281,39 @@ impl FindBar { if let Some(mut index) = self.active_match_index { if let Some(editor) = self.active_editor.as_ref() { editor.update(cx, |editor, cx| { - if let Some((_, ranges)) = editor.highlighted_ranges_for_type::() { - match direction { - Direction::Prev => { + let newest_selection = editor.newest_anchor_selection().cloned(); + if let Some(((_, ranges), newest_selection)) = editor + .highlighted_ranges_for_type::() + .zip(newest_selection) + { + let position = newest_selection.head(); + let buffer = editor.buffer().read(cx).read(cx); + if ranges[index].start.cmp(&position, &buffer).unwrap().is_le() + && ranges[index].end.cmp(&position, &buffer).unwrap().is_ge() + { + if *direction == Direction::Prev { if index == 0 { index = ranges.len() - 1; } else { index -= 1; } - } - Direction::Next => { + } else { if index == ranges.len() - 1 { - index = 0; + index = 0 } else { index += 1; } } + } else if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } } let range_to_select = ranges[index].clone(); + drop(buffer); editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); } }); @@ -668,6 +682,94 @@ mod tests { find_bar.update(&mut cx, |find_bar, cx| { find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); }); } } From 969d81b632ce3727bcaee67b311ada293379badb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 29 Jan 2022 16:34:49 +0100 Subject: [PATCH 20/35] Determine active match index correctly when cursor is after last match --- crates/find/src/find.rs | 96 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index dc89f95a5a..4096abe03c 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -288,21 +288,17 @@ impl FindBar { { let position = newest_selection.head(); let buffer = editor.buffer().read(cx).read(cx); - if ranges[index].start.cmp(&position, &buffer).unwrap().is_le() - && ranges[index].end.cmp(&position, &buffer).unwrap().is_ge() - { + if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { if *direction == Direction::Prev { if index == 0 { index = ranges.len() - 1; } else { index -= 1; } - } else { - if index == ranges.len() - 1 { - index = 0 - } else { - index += 1; - } + } + } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { + if *direction == Direction::Next { + index = 0; } } else if *direction == Direction::Prev { if index == 0 { @@ -310,6 +306,12 @@ impl FindBar { } else { index -= 1; } + } else if *direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } } let range_to_select = ranges[index].clone(); @@ -430,7 +432,7 @@ impl FindBar { Ordering::Equal } }) { - Ok(i) | Err(i) => Some(i), + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), } } } @@ -680,7 +682,11 @@ mod tests { ); }); + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); find_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), @@ -757,6 +763,8 @@ mod tests { assert_eq!(find_bar.active_match_index, Some(0)); }); + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); }); @@ -771,5 +779,73 @@ mod tests { find_bar.read_with(&cx, |find_bar, _| { assert_eq!(find_bar.active_match_index, Some(0)); }); + + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor before the first match and ensure that going to the previous match + // selects the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); } } From c16bd98f565f48f99e2ef311bfe2d71b36633764 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 29 Jan 2022 18:21:19 +0100 Subject: [PATCH 21/35] Fix panic when no matches were found --- crates/find/src/find.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 4096abe03c..f1725d4b96 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -432,7 +432,7 @@ impl FindBar { Ordering::Equal } }) { - Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len().saturating_sub(1))), } } } From e8105c9a99512f736066123178b4638e1c5e3fef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 29 Jan 2022 18:23:14 +0100 Subject: [PATCH 22/35] Show a message when no matches were found --- crates/find/src/find.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index f1725d4b96..6b8a061892 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -107,14 +107,16 @@ impl View for FindBar { let (_, highlighted_ranges) = editor.read(cx).highlighted_ranges_for_type::()?; let match_ix = cmp::min(self.active_match_index? + 1, highlighted_ranges.len()); + let message = if highlighted_ranges.is_empty() { + "No matches".to_string() + } else { + format!("{} of {}", match_ix, highlighted_ranges.len()) + }; Some( - Label::new( - format!("{} of {}", match_ix, highlighted_ranges.len()), - theme.match_index.text.clone(), - ) - .contained() - .with_style(theme.match_index.container) - .boxed(), + Label::new(message, theme.match_index.text.clone()) + .contained() + .with_style(theme.match_index.container) + .boxed(), ) })) .contained() From 2a1b1adfabc3dc413d6d88e3e2597e040fca6b00 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 30 Jan 2022 15:20:57 +0100 Subject: [PATCH 23/35] Bind `enter` and `shift-enter` in `FindBar` --- crates/find/src/find.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 6b8a061892..6c5aaec8a1 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -38,6 +38,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-f", Deploy, Some("Editor && mode == full")), Binding::new("escape", Cancel, Some("FindBar")), + Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), + Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), ]); cx.add_action(FindBar::deploy); cx.add_action(FindBar::cancel); From c53b6b907a94334b6badd4c7f171e0bb5a1b08ef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 30 Jan 2022 16:00:16 +0100 Subject: [PATCH 24/35] Populate query with text under selection when hitting `cmd-f` --- crates/editor/src/editor.rs | 2 +- crates/find/src/find.rs | 45 +++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 425d500d3d..0ee4f3191e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -47,7 +47,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::Bias; +pub use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; use util::post_inc; diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 6c5aaec8a1..78a04a2126 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,7 +1,10 @@ use aho_corasick::AhoCorasickBuilder; use anyhow::Result; use collections::HashSet; -use editor::{char_kind, Anchor, Autoscroll, Editor, EditorSettings, MultiBufferSnapshot}; +use editor::{ + char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings, + MultiBufferSnapshot, +}; use gpui::{ action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -186,13 +189,13 @@ impl FindBar { } } - #[cfg(test)] fn set_query(&mut self, query: &str, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { let len = query_buffer.read(cx).len(); query_buffer.edit([0..len], query, cx); }); + query_editor.select_ranges([0..query.len()], None, cx); }); } @@ -250,8 +253,42 @@ impl FindBar { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); - if let Some(toolbar) = pane.active_toolbar() { - cx.focus(toolbar); + if let Some(find_bar) = pane + .active_toolbar() + .and_then(|toolbar| toolbar.downcast::()) + { + cx.focus(&find_bar); + + if let Some(editor) = pane + .active_item() + .and_then(|editor| editor.act_as::(cx)) + { + let display_map = editor + .update(cx, |editor, cx| editor.snapshot(cx)) + .display_snapshot; + let selection = editor + .read(cx) + .newest_selection::(&display_map.buffer_snapshot); + + let 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() { + return; + } + } else { + text = display_map + .buffer_snapshot + .text_for_range(selection.start..selection.end) + .collect(); + } + + find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + } } }); } From ce5270488f60aa7333dfa8dddef4d4b5efb70066 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 30 Jan 2022 16:01:13 +0100 Subject: [PATCH 25/35] Propagate `Cancel` when editor is not in full mode This is consistent with what VS Code and Sublime Text do and allows the user to perform only one keybinding to e.g. dismiss the find bar when the query is (partially) selected. --- crates/editor/src/editor.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0ee4f3191e..db0c099c4b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1091,6 +1091,11 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.mode != EditorMode::Full { + cx.propagate_action(); + return; + } + if self.active_diagnostics.is_some() { self.dismiss_diagnostics(cx); } else if let Some(PendingSelection { selection, .. }) = self.pending_selection.take() { From 9ce1eda30509a8bedd7e6664bec2b8ec48563c23 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 30 Jan 2022 16:11:48 +0100 Subject: [PATCH 26/35] Bind `cmd-e` to deploy `FindBar` without focusing it --- crates/find/src/find.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 78a04a2126..f5cd80fb8b 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -19,7 +19,7 @@ use std::{ }; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; -action!(Deploy); +action!(Deploy, bool); action!(Cancel); action!(ToggleMode, SearchMode); action!(GoToMatch, Direction); @@ -39,7 +39,8 @@ pub enum SearchMode { pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ - Binding::new("cmd-f", Deploy, Some("Editor && mode == full")), + Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), + Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), Binding::new("escape", Cancel, Some("FindBar")), Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), @@ -74,6 +75,9 @@ impl View for FindBar { } fn on_focus(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&editor::SelectAll, cx); + }); cx.focus(&self.query_editor); } @@ -195,7 +199,6 @@ impl FindBar { let len = query_buffer.read(cx).len(); query_buffer.edit([0..len], query, cx); }); - query_editor.select_ranges([0..query.len()], None, cx); }); } @@ -249,7 +252,7 @@ impl FindBar { .boxed() } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); @@ -257,8 +260,6 @@ impl FindBar { .active_toolbar() .and_then(|toolbar| toolbar.downcast::()) { - cx.focus(&find_bar); - if let Some(editor) = pane .active_item() .and_then(|editor| editor.act_as::(cx)) @@ -270,7 +271,7 @@ impl FindBar { .read(cx) .newest_selection::(&display_map.buffer_snapshot); - let text: String; + 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); @@ -278,7 +279,7 @@ impl FindBar { ..range.end.to_offset(&display_map, Bias::Right); text = display_map.buffer_snapshot.text_for_range(range).collect(); if text.trim().is_empty() { - return; + text = String::new(); } } else { text = display_map @@ -287,7 +288,13 @@ impl FindBar { .collect(); } - find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + if !text.is_empty() { + find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + } + } + + if *focus { + cx.focus(&find_bar); } } }); From 83423a4344fcb3cb7da2fb766ac9f8c16a78935a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 30 Jan 2022 16:30:07 +0100 Subject: [PATCH 27/35] Use cmd-f to move focus back to the editor when find bar is focused --- crates/find/src/find.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index f5cd80fb8b..584f701f65 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -21,6 +21,7 @@ use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Cancel); +action!(FocusEditor); action!(ToggleMode, SearchMode); action!(GoToMatch, Direction); @@ -42,11 +43,13 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), Binding::new("escape", Cancel, Some("FindBar")), + Binding::new("cmd-f", FocusEditor, Some("FindBar")), Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), ]); cx.add_action(FindBar::deploy); cx.add_action(FindBar::cancel); + cx.add_action(FindBar::focus_editor); cx.add_action(FindBar::toggle_mode); cx.add_action(FindBar::go_to_match); } @@ -306,6 +309,12 @@ impl FindBar { .update(cx, |pane, cx| pane.hide_toolbar(cx)); } + fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + if let Some(active_editor) = self.active_editor.as_ref() { + cx.focus(active_editor); + } + } + fn is_mode_enabled(&self, mode: SearchMode) -> bool { match mode { SearchMode::WholeWord => self.whole_word_mode, From f90193beea2b35b19e70d9fbc21d3f76a1efeaf9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 30 Jan 2022 16:44:42 +0100 Subject: [PATCH 28/35] Populate query and select it only if find bar isn't already deployed --- crates/find/src/find.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 584f701f65..65a164b969 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -78,9 +78,6 @@ impl View for FindBar { } fn on_focus(&mut self, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&editor::SelectAll, cx); - }); cx.focus(&self.query_editor); } @@ -258,15 +255,18 @@ impl FindBar { fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { + let findbar_was_visible = pane + .active_toolbar() + .map_or(false, |toolbar| toolbar.downcast::().is_some()); + pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + if let Some(find_bar) = pane .active_toolbar() .and_then(|toolbar| toolbar.downcast::()) { - if let Some(editor) = pane - .active_item() - .and_then(|editor| editor.act_as::(cx)) - { + if !findbar_was_visible { + let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); let display_map = editor .update(cx, |editor, cx| editor.snapshot(cx)) .display_snapshot; @@ -297,6 +297,12 @@ impl FindBar { } if *focus { + if !findbar_was_visible { + let query_editor = find_bar.read(cx).query_editor.clone(); + query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&editor::SelectAll, cx); + }); + } cx.focus(&find_bar); } } From b1639e56772cc48f1d9fe90a382b63956b06e171 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 30 Jan 2022 20:59:20 -0700 Subject: [PATCH 29/35] Add cmd-g and cmd-shift-g to jump to next / previous result I added the action handler on Pane so we can use these bindings when the find bar isn't focused. --- crates/find/src/find.rs | 11 ++++++++++- crates/workspace/src/pane.rs | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 65a164b969..f7e82f4c02 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -17,7 +17,7 @@ use std::{ ops::Range, sync::Arc, }; -use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; +use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Cancel); @@ -46,12 +46,15 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-f", FocusEditor, Some("FindBar")), Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), + Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), + Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), ]); cx.add_action(FindBar::deploy); cx.add_action(FindBar::cancel); cx.add_action(FindBar::focus_editor); cx.add_action(FindBar::toggle_mode); cx.add_action(FindBar::go_to_match); + cx.add_action(FindBar::go_to_match_on_pane); } struct FindBar { @@ -386,6 +389,12 @@ impl FindBar { } } + fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { + if let Some(find_bar) = pane.toolbar::() { + find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); + } + } + fn on_query_editor_event( &mut self, _: ViewHandle, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a112213795..6721401817 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -417,6 +417,12 @@ impl Pane { cx.notify(); } + pub fn toolbar(&self) -> Option> { + self.toolbars + .get(&TypeId::of::()) + .and_then(|toolbar| toolbar.to_any().downcast()) + } + pub fn active_toolbar(&self) -> Option { let type_id = self.active_toolbar_type?; let toolbar = self.toolbars.get(&type_id)?; From 611538f6bda72de8157cda91d4bfe00350c4fc41 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 09:58:03 +0100 Subject: [PATCH 30/35] Clear highlighted matches when dismissing `FindBar` --- crates/find/src/find.rs | 38 +++++++++++++++++++----------- crates/workspace/src/pane.rs | 45 +++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index f7e82f4c02..47f38ab3f9 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -20,7 +20,7 @@ use std::{ use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); -action!(Cancel); +action!(Dismiss); action!(FocusEditor); action!(ToggleMode, SearchMode); action!(GoToMatch, Direction); @@ -42,7 +42,7 @@ 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", Cancel, Some("FindBar")), + Binding::new("escape", Dismiss, Some("FindBar")), Binding::new("cmd-f", FocusEditor, Some("FindBar")), Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), @@ -50,7 +50,7 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), ]); cx.add_action(FindBar::deploy); - cx.add_action(FindBar::cancel); + cx.add_action(FindBar::dismiss); cx.add_action(FindBar::focus_editor); cx.add_action(FindBar::toggle_mode); cx.add_action(FindBar::go_to_match); @@ -157,6 +157,14 @@ impl Toolbar for FindBar { false } } + + fn on_dismiss(&mut self, cx: &mut ViewContext) { + self.active_editor.take(); + self.active_editor_subscription.take(); + self.active_match_index.take(); + self.pending_search.take(); + self.clear_matches(cx); + } } impl FindBar { @@ -312,10 +320,10 @@ impl FindBar { }); } - fn cancel(workspace: &mut Workspace, _: &Cancel, cx: &mut ViewContext) { + fn dismiss(workspace: &mut Workspace, _: &Dismiss, cx: &mut ViewContext) { workspace .active_pane() - .update(cx, |pane, cx| pane.hide_toolbar(cx)); + .update(cx, |pane, cx| pane.dismiss_toolbar(cx)); } fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { @@ -403,16 +411,8 @@ impl FindBar { ) { match event { editor::Event::Edited => { - for editor in self.highlighted_editors.drain() { - if let Some(editor) = editor.upgrade(cx) { - if Some(&editor) != self.active_editor.as_ref() { - editor.update(cx, |editor, cx| { - editor.clear_highlighted_ranges::(cx) - }); - } - } - } self.query_contains_error = false; + self.clear_matches(cx); self.update_matches(cx); cx.notify(); } @@ -433,6 +433,16 @@ impl FindBar { } } + fn clear_matches(&mut self, cx: &mut ViewContext) { + for editor in self.highlighted_editors.drain() { + if let Some(editor) = editor.upgrade(cx) { + if Some(&editor) != self.active_editor.as_ref() { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } + } + } + } + fn update_matches(&mut self, cx: &mut ViewContext) { let query = self.query_editor.read(cx).text(cx); self.pending_search.take(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 6721401817..ac7ea21471 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -92,6 +92,7 @@ pub trait Toolbar: View { item: Option>, cx: &mut ViewContext, ) -> bool; + fn on_dismiss(&mut self, cx: &mut ViewContext); } trait ToolbarHandle { @@ -100,6 +101,7 @@ trait ToolbarHandle { item: Option>, cx: &mut MutableAppContext, ) -> bool; + fn on_dismiss(&self, cx: &mut MutableAppContext); fn to_any(&self) -> AnyViewHandle; } @@ -401,20 +403,31 @@ impl Pane { V: Toolbar, { let type_id = TypeId::of::(); - 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(); + 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 hide_toolbar(&mut self, cx: &mut ViewContext) { - self.active_toolbar_type = None; - self.active_toolbar_visible = false; - self.focus_active_item(cx); - cx.notify(); + pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext) { + if let Some(active_toolbar_type) = self.active_toolbar_type.take() { + self.toolbars + .get_mut(&active_toolbar_type) + .unwrap() + .on_dismiss(cx); + self.active_toolbar_visible = false; + self.focus_active_item(cx); + cx.notify(); + } } pub fn toolbar(&self) -> Option> { @@ -437,7 +450,9 @@ impl Pane { if let Some(type_id) = self.active_toolbar_type { if let Some(toolbar) = self.toolbars.get(&type_id) { self.active_toolbar_visible = toolbar.active_item_changed( - Some(self.item_views[self.active_item_index].1.clone()), + self.item_views + .get(self.active_item_index) + .map(|i| i.1.clone()), cx, ); } @@ -621,6 +636,10 @@ impl ToolbarHandle for ViewHandle { 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() } From 7db4cad9e0620857d462abfff60e11aa23bebe49 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 10:10:15 +0100 Subject: [PATCH 31/35] Fix panic when closing pane's last item --- crates/workspace/src/pane.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ac7ea21471..05b7b7e19b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -385,6 +385,8 @@ impl Pane { self.update_active_toolbar(cx); cx.emit(Event::Remove); } + + cx.notify(); } fn focus_active_item(&mut self, cx: &mut ViewContext) { From 803cdd00a66afa12eda982b237074fea02c43c75 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 11:00:29 +0100 Subject: [PATCH 32/35] Add some basic styling to `FindBar` --- crates/find/src/find.rs | 54 +++++++++++++++++------------ crates/theme/src/theme.rs | 6 ++++ crates/zed/assets/themes/_base.toml | 15 ++++++-- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 47f38ab3f9..ea2b29a277 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -6,8 +6,8 @@ use editor::{ MultiBufferSnapshot, }; use gpui::{ - action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Subscription, - Task, View, ViewContext, ViewHandle, WeakViewHandle, + action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use postage::watch; use regex::RegexBuilder; @@ -85,34 +85,30 @@ impl View for FindBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &self.settings.borrow().theme.find; + let theme = &self.settings.borrow().theme; let editor_container = if self.query_contains_error { - theme.invalid_editor + theme.find.invalid_editor } else { - theme.editor.input.container + theme.find.editor.input.container }; Flex::row() .with_child( ChildView::new(&self.query_editor) .contained() .with_style(editor_container) + .aligned() .constrained() - .with_max_width(theme.editor.max_width) + .with_max_width(theme.find.editor.max_width) .boxed(), ) .with_child( Flex::row() - .with_child(self.render_mode_button("Aa", SearchMode::CaseSensitive, theme, cx)) - .with_child(self.render_mode_button("|ab|", SearchMode::WholeWord, theme, cx)) - .with_child(self.render_mode_button(".*", SearchMode::Regex, theme, cx)) + .with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx)) + .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx)) + .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx)) .contained() - .with_style(theme.mode_button_group) - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, theme, cx)) - .with_child(self.render_nav_button(">", Direction::Next, theme, cx)) + .with_style(theme.find.mode_button_group) + .aligned() .boxed(), ) .with_children(self.active_editor.as_ref().and_then(|editor| { @@ -122,18 +118,28 @@ impl View for FindBar { let message = if highlighted_ranges.is_empty() { "No matches".to_string() } else { - format!("{} of {}", match_ix, highlighted_ranges.len()) + format!("{}/{}", match_ix, highlighted_ranges.len()) }; Some( - Label::new(message, theme.match_index.text.clone()) + Label::new(message, theme.find.match_index.text.clone()) .contained() - .with_style(theme.match_index.container) + .with_style(theme.find.match_index.container) + .aligned() .boxed(), ) })) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + ) .contained() - .with_style(theme.container) - .boxed() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") } } @@ -217,9 +223,9 @@ impl FindBar { &self, icon: &str, mode: SearchMode, - theme: &theme::Find, cx: &mut RenderContext, ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; let is_active = self.is_mode_enabled(mode); MouseEventHandler::new::((cx.view_id(), mode as usize), cx, |state, _| { let style = match (is_active, state.hovered) { @@ -234,6 +240,7 @@ impl FindBar { .boxed() }) .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -241,9 +248,9 @@ impl FindBar { &self, icon: &str, direction: Direction, - theme: &theme::Find, cx: &mut RenderContext, ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; MouseEventHandler::new::( (cx.view_id(), 10 + direction as usize), cx, @@ -260,6 +267,7 @@ impl FindBar { }, ) .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .with_cursor_style(CursorStyle::PointingHand) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 37414eca2f..fec42853dc 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -38,6 +38,7 @@ pub struct Workspace { pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, + pub toolbar: Toolbar, } #[derive(Clone, Deserialize, Default)] @@ -88,6 +89,11 @@ pub struct Tab { pub icon_conflict: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct Toolbar { + pub height: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct Find { #[serde(flatten)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 360d5d03e1..69a938dacf 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -81,6 +81,9 @@ item_spacing = 24 cursor_position = "$text.2" diagnostic_message = "$text.2" +[workspace.toolbar] +height = 44 + [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } @@ -325,10 +328,14 @@ background = "$surface.1" [find.mode_button] extends = "$text.1" - -[find.mode_button_group] +padding = { left = 6, right = 6, top = 1, bottom = 1 } corner_radius = 6 border = { width = 1, color = "$border.0" } +margin.left = 1 +margin.right = 1 + +[find.mode_button_group] +padding = { left = 2, right = 2 } [find.active_mode_button] extends = "$find.mode_button" @@ -344,12 +351,14 @@ background = "$surface.2" [find.match_index] extends = "$text.1" +padding = 6 +margin.left = 24 [find.editor] max_width = 400 background = "$surface.0" corner_radius = 6 -padding = { left = 16, right = 16, top = 7, bottom = 7 } +padding = { left = 13, right = 13, top = 3, bottom = 3 } margin = { top = 5, bottom = 5, left = 5, right = 5 } text = "$text.0" placeholder_text = "$text.2" From eb537214ed4c4ce0c0eed9e96c3eea8a01e32a22 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 11:04:53 +0100 Subject: [PATCH 33/35] Fix panic when moving to next/prev result but there are no matches --- crates/find/src/find.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index ea2b29a277..d393cbd9c8 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -512,17 +512,21 @@ impl FindBar { let editor = editor.read(cx); let position = editor.newest_anchor_selection()?.head(); let ranges = editor.highlighted_ranges_for_type::()?.1; - let buffer = editor.buffer().read(cx).read(cx); - match ranges.binary_search_by(|probe| { - if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { - Ordering::Less - } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { - Ordering::Greater - } else { - Ordering::Equal + if ranges.is_empty() { + None + } else { + let buffer = editor.buffer().read(cx).read(cx); + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), } - }) { - Ok(i) | Err(i) => Some(cmp::min(i, ranges.len().saturating_sub(1))), } } } From 51ec350635015cd7c693fa302a6bd9fa8797612d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 13:52:06 +0100 Subject: [PATCH 34/35] Assign a background to find buttons --- crates/find/src/find.rs | 8 ++++---- crates/zed/assets/themes/_base.toml | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index d393cbd9c8..2bf2e1133d 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -114,12 +114,12 @@ impl View for FindBar { .with_children(self.active_editor.as_ref().and_then(|editor| { let (_, highlighted_ranges) = editor.read(cx).highlighted_ranges_for_type::()?; - let match_ix = cmp::min(self.active_match_index? + 1, highlighted_ranges.len()); - let message = if highlighted_ranges.is_empty() { - "No matches".to_string() + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, highlighted_ranges.len()) } else { - format!("{}/{}", match_ix, highlighted_ranges.len()) + "No matches".to_string() }; + Some( Label::new(message, theme.find.match_index.text.clone()) .contained() diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 69a938dacf..80d9c7cc69 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -330,6 +330,7 @@ background = "$surface.1" extends = "$text.1" padding = { left = 6, right = 6, top = 1, bottom = 1 } corner_radius = 6 +background = "$surface.1" border = { width = 1, color = "$border.0" } margin.left = 1 margin.right = 1 From 3ccbd7726949bd6fef1d2fefc651aac23017902b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Jan 2022 14:06:49 +0100 Subject: [PATCH 35/35] Move match index before the navigation buttons Co-Authored-By: Nathan Sobo --- crates/find/src/find.rs | 14 +++++++------- crates/zed/assets/themes/_base.toml | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 2bf2e1133d..859e9f4923 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -111,6 +111,13 @@ impl View for FindBar { .aligned() .boxed(), ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + ) .with_children(self.active_editor.as_ref().and_then(|editor| { let (_, highlighted_ranges) = editor.read(cx).highlighted_ranges_for_type::()?; @@ -128,13 +135,6 @@ impl View for FindBar { .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(), - ) .contained() .with_style(theme.find.container) .constrained() diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 80d9c7cc69..93019db6e4 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -353,7 +353,6 @@ background = "$surface.2" [find.match_index] extends = "$text.1" padding = 6 -margin.left = 24 [find.editor] max_width = 400