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 993a0cec0e6e6b923d2141195f47674267f7d3a6..b155d9fc3260225cdddae2529a90ce34b16d5f67 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 d99713308ad298421d772e8c2bce1db0540a555b..ac197a7456eb6ae94f27cbcab1ea9847a14de547 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 3bf489df9194ea84de0f2fb3aed40da6250acfc7..e950dc0fcb74cc001cc7123008906b24010761b0 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 68d7424ae4b10b80782234710bc6b3a0bb7c4206..5a87aa6bc13d72eb82c61c1c8a16cbd0c8c3d33c 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 354f0a0f821af81040581a7afedb2eb4652acbc2..2666a329f0613d26711589d3c05b94412845ea90 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 151e3ec0bb3187caea98d0fe0b8f8464baaae80f..b6921ce69f63c18d9cb92df28bbd61af97701de8 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 f359bd85ddbbe1f961df2b8b669d2a1ea3c1a9a1..5e536394e0b61b72dba1bcd470027cc00d0c5187 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 739d07aab11baa2fbdd51310256e696d80042e27..deb19f47b572109cc2aacd43b12ad95769c72043 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 a2b3803b85b5354d6e4266a368f42db936b0c5eb..2b56a023fc2a7dbc423b177503c7117e6c02b044 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 8d3d0f63709b895d720db31a921602bae01a95ca..7c0cb5c91184f0c892a926b5b359fe404b1aaeba 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 162394ed00edd9740b90641bb060ecb471c33491..2d26c33a8aa70e4ec44b358367751a69f6c86f4a 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 48dcb4907bb6f3bce747034252ff8f2a612086a7..86d5271224d80e114eb4885f5f886d597e6c5133 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 9417057bbae7d648420c6d543318318eaaaccd4c..fea28086329023c9aaf8a897ef098b63485b877f 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 0000000000000000000000000000000000000000..94ac64fcf7477d30449dd74fdc1eb37ca8de14ba --- /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 0000000000000000000000000000000000000000..b8949703ffa589e5710d67033940f341f178c1ba --- /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 deb19f47b572109cc2aacd43b12ad95769c72043..95fb7c863ba701e05bdd48f5475d8e19f05451b4 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 21ad632bd2895ab0600a879b1d6834dc17b288d0..5ab0262288e27dc40e055e8790e56addeb43442a 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 59804cf87c1bc8e8a6a8f2ea12c44faabdc10a02..dd658255550ee6295733d9ff47899a2fd6cb2915 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 fea28086329023c9aaf8a897ef098b63485b877f..45fe7ad8e03fd17defbdbeafdcf2aef43d5532ad 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 94ac64fcf7477d30449dd74fdc1eb37ca8de14ba..08805a67d3583272853d93fc6ebe605b7fcdcfa1 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 b8949703ffa589e5710d67033940f341f178c1ba..6c43725a4a62a21560717947d19aa4340d9fc6c0 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 95fb7c863ba701e05bdd48f5475d8e19f05451b4..5cbd9fe559f9908c73f2132d14205a887e5c1868 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 45fe7ad8e03fd17defbdbeafdcf2aef43d5532ad..49533117c135b02eaf198357829d08c2918df07c 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 08805a67d3583272853d93fc6ebe605b7fcdcfa1..16a9b1f82725533b3c3f0366698ca87fa1f1967c 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 6c43725a4a62a21560717947d19aa4340d9fc6c0..929e9ce67b289641286f8a79ee4b5b8631aceae9 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 27a8c79ba3cf4b1b1fc7ea8dca764e66a3de35f1..c57bbf0aedb7a70ee63ca4a920c117d5a34c7b2e 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 f1af1b4ce8beac39d4afc37abb9270c581782f0c..1b1ae4649191a5a482ecf2788ae78c743be0962d 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; - let mut cursors = SmallVec::<[Cursor; 32]>::new(); - - let content_origin = bounds.origin() + layout.text_offset; + 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(); 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 929e9ce67b289641286f8a79ee4b5b8631aceae9..19b105ccf8e959b9c8b3ae1580e095c53ef559b6 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 c3d37b950c86e3dad9e21b92a1e0a538ffe9e3eb..9452204617aec86aaba416123188be000ad47879 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 b818b514a778d2365b93497073fdaa92dbe3aa89..e92715793cc44a66d4b66885fd2399f90ce03749 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 5cbd9fe559f9908c73f2132d14205a887e5c1868..a112213795951cae3607cbb8a72d4d0c487ba892 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 49533117c135b02eaf198357829d08c2918df07c..cce55da25dfc85e0b7491fb3fbd6cbd95dfc4a39 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 16a9b1f82725533b3c3f0366698ca87fa1f1967c..2984f013ecb819491c79e326ecabd859616ee3a4 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 19b105ccf8e959b9c8b3ae1580e095c53ef559b6..41dcbe38b21d885a53217e747e09a6922d1c6d05 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 9452204617aec86aaba416123188be000ad47879..0ea5ebee404211a435edf9241c925635827f9916 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 e92715793cc44a66d4b66885fd2399f90ce03749..0f8326e8384c489747f1ca6a2d074765d5383b6a 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 41dcbe38b21d885a53217e747e09a6922d1c6d05..2b862fc075216a3b02e5fee80a047cf77c6db132 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 0ea5ebee404211a435edf9241c925635827f9916..e4274653e84dc532911c9474cd2bdb4f1c0f8081 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 0f8326e8384c489747f1ca6a2d074765d5383b6a..b52e80e400d827094eb25204a5adea35d2430984 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 c57bbf0aedb7a70ee63ca4a920c117d5a34c7b2e..dc662969dc5a74740f71f2c0dd74a6a26d3477a0 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 466b6e932390fc7b0ae1e1da286fb455f50be634..9a800f9abba9bc63dfa53db2f3a2aae6192ea486 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 2b862fc075216a3b02e5fee80a047cf77c6db132..cea47a4a77073267e9ed43fd53837bbdaaf57ad7 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 4cedfa80d2703f3987bf92aac080db2d98733f48..3d9b7ceb3985f75901bf0de8781cc1117467aa31 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 | 92 +++++++++++++++++++++++++++-------------- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cce55da25dfc85e0b7491fb3fbd6cbd95dfc4a39..a876093e525fd92928d76efb3de58dd3b19a2fb0 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 2984f013ecb819491c79e326ecabd859616ee3a4..cba620f6880970aac825132396d49fcfb8c27f58 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 cea47a4a77073267e9ed43fd53837bbdaaf57ad7..ee70eca204a0731797adbf3d9520bc1d91508d15 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; - } - - 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| { + 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 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 a876093e525fd92928d76efb3de58dd3b19a2fb0..889b620198a3c3240f96c64480e9cc8cf28cc352 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 cba620f6880970aac825132396d49fcfb8c27f58..8efce7024ecf7156aa1c11ec9ec6d171a5b59c15 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 ee70eca204a0731797adbf3d9520bc1d91508d15..a144019f28c307e61f17bdff1e220a201d95d645 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 31efb4991ea73b1b83cc03fbdd6546acb4d126e2..3a2124de95e3a04e1e360dda59fe3e37c62b253d 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 | 173 +++++++++++++++++++--------- crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 6 +- 7 files changed, 137 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 889b620198a3c3240f96c64480e9cc8cf28cc352..e00df6c0d0675eb55543abd409d844ec3474a60d 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 dc662969dc5a74740f71f2c0dd74a6a26d3477a0..146985bcc23ee6b48cda44096c4025d591fdea4e 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 1b1ae4649191a5a482ecf2788ae78c743be0962d..ecff2b93d1c23729db867ba57e4ff3ebec0a9f47 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 8efce7024ecf7156aa1c11ec9ec6d171a5b59c15..eb3d99ce346a2553d6f15f0b7eeadd02353676ca 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 a144019f28c307e61f17bdff1e220a201d95d645..90412fb99b60466120125ca1baeefce916ca2e3a 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 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 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; + let editor = editor.downgrade(); + 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, + ) + }); + }); } } - - 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; - 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 e4274653e84dc532911c9474cd2bdb4f1c0f8081..d9fab286b00f2e78ba3a997c7ef60a2292c818eb 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 b52e80e400d827094eb25204a5adea35d2430984..c88c4bf3d868c4d5426281c2e89f74642ee80335 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 | 52 ++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 90412fb99b60466120125ca1baeefce916ca2e3a..f57c531987fa8742ae95ffb7b41cd86960863807 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())); } + } 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; + } - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + 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 | 28 +++++-- crates/editor/src/element.rs | 4 +- crates/find/Cargo.toml | 6 ++ crates/find/src/find.rs | 150 +++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e00df6c0d0675eb55543abd409d844ec3474a60d..a8e555b42362bfa89a8cc308363fcd4df5d448bd 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 146985bcc23ee6b48cda44096c4025d591fdea4e..5b04e129c4c59844a0127c10daab0dae42be55bc 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(), ), ( - 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 ecff2b93d1c23729db867ba57e4ff3ebec0a9f47..8ef66568970ac5037711d258a8b502352054f62c 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 eb3d99ce346a2553d6f15f0b7eeadd02353676ca..acab695d12c17c3bd6501ac64adf9a4f2f718daf 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 f57c531987fa8742ae95ffb7b41cd86960863807..db2d74a0a9b64cdbcbfbac135ad03ac137e4e2c4 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 5b04e129c4c59844a0127c10daab0dae42be55bc..425d500d3d59cd9b5fcb02c1f51924166a66eede 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 1dafec32c6f080dfe2799513840c14ad26eff28e..97ce05615246cdd236e4a32cf580296bd142d119 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 db2d74a0a9b64cdbcbfbac135ad03ac137e4e2c4..434056b9a96d632f458ea3ba70378aa856610e95 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 434056b9a96d632f458ea3ba70378aa856610e95..fe91af58bede507f4bfdd5a8503660837f4c8b4e 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 d9fab286b00f2e78ba3a997c7ef60a2292c818eb..37414eca2f924784b213e95b780f1bde602f3cab 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 c88c4bf3d868c4d5426281c2e89f74642ee80335..360d5d03e113c15dbf91a354f03280bfafe84abc 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 fe91af58bede507f4bfdd5a8503660837f4c8b4e..dc89f95a5ae9c9587ccb924491f6a03eeb7cd894 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 dc89f95a5ae9c9587ccb924491f6a03eeb7cd894..4096abe03c028a03ab771659db80fe4baf1e0c78 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 4096abe03c028a03ab771659db80fe4baf1e0c78..f1725d4b9629bcd002340ee4f1e7035eff3fd066 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 f1725d4b9629bcd002340ee4f1e7035eff3fd066..6b8a061892f34c4572cf38a52eadfeb3b6a57b46 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 6b8a061892f34c4572cf38a52eadfeb3b6a57b46..6c5aaec8a1c51495a7bc04bad90a7a49e43eed6f 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 425d500d3d59cd9b5fcb02c1f51924166a66eede..0ee4f3191e8bf0f820fddd4f36fe616cc6fb16eb 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 6c5aaec8a1c51495a7bc04bad90a7a49e43eed6f..78a04a2126aedccd6d12b2d3390ec302a1f3ab18 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 0ee4f3191e8bf0f820fddd4f36fe616cc6fb16eb..db0c099c4b8e228eea8c2349e945770c836bda82 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 78a04a2126aedccd6d12b2d3390ec302a1f3ab18..f5cd80fb8b515ab115d8264acd30066c103384ea 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 f5cd80fb8b515ab115d8264acd30066c103384ea..584f701f651e84a97a0760a34515cfbf1eb63389 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 584f701f651e84a97a0760a34515cfbf1eb63389..65a164b96996013a230cbb0cf8eda078db54be8c 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 65a164b96996013a230cbb0cf8eda078db54be8c..f7e82f4c02fc5bc6e471b99928078cb29483c4ae 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 a112213795951cae3607cbb8a72d4d0c487ba892..6721401817d3bdd38ff22da4d71bfdd2dc297361 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 f7e82f4c02fc5bc6e471b99928078cb29483c4ae..47f38ab3f9a837269091b9b362b4c6ba65cbc0ad 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 6721401817d3bdd38ff22da4d71bfdd2dc297361..ac7ea21471c99b4a3d7ca1662f2ca92040452dc5 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 ac7ea21471c99b4a3d7ca1662f2ca92040452dc5..05b7b7e19b433b2db654607a85e53de97b29badb 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 47f38ab3f9a837269091b9b362b4c6ba65cbc0ad..ea2b29a277bb6a7093af6acba53a476e0b8370f4 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 37414eca2f924784b213e95b780f1bde602f3cab..fec42853dcb146360c3211352260d66dd7035c0c 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 360d5d03e113c15dbf91a354f03280bfafe84abc..69a938dacf8d71d27f730ea7084938880bd30a57 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 ea2b29a277bb6a7093af6acba53a476e0b8370f4..d393cbd9c84d6b32cbb6c5ac336a0097253b79f1 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 d393cbd9c84d6b32cbb6c5ac336a0097253b79f1..2bf2e1133df4975cb74bb3f1f78c9c802c35b00a 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 69a938dacf8d71d27f730ea7084938880bd30a57..80d9c7cc697762c874fda9b4db6717de5b8d1d06 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 2bf2e1133df4975cb74bb3f1f78c9c802c35b00a..859e9f4923f1804daf5de5202b59d278de90e61b 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 80d9c7cc697762c874fda9b4db6717de5b8d1d06..93019db6e41218ad611e5b0bdb3f4817816537d9 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