Add buffer method for getting the symbols containing a position

Max Brunsfeld created

Change summary

crates/language/src/buffer.rs  |  33 ++++++-
crates/language/src/outline.rs |   2 
crates/language/src/tests.rs   | 149 ++++++++++++++++++++++++++++-------
3 files changed, 146 insertions(+), 38 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -1667,6 +1667,31 @@ impl BufferSnapshot {
     }
 
     pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
+        self.outline_items_containing(0..self.len(), theme)
+            .map(Outline::new)
+    }
+
+    pub fn symbols_containing<T: ToOffset>(
+        &self,
+        position: T,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<Vec<OutlineItem<Anchor>>> {
+        let position = position.to_offset(&self);
+        let mut items = self.outline_items_containing(position - 1..position + 1, theme)?;
+        let mut prev_depth = None;
+        items.retain(|item| {
+            let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
+            prev_depth = Some(item.depth);
+            result
+        });
+        Some(items)
+    }
+
+    fn outline_items_containing(
+        &self,
+        range: Range<usize>,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<Vec<OutlineItem<Anchor>>> {
         let tree = self.tree.as_ref()?;
         let grammar = self
             .language
@@ -1674,6 +1699,7 @@ impl BufferSnapshot {
             .and_then(|language| language.grammar.as_ref())?;
 
         let mut cursor = QueryCursorHandle::new();
+        cursor.set_byte_range(range);
         let matches = cursor.matches(
             &grammar.outline_query,
             tree.root_node(),
@@ -1766,12 +1792,7 @@ impl BufferSnapshot {
                 })
             })
             .collect::<Vec<_>>();
-
-        if items.is_empty() {
-            None
-        } else {
-            Some(Outline::new(items))
-        }
+        Some(items)
     }
 
     pub fn enclosing_bracket_ranges<T: ToOffset>(

crates/language/src/outline.rs 🔗

@@ -10,7 +10,7 @@ pub struct Outline<T> {
     path_candidate_prefixes: Vec<usize>,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct OutlineItem<T> {
     pub depth: usize,
     pub range: Range<T>,

crates/language/src/tests.rs 🔗

@@ -282,36 +282,6 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_outline(cx: &mut gpui::TestAppContext) {
-    let language = 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
-                (mod_item
-                    "mod" @context
-                    name: (_) @name) @item
-                "#,
-            )
-            .unwrap(),
-    );
-
     let text = r#"
         struct Person {
             name: String,
@@ -339,7 +309,8 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
     let outline = buffer
         .read_with(cx, |buffer, _| buffer.snapshot().outline(None))
         .unwrap();
@@ -413,6 +384,93 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
     }
 }
 
+#[gpui::test]
+async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
+    let text = r#"
+        impl Person {
+            fn one() {
+                1
+            }
+
+            fn two() {
+                2
+            }fn three() {
+                3
+            }
+        }
+    "#
+    .unindent();
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
+    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+    // point is at the start of an item
+    assert_eq!(
+        symbols_containing(Point::new(1, 4), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is in the middle of an item
+    assert_eq!(
+        symbols_containing(Point::new(2, 8), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is at the end of an item
+    assert_eq!(
+        symbols_containing(Point::new(3, 5), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is in between two adjacent items
+    assert_eq!(
+        symbols_containing(Point::new(7, 5), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
+        ]
+    );
+
+    fn symbols_containing<'a>(
+        position: Point,
+        snapshot: &'a BufferSnapshot,
+    ) -> Vec<(String, Range<Point>)> {
+        snapshot
+            .symbols_containing(position, None)
+            .unwrap()
+            .into_iter()
+            .map(|item| {
+                (
+                    item.text,
+                    item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
+                )
+            })
+            .collect()
+    }
+}
+
 #[gpui::test]
 fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
     let buffer = cx.add_model(|cx| {
@@ -851,6 +909,35 @@ fn rust_lang() -> Language {
         "#,
     )
     .unwrap()
+    .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
+            type: (_) @name) @item
+        (impl_item
+            "impl" @context
+            trait: (_) @name
+            "for" @context
+            type: (_) @name) @item
+        (function_item
+            "fn" @context
+            name: (_) @name) @item
+        (mod_item
+            "mod" @context
+            name: (_) @name) @item
+        "#,
+    )
+    .unwrap()
 }
 
 fn empty(point: Point) -> Range<Point> {