Match full path when query contains spaces

Nathan Sobo and Antonio Scandurra created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

crates/language/src/outline.rs | 49 ++++++++++++++++++++++++++++++++++-
1 file changed, 47 insertions(+), 2 deletions(-)

Detailed changes

crates/language/src/outline.rs 🔗

@@ -6,6 +6,8 @@ use std::{ops::Range, sync::Arc};
 pub struct Outline<T> {
     pub items: Vec<OutlineItem<T>>,
     candidates: Vec<StringMatchCandidate>,
+    path_candidates: Vec<StringMatchCandidate>,
+    path_candidate_prefixes: Vec<usize>,
 }
 
 #[derive(Clone, Debug)]
@@ -18,6 +20,30 @@ pub struct OutlineItem<T> {
 
 impl<T> Outline<T> {
     pub fn new(items: Vec<OutlineItem<T>>) -> Self {
+        let mut path_candidates = Vec::new();
+        let mut path_candidate_prefixes = Vec::new();
+        let mut item_text = String::new();
+        let mut 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_text.is_empty() {
+                item_text.push(' ');
+            }
+            path_candidate_prefixes.push(item_text.len());
+            item_text.push_str(&item.text);
+            stack.push(item_text.len());
+
+            path_candidates.push(StringMatchCandidate {
+                id,
+                string: item_text.clone(),
+                char_bag: item_text.as_str().into(),
+            });
+        }
+
         Self {
             candidates: items
                 .iter()
@@ -28,13 +54,21 @@ impl<T> Outline<T> {
                     string: item.text.clone(),
                 })
                 .collect(),
+            path_candidates,
+            path_candidate_prefixes,
             items,
         }
     }
 
     pub async fn search(&self, query: &str, executor: Arc<Background>) -> Vec<StringMatch> {
+        let query = query.trim_start();
+        let is_path_query = query.contains(' ');
         let mut matches = fuzzy::match_strings(
-            &self.candidates,
+            if is_path_query {
+                &self.path_candidates
+            } else {
+                &self.candidates
+            },
             query,
             true,
             100,
@@ -47,8 +81,19 @@ impl<T> Outline<T> {
         let mut tree_matches = Vec::new();
 
         let mut prev_item_ix = 0;
-        for string_match in matches {
+        for mut string_match in matches {
             let outline_match = &self.items[string_match.candidate_id];
+
+            if is_path_query {
+                let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
+                string_match
+                    .positions
+                    .retain(|position| *position >= prefix_len);
+                for position in &mut string_match.positions {
+                    *position -= prefix_len;
+                }
+            }
+
             let insertion_ix = tree_matches.len();
             let mut cur_depth = outline_match.depth;
             for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]