Match on names only when outline query has no spaces

Max Brunsfeld , Antonio Scandurra , and Nathan Sobo created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/multi_buffer.rs |  1 
crates/language/src/buffer.rs     | 17 +++++++++
crates/language/src/outline.rs    | 58 +++++++++++++++++++++-----------
crates/language/src/tests.rs      | 25 ++++++++-----
4 files changed, 70 insertions(+), 31 deletions(-)

Detailed changes

crates/editor/src/multi_buffer.rs 🔗

@@ -1712,6 +1712,7 @@ impl MultiBufferSnapshot {
                         ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
                     text: item.text,
                     highlight_ranges: item.highlight_ranges,
+                    name_ranges: item.name_ranges,
                 })
                 .collect(),
         ))

crates/language/src/buffer.rs 🔗

@@ -1864,11 +1864,15 @@ impl BufferSnapshot {
                 let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?;
                 let range = item_node.start_byte()..item_node.end_byte();
                 let mut text = String::new();
+                let mut name_ranges = Vec::new();
                 let mut highlight_ranges = Vec::new();
 
                 for capture in mat.captures {
+                    let node_is_name;
                     if capture.index == name_capture_ix {
+                        node_is_name = true;
                     } else if capture.index == context_capture_ix {
+                        node_is_name = false;
                     } else {
                         continue;
                     }
@@ -1877,6 +1881,18 @@ impl BufferSnapshot {
                     if !text.is_empty() {
                         text.push(' ');
                     }
+                    if node_is_name {
+                        let mut start = text.len();
+                        let end = start + range.len();
+
+                        // When multiple names are captured, then the matcheable text
+                        // includes the whitespace in between the names.
+                        if !name_ranges.is_empty() {
+                            start -= 1;
+                        }
+
+                        name_ranges.push(start..end);
+                    }
 
                     let mut offset = range.start;
                     chunks.seek(offset);
@@ -1911,6 +1927,7 @@ impl BufferSnapshot {
                     range: self.anchor_after(range.start)..self.anchor_before(range.end),
                     text,
                     highlight_ranges,
+                    name_ranges,
                 })
             })
             .collect::<Vec<_>>();

crates/language/src/outline.rs 🔗

@@ -16,44 +16,49 @@ pub struct OutlineItem<T> {
     pub range: Range<T>,
     pub text: String,
     pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
+    pub name_ranges: Vec<Range<usize>>,
 }
 
 impl<T> Outline<T> {
     pub fn new(items: Vec<OutlineItem<T>>) -> Self {
+        let mut candidates = Vec::new();
         let mut path_candidates = Vec::new();
         let mut path_candidate_prefixes = Vec::new();
-        let mut item_text = String::new();
-        let mut stack = Vec::new();
+        let mut path_text = String::new();
+        let mut path_stack = Vec::new();
 
         for (id, item) in items.iter().enumerate() {
-            if item.depth < stack.len() {
-                stack.truncate(item.depth);
-                item_text.truncate(stack.last().copied().unwrap_or(0));
+            if item.depth < path_stack.len() {
+                path_stack.truncate(item.depth);
+                path_text.truncate(path_stack.last().copied().unwrap_or(0));
             }
-            if !item_text.is_empty() {
-                item_text.push(' ');
+            if !path_text.is_empty() {
+                path_text.push(' ');
             }
-            path_candidate_prefixes.push(item_text.len());
-            item_text.push_str(&item.text);
-            stack.push(item_text.len());
+            path_candidate_prefixes.push(path_text.len());
+            path_text.push_str(&item.text);
+            path_stack.push(path_text.len());
+
+            let candidate_text = item
+                .name_ranges
+                .iter()
+                .map(|range| &item.text[range.start as usize..range.end as usize])
+                .collect::<String>();
 
             path_candidates.push(StringMatchCandidate {
                 id,
-                string: item_text.clone(),
-                char_bag: item_text.as_str().into(),
+                char_bag: path_text.as_str().into(),
+                string: path_text.clone(),
+            });
+            candidates.push(StringMatchCandidate {
+                id,
+                char_bag: candidate_text.as_str().into(),
+                string: candidate_text,
             });
         }
 
         Self {
-            candidates: items
-                .iter()
-                .enumerate()
-                .map(|(id, item)| StringMatchCandidate {
-                    id,
-                    char_bag: item.text.as_str().into(),
-                    string: item.text.clone(),
-                })
-                .collect(),
+            candidates,
             path_candidates,
             path_candidate_prefixes,
             items,
@@ -93,6 +98,17 @@ impl<T> Outline<T> {
                 for position in &mut string_match.positions {
                     *position -= prefix_len;
                 }
+            } else {
+                let mut name_ranges = outline_match.name_ranges.iter();
+                let mut name_range = name_ranges.next().unwrap();
+                let mut preceding_ranges_len = 0;
+                for position in &mut string_match.positions {
+                    while *position >= preceding_ranges_len + name_range.len() as usize {
+                        preceding_ranges_len += name_range.len();
+                        name_range = name_ranges.next().unwrap();
+                    }
+                    *position = name_range.start as usize + (*position - preceding_ranges_len);
+                }
             }
 
             let insertion_ix = tree_matches.len();

crates/language/src/tests.rs 🔗

@@ -365,29 +365,34 @@ async fn test_outline(mut cx: gpui::TestAppContext) {
         ]
     );
 
+    // Without space, we only match on names
     assert_eq!(
         search(&outline, "oon", &cx).await,
         &[
-            ("mod module", vec![]),                  // included as the parent of a match
-            ("enum LoginState", vec![]),             // included as the parent of a match
-            ("LoggingOn", vec![1, 7, 8]),            // matches
-            ("impl Eq for Person", vec![9, 16, 17]), // matches part of the context
-            ("impl Drop for Person", vec![11, 18, 19]), // matches in two disjoint names
+            ("mod module", vec![]),                    // included as the parent of a match
+            ("enum LoginState", vec![]),               // included as the parent of a match
+            ("LoggingOn", vec![1, 7, 8]),              // matches
+            ("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
         ]
     );
+
     assert_eq!(
         search(&outline, "dp p", &cx).await,
-        &[("impl Drop for Person", vec![5, 8, 9, 14])]
+        &[
+            ("impl Drop for Person", vec![5, 8, 9, 14]),
+            ("fn drop", vec![]),
+        ]
     );
     assert_eq!(
         search(&outline, "dpn", &cx).await,
-        &[("impl Drop for Person", vec![5, 8, 19])]
+        &[("impl Drop for Person", vec![5, 14, 19])]
     );
     assert_eq!(
-        search(&outline, "impl", &cx).await,
+        search(&outline, "impl ", &cx).await,
         &[
-            ("impl Eq for Person", vec![0, 1, 2, 3]),
-            ("impl Drop for Person", vec![0, 1, 2, 3])
+            ("impl Eq for Person", vec![0, 1, 2, 3, 4]),
+            ("impl Drop for Person", vec![0, 1, 2, 3, 4]),
+            ("fn drop", vec![]),
         ]
     );