Refine project find's UX

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/multi_buffer.rs   |  6 +++
crates/find/src/project_find.rs     | 60 ++++++++++++++++++++----------
crates/sum_tree/src/cursor.rs       | 28 ++++++++-----
crates/sum_tree/src/sum_tree.rs     |  8 ++++
crates/theme/src/theme.rs           |  1 
crates/zed/assets/themes/_base.toml |  1 
6 files changed, 73 insertions(+), 31 deletions(-)

Detailed changes

crates/editor/src/multi_buffer.rs 🔗

@@ -1175,6 +1175,12 @@ impl MultiBuffer {
 
         let mut buffers = Vec::new();
         for _ in 0..mutation_count {
+            if rng.gen_bool(0.05) {
+                log::info!("Clearing multi-buffer");
+                self.clear(cx);
+                continue;
+            }
+
             let excerpt_ids = self
                 .buffers
                 .borrow()

crates/find/src/project_find.rs 🔗

@@ -30,7 +30,7 @@ pub fn init(cx: &mut MutableAppContext) {
 struct ProjectFind {
     project: ModelHandle<Project>,
     excerpts: ModelHandle<MultiBuffer>,
-    pending_search: Task<Option<()>>,
+    pending_search: Option<Task<Option<()>>>,
     highlighted_ranges: Vec<Range<Anchor>>,
 }
 
@@ -55,7 +55,7 @@ impl ProjectFind {
         Self {
             project,
             excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
-            pending_search: Task::ready(None),
+            pending_search: None,
             highlighted_ranges: Default::default(),
         }
     }
@@ -64,7 +64,8 @@ impl ProjectFind {
         let search = self
             .project
             .update(cx, |project, cx| project.search(query, cx));
-        self.pending_search = cx.spawn_weak(|this, mut cx| async move {
+        self.highlighted_ranges.clear();
+        self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
             let matches = search.await;
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
@@ -84,11 +85,13 @@ impl ProjectFind {
                             this.highlighted_ranges.extend(ranges_to_highlight);
                         }
                     });
+                    this.pending_search.take();
                     cx.notify();
                 });
             }
             None
-        });
+        }));
+        cx.notify();
     }
 }
 
@@ -147,13 +150,31 @@ impl View for ProjectFindView {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let model = &self.model.read(cx);
+        let results = if model.highlighted_ranges.is_empty() {
+            let theme = &self.settings.borrow().theme;
+            let text = if self.query_editor.read(cx).text(cx).is_empty() {
+                ""
+            } else if model.pending_search.is_some() {
+                "Searching..."
+            } else {
+                "No results"
+            };
+            Label::new(text.to_string(), theme.find.results_status.clone())
+                .aligned()
+                .contained()
+                .with_background_color(theme.editor.background)
+                .flexible(1., true)
+                .boxed()
+        } else {
+            ChildView::new(&self.results_editor)
+                .flexible(1., true)
+                .boxed()
+        };
+
         Flex::column()
             .with_child(self.render_query_editor(cx))
-            .with_child(
-                ChildView::new(&self.results_editor)
-                    .flexible(1., true)
-                    .boxed(),
-            )
+            .with_child(results)
             .boxed()
     }
 
@@ -265,17 +286,16 @@ impl ProjectFindView {
     }
 
     fn on_model_changed(&mut self, _: ModelHandle<ProjectFind>, cx: &mut ViewContext<Self>) {
-        let theme = &self.settings.borrow().theme.find;
-        self.results_editor.update(cx, |editor, cx| {
-            let model = self.model.read(cx);
-            editor.highlight_ranges::<Self>(
-                model.highlighted_ranges.clone(),
-                theme.match_background,
-                cx,
-            );
-            editor.select_ranges([0..0], Some(Autoscroll::Fit), cx);
-        });
-        cx.focus(&self.results_editor);
+        let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone();
+        if !highlighted_ranges.is_empty() {
+            let theme = &self.settings.borrow().theme.find;
+            self.results_editor.update(cx, |editor, cx| {
+                editor.highlight_ranges::<Self>(highlighted_ranges, theme.match_background, cx);
+                editor.select_ranges([0..0], Some(Autoscroll::Fit), cx);
+            });
+            cx.focus(&self.results_editor);
+        }
+        cx.notify();
     }
 
     fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {

crates/sum_tree/src/cursor.rs 🔗

@@ -34,13 +34,13 @@ where
             stack: ArrayVec::new(),
             position: D::default(),
             did_seek: false,
-            at_end: false,
+            at_end: tree.is_empty(),
         }
     }
 
     fn reset(&mut self) {
         self.did_seek = false;
-        self.at_end = false;
+        self.at_end = self.tree.is_empty();
         self.stack.truncate(0);
         self.position = D::default();
     }
@@ -139,7 +139,7 @@ where
         if self.at_end {
             self.position = D::default();
             self.descend_to_last_item(self.tree, cx);
-            self.at_end = false;
+            self.at_end = self.tree.is_empty();
         } else {
             while let Some(entry) = self.stack.pop() {
                 if entry.index > 0 {
@@ -195,13 +195,15 @@ where
     {
         let mut descend = false;
 
-        if self.stack.is_empty() && !self.at_end {
-            self.stack.push(StackEntry {
-                tree: self.tree,
-                index: 0,
-                position: D::default(),
-            });
-            descend = true;
+        if self.stack.is_empty() {
+            if !self.at_end {
+                self.stack.push(StackEntry {
+                    tree: self.tree,
+                    index: 0,
+                    position: D::default(),
+                });
+                descend = true;
+            }
             self.did_seek = true;
         }
 
@@ -279,6 +281,10 @@ where
         cx: &<T::Summary as Summary>::Context,
     ) {
         self.did_seek = true;
+        if subtree.is_empty() {
+            return;
+        }
+
         loop {
             match subtree.0.as_ref() {
                 Node::Internal {
@@ -298,7 +304,7 @@ where
                     subtree = child_trees.last().unwrap();
                 }
                 Node::Leaf { item_summaries, .. } => {
-                    let last_index = item_summaries.len().saturating_sub(1);
+                    let last_index = item_summaries.len() - 1;
                     for item_summary in &item_summaries[0..last_index] {
                         self.position.add_summary(item_summary, cx);
                     }

crates/sum_tree/src/sum_tree.rs 🔗

@@ -821,6 +821,14 @@ mod tests {
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
         assert_eq!(cursor.start().sum, 0);
+        cursor.prev(&());
+        assert_eq!(cursor.item(), None);
+        assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.start().sum, 0);
+        cursor.next(&());
+        assert_eq!(cursor.item(), None);
+        assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.start().sum, 0);
 
         // Single-element tree
         let mut tree = SumTree::<u8>::new();

crates/theme/src/theme.rs 🔗

@@ -107,6 +107,7 @@ pub struct Find {
     pub active_hovered_option_button: ContainedText,
     pub match_background: Color,
     pub match_index: ContainedText,
+    pub results_status: TextStyle,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/zed/assets/themes/_base.toml 🔗

@@ -351,6 +351,7 @@ tab_summary_spacing = 10
 [find]
 match_background = "$state.highlighted_line"
 background = "$surface.1"
+results_status = { extends = "$text.0", size = 18 }
 
 [find.option_button]
 extends = "$text.1"