diff --git a/Cargo.lock b/Cargo.lock index c02c7943ab..3604004d0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3132,6 +3132,7 @@ dependencies = [ "language", "ordered-float", "postage", + "smol", "text", "workspace", ] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 128cefd49e..05294c1419 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1707,12 +1707,11 @@ impl MultiBufferSnapshot { .items .into_iter() .map(|item| OutlineItem { - id: item.id, depth: item.depth, range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, - name_range_in_text: item.name_range_in_text, + name_ranges: item.name_ranges, }) .collect(), )) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 317cff84b6..e699023d84 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1850,52 +1850,45 @@ impl BufferSnapshot { ); let item_capture_ix = grammar.outline_query.capture_index_for_name("item")?; - let context_capture_ix = grammar.outline_query.capture_index_for_name("context")?; let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?; + let context_capture_ix = grammar + .outline_query + .capture_index_for_name("context") + .unwrap_or(u32::MAX); - let mut stack: Vec> = Default::default(); - let mut id = 0; + let mut stack = Vec::>::new(); let items = matches .filter_map(|mat| { let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?; - let mut name_node = Some(mat.nodes_for_capture_index(name_capture_ix).next()?); - let mut context_nodes = mat.nodes_for_capture_index(context_capture_ix).peekable(); - - let id = post_inc(&mut id); let range = item_node.start_byte()..item_node.end_byte(); - let mut text = String::new(); - let mut name_range_in_text = 0..0; - loop { - let node; + let mut name_ranges = Vec::new(); + + for capture in mat.captures { let node_is_name; - match (context_nodes.peek(), name_node.as_ref()) { - (None, None) => break, - (None, Some(_)) => { - node = name_node.take().unwrap(); - node_is_name = true; - } - (Some(_), None) => { - node = context_nodes.next().unwrap(); - node_is_name = false; - } - (Some(context_node), Some(name)) => { - if context_node.start_byte() < name.start_byte() { - node = context_nodes.next().unwrap(); - node_is_name = false; - } else { - node = name_node.take().unwrap(); - node_is_name = true; - } - } + if capture.index == name_capture_ix { + node_is_name = true; + } else if capture.index == context_capture_ix { + node_is_name = false; + } else { + continue; } + let range = capture.node.start_byte()..capture.node.end_byte(); if !text.is_empty() { text.push(' '); } - let range = node.start_byte()..node.end_byte(); if node_is_name { - name_range_in_text = text.len()..(text.len() + range.len()) + let mut start = text.len() as u32; + let end = start + range.len() as u32; + + // 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); } text.extend(self.text_for_range(range)); } @@ -1908,11 +1901,10 @@ impl BufferSnapshot { stack.push(range.clone()); Some(OutlineItem { - id, depth: stack.len() - 1, range: self.anchor_after(range.start)..self.anchor_before(range.end), text, - name_range_in_text, + name_ranges: name_ranges.into_boxed_slice(), }) }) .collect::>(); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 7d2e47d964..76b7462a97 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -1,7 +1,6 @@ -use std::ops::Range; - use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::AppContext; +use gpui::executor::Background; +use std::{ops::Range, sync::Arc}; #[derive(Debug)] pub struct Outline { @@ -11,11 +10,10 @@ pub struct Outline { #[derive(Clone, Debug)] pub struct OutlineItem { - pub id: usize, pub depth: usize, pub range: Range, pub text: String, - pub name_range_in_text: Range, + pub name_ranges: Box<[Range]>, } impl Outline { @@ -24,10 +22,14 @@ impl Outline { candidates: items .iter() .map(|item| { - let text = &item.text[item.name_range_in_text.clone()]; + let text = item + .name_ranges + .iter() + .map(|range| &item.text[range.start as usize..range.end as usize]) + .collect::(); StringMatchCandidate { - string: text.to_string(), - char_bag: text.into(), + char_bag: text.as_str().into(), + string: text, } }) .collect(), @@ -35,15 +37,16 @@ impl Outline { } } - pub fn search(&self, query: &str, cx: &AppContext) -> Vec { - let mut matches = smol::block_on(fuzzy::match_strings( + pub async fn search(&self, query: &str, executor: Arc) -> Vec { + let mut matches = fuzzy::match_strings( &self.candidates, query, true, 100, &Default::default(), - cx.background().clone(), - )); + executor, + ) + .await; matches.sort_unstable_by_key(|m| m.candidate_index); let mut tree_matches = Vec::new(); @@ -51,8 +54,16 @@ impl Outline { let mut prev_item_ix = 0; for mut string_match in matches { let outline_match = &self.items[string_match.candidate_index]; + + 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 { - *position += outline_match.name_range_in_text.start; + 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 mut cur_depth = outline_match.depth; diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index cf73e8dd23..1622ca92a9 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -278,6 +278,121 @@ async fn test_reparse(mut cx: gpui::TestAppContext) { } } +#[gpui::test] +async fn test_outline(mut cx: gpui::TestAppContext) { + let language = Some(Arc::new( + rust_lang() + .with_outline_query( + r#" + (struct_item + "struct" @context + name: (_) @name) @item + (enum_item + "enum" @context + name: (_) @name) @item + (enum_variant + name: (_) @name) @item + (field_declaration + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_) @name + "for" @context + type: (_) @name) @item + (function_item + "fn" @context + name: (_) @name) @item + "#, + ) + .unwrap(), + )); + + let text = r#" + struct Person { + name: String, + age: usize, + } + + enum LoginState { + LoggedOut, + LoggingOn, + LoggedIn { + person: Person, + time: Instant, + } + } + + impl Drop for Person { + fn drop(&mut self) { + println!("bye"); + } + } + "# + .unindent(); + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let outline = buffer + .read_with(&cx, |buffer, _| buffer.snapshot().outline()) + .unwrap(); + + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.name_ranges.as_ref(), item.depth)) + .collect::>(), + &[ + ("struct Person", [7..13].as_slice(), 0), + ("name", &[0..4], 1), + ("age", &[0..3], 1), + ("enum LoginState", &[5..15], 0), + ("LoggedOut", &[0..9], 1), + ("LoggingOn", &[0..9], 1), + ("LoggedIn", &[0..8], 1), + ("person", &[0..6], 2), + ("time", &[0..4], 2), + ("impl Drop for Person", &[5..9, 13..20], 0), + ("fn drop", &[3..7], 1), + ] + ); + + assert_eq!( + search(&outline, "oon", &cx).await, + &[ + ("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, 13, 14])] + ); + assert_eq!( + search(&outline, "dpn", &cx).await, + &[("impl Drop for Person", vec![5, 8, 19])] + ); + + async fn search<'a>( + outline: &'a Outline, + query: &str, + cx: &gpui::TestAppContext, + ) -> Vec<(&'a str, Vec)> { + let matches = cx + .read(|cx| outline.search(query, cx.background().clone())) + .await; + matches + .into_iter() + .map(|mat| { + ( + outline.items[mat.candidate_index].text.as_str(), + mat.positions, + ) + }) + .collect::>() + } +} + #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { let buffer = cx.add_model(|cx| { @@ -1017,14 +1132,18 @@ fn rust_lang() -> Language { ) .with_indents_query( r#" - (call_expression) @indent - (field_expression) @indent - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, ) .unwrap() - .with_brackets_query(r#" ("{" @open "}" @close) "#) + .with_brackets_query( + r#" + ("{" @open "}" @close) + "#, + ) .unwrap() } diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 80e612bf3b..51c3579228 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -15,3 +15,4 @@ text = { path = "../text" } workspace = { path = "../workspace" } ordered-float = "2.1.1" postage = { version = "0.4", features = ["futures-traits"] } +smol = "1.2" diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index ad57608183..3eb8374a77 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -301,7 +301,7 @@ impl OutlineView { .0; navigate_to_selected_index = false; } else { - self.matches = self.outline.search(&query, cx); + self.matches = smol::block_on(self.outline.search(&query, cx.background().clone())); selected_index = self .matches .iter() @@ -309,7 +309,7 @@ impl OutlineView { .max_by_key(|(_, m)| OrderedFloat(m.score)) .map(|(ix, _)| ix) .unwrap_or(0); - navigate_to_selected_index = true; + navigate_to_selected_index = !self.matches.is_empty(); } self.select(selected_index, navigate_to_selected_index, cx); } diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/languages/rust/outline.scm index bf92b3fdfa..c954acf152 100644 --- a/crates/zed/languages/rust/outline.scm +++ b/crates/zed/languages/rust/outline.scm @@ -14,7 +14,7 @@ (impl_item "impl" @context - trait: (_)? @context + trait: (_)? @name "for"? @context type: (_) @name) @item