Fix missing TypeScript outline entries and breadcrumbs

Max Brunsfeld created

Change summary

crates/language/Cargo.toml                      |  4 
crates/language/src/buffer.rs                   | 13 ++-
crates/zed/src/languages.rs                     |  2 
crates/zed/src/languages/typescript.rs          | 51 +++++++++++++++++++
crates/zed/src/languages/typescript/outline.scm | 10 +++
5 files changed, 72 insertions(+), 8 deletions(-)

Detailed changes

crates/language/Cargo.toml 🔗

@@ -57,6 +57,6 @@ util = { path = "../util", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"
 rand = "0.8.3"
-tree-sitter-json = "0.19.0"
-tree-sitter-rust = "0.20.0"
+tree-sitter-json = "*"
+tree-sitter-rust = "*"
 unindent = "0.1.7"

crates/language/src/buffer.rs 🔗

@@ -1730,7 +1730,7 @@ impl BufferSnapshot {
             .and_then(|language| language.grammar.as_ref())?;
 
         let mut cursor = QueryCursorHandle::new();
-        cursor.set_byte_range(range);
+        cursor.set_byte_range(range.clone());
         let matches = cursor.matches(
             &grammar.outline_query,
             tree.root_node(),
@@ -1750,7 +1750,10 @@ impl BufferSnapshot {
         let items = matches
             .filter_map(|mat| {
                 let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?;
-                let range = item_node.start_byte()..item_node.end_byte();
+                let item_range = item_node.start_byte()..item_node.end_byte();
+                if item_range.end < range.start || item_range.start > range.end {
+                    return None;
+                }
                 let mut text = String::new();
                 let mut name_ranges = Vec::new();
                 let mut highlight_ranges = Vec::new();
@@ -1808,15 +1811,15 @@ impl BufferSnapshot {
                 }
 
                 while stack.last().map_or(false, |prev_range| {
-                    !prev_range.contains(&range.start) || !prev_range.contains(&range.end)
+                    !prev_range.contains(&item_range.start) || !prev_range.contains(&item_range.end)
                 }) {
                     stack.pop();
                 }
-                stack.push(range.clone());
+                stack.push(item_range.clone());
 
                 Some(OutlineItem {
                     depth: stack.len() - 1,
-                    range: self.anchor_after(range.start)..self.anchor_before(range.end),
+                    range: self.anchor_after(item_range.start)..self.anchor_before(item_range.end),
                     text,
                     highlight_ranges,
                     name_ranges,

crates/zed/src/languages.rs 🔗

@@ -63,7 +63,7 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
     languages
 }
 
-fn language(
+pub(crate) fn language(
     name: &str,
     grammar: tree_sitter::Language,
     lsp_adapter: Option<Arc<dyn LspAdapter>>,

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

@@ -144,3 +144,54 @@ impl LspAdapter for TypeScriptLspAdapter {
         }))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use gpui::MutableAppContext;
+    use unindent::Unindent;
+
+    #[gpui::test]
+    fn test_outline(cx: &mut MutableAppContext) {
+        let language = crate::languages::language(
+            "typescript",
+            tree_sitter_typescript::language_typescript(),
+            None,
+        );
+
+        let text = r#"
+            function a() {
+              // local variables are omitted
+              let a1 = 1;
+              // all functions are included
+              async function a2() {}
+            }
+            // top-level variables are included
+            let b: C
+            function getB() {}
+            // exported variables are included
+            export const d = e;
+        "#
+        .unindent();
+
+        let buffer = cx.add_model(|cx| {
+            language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx)
+        });
+        let outline = buffer.read(cx).snapshot().outline(None).unwrap();
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("function a ( )", 0),
+                ("async function a2 ( )", 1),
+                ("let b", 0),
+                ("function getB ( )", 0),
+                ("const d", 0),
+            ]
+        );
+    }
+}

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

@@ -6,6 +6,10 @@
     "enum" @context
     name: (_) @name) @item
 
+(type_alias_declaration
+    "type" @context
+    name: (_) @name) @item
+
 (function_declaration
     "async"? @context
     "function" @context
@@ -18,6 +22,12 @@
     "interface" @context
     name: (_) @name) @item
 
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (_) @name) @item))
+
 (program
     (lexical_declaration
         ["let" "const"] @context