Show function parameters in elixir outline view

Max Brunsfeld created

Introduce a new capture in the outline query called 'context.extra', which
causes text to appear in the outline, but not in the breadcrumbs.

Change summary

crates/language/src/buffer.rs               | 13 +++++-
crates/language/src/buffer_tests.rs         | 46 +++++++++++++++++++++++
crates/language/src/language.rs             |  4 ++
crates/zed/src/languages/elixir/indents.scm |  4 -
crates/zed/src/languages/elixir/outline.scm | 14 ++++++-
crates/zed/src/languages/typescript.rs      |  6 +-
6 files changed, 76 insertions(+), 11 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -2253,7 +2253,7 @@ impl BufferSnapshot {
     }
 
     pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
-        self.outline_items_containing(0..self.len(), theme)
+        self.outline_items_containing(0..self.len(), true, theme)
             .map(Outline::new)
     }
 
@@ -2265,6 +2265,7 @@ impl BufferSnapshot {
         let position = position.to_offset(self);
         let mut items = self.outline_items_containing(
             position.saturating_sub(1)..self.len().min(position + 1),
+            false,
             theme,
         )?;
         let mut prev_depth = None;
@@ -2279,6 +2280,7 @@ impl BufferSnapshot {
     fn outline_items_containing(
         &self,
         range: Range<usize>,
+        include_extra_context: bool,
         theme: Option<&SyntaxTheme>,
     ) -> Option<Vec<OutlineItem<Anchor>>> {
         let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
@@ -2313,7 +2315,10 @@ impl BufferSnapshot {
                 let node_is_name;
                 if capture.index == config.name_capture_ix {
                     node_is_name = true;
-                } else if Some(capture.index) == config.context_capture_ix {
+                } else if Some(capture.index) == config.context_capture_ix
+                    || (Some(capture.index) == config.extra_context_capture_ix
+                        && include_extra_context)
+                {
                     node_is_name = false;
                 } else {
                     continue;
@@ -2340,10 +2345,12 @@ impl BufferSnapshot {
                 buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
                 true,
             );
+            let mut last_buffer_range_end = 0;
             for (buffer_range, is_name) in buffer_ranges {
-                if !text.is_empty() {
+                if !text.is_empty() && buffer_range.start > last_buffer_range_end {
                     text.push(' ');
                 }
+                last_buffer_range_end = buffer_range.end;
                 if is_name {
                     let mut start = text.len();
                     let end = start + buffer_range.len();

crates/language/src/buffer_tests.rs 🔗

@@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
+    let language = javascript_lang()
+        .with_outline_query(
+            r#"
+            (function_declaration
+                "function" @context
+                name: (_) @name
+                parameters: (formal_parameters
+                    "(" @context.extra
+                    ")" @context.extra)) @item
+            "#,
+        )
+        .unwrap();
+
+    let text = r#"
+        function a() {}
+        function b(c) {}
+    "#
+    .unindent();
+
+    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
+    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+    // extra context nodes are included in the outline.
+    let outline = snapshot.outline(None).unwrap();
+    assert_eq!(
+        outline
+            .items
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("function a()", 0), ("function b( )", 0),]
+    );
+
+    // extra context nodes do not appear in breadcrumbs.
+    let symbols = snapshot.symbols_containing(3, None).unwrap();
+    assert_eq!(
+        symbols
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("function a", 0)]
+    );
+}
+
 #[gpui::test]
 async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
     let text = r#"

crates/language/src/language.rs 🔗

@@ -455,6 +455,7 @@ struct OutlineConfig {
     item_capture_ix: u32,
     name_capture_ix: u32,
     context_capture_ix: Option<u32>,
+    extra_context_capture_ix: Option<u32>,
 }
 
 struct InjectionConfig {
@@ -1091,12 +1092,14 @@ impl Language {
         let mut item_capture_ix = None;
         let mut name_capture_ix = None;
         let mut context_capture_ix = None;
+        let mut extra_context_capture_ix = None;
         get_capture_indices(
             &query,
             &mut [
                 ("item", &mut item_capture_ix),
                 ("name", &mut name_capture_ix),
                 ("context", &mut context_capture_ix),
+                ("context.extra", &mut extra_context_capture_ix),
             ],
         );
         if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
@@ -1105,6 +1108,7 @@ impl Language {
                 item_capture_ix,
                 name_capture_ix,
                 context_capture_ix,
+                extra_context_capture_ix,
             });
         }
         Ok(self)

crates/zed/src/languages/elixir/outline.scm 🔗

@@ -8,9 +8,19 @@
   (arguments
     [
       (identifier) @name
-      (call target: (identifier) @name)
+      (call
+          target: (identifier) @name
+          (arguments
+              "(" @context.extra
+              _* @context.extra
+              ")" @context.extra))
       (binary_operator
-        left: (call target: (identifier) @name)
+        left: (call
+            target: (identifier) @name
+            (arguments
+                "(" @context.extra
+                _* @context.extra
+                ")" @context.extra))
         operator: "when")
     ])
   (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item

crates/zed/src/languages/typescript.rs 🔗

@@ -327,10 +327,10 @@ mod tests {
                 .map(|item| (item.text.as_str(), item.depth))
                 .collect::<Vec<_>>(),
             &[
-                ("function a ( )", 0),
-                ("async function a2 ( )", 1),
+                ("function a()", 0),
+                ("async function a2()", 1),
                 ("let b", 0),
-                ("function getB ( )", 0),
+                ("function getB()", 0),
                 ("const d", 0),
             ]
         );