Cargo.lock 🔗
@@ -3132,6 +3132,7 @@ dependencies = [
"language",
"ordered-float",
"postage",
+ "smol",
"text",
"workspace",
]
Max Brunsfeld created
Cargo.lock | 1
crates/editor/src/multi_buffer.rs | 3
crates/language/src/buffer.rs | 60 +++++-------
crates/language/src/outline.rs | 37 +++++--
crates/language/src/tests.rs | 131 +++++++++++++++++++++++++++-
crates/outline/Cargo.toml | 1
crates/outline/src/outline.rs | 4
crates/zed/languages/rust/outline.scm | 2
8 files changed, 181 insertions(+), 58 deletions(-)
@@ -3132,6 +3132,7 @@ dependencies = [
"language",
"ordered-float",
"postage",
+ "smol",
"text",
"workspace",
]
@@ -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(),
))
@@ -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<Range<usize>> = Default::default();
- let mut id = 0;
+ let mut stack = Vec::<Range<usize>>::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::<Vec<_>>();
@@ -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<T> {
@@ -11,11 +10,10 @@ pub struct Outline<T> {
#[derive(Clone, Debug)]
pub struct OutlineItem<T> {
- pub id: usize,
pub depth: usize,
pub range: Range<T>,
pub text: String,
- pub name_range_in_text: Range<usize>,
+ pub name_ranges: Box<[Range<u32>]>,
}
impl<T> Outline<T> {
@@ -24,10 +22,14 @@ impl<T> Outline<T> {
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::<String>();
StringMatchCandidate {
- string: text.to_string(),
- char_bag: text.into(),
+ char_bag: text.as_str().into(),
+ string: text,
}
})
.collect(),
@@ -35,15 +37,16 @@ impl<T> Outline<T> {
}
}
- pub fn search(&self, query: &str, cx: &AppContext) -> Vec<StringMatch> {
- let mut matches = smol::block_on(fuzzy::match_strings(
+ pub async fn search(&self, query: &str, executor: Arc<Background>) -> Vec<StringMatch> {
+ 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<T> Outline<T> {
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;
@@ -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::<Vec<_>>(),
+ &[
+ ("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<Anchor>,
+ query: &str,
+ cx: &gpui::TestAppContext,
+ ) -> Vec<(&'a str, Vec<usize>)> {
+ 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::<Vec<_>>()
+ }
+}
+
#[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()
}
@@ -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"
@@ -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);
}
@@ -14,7 +14,7 @@
(impl_item
"impl" @context
- trait: (_)? @context
+ trait: (_)? @name
"for"? @context
type: (_) @name) @item