Improve TS and JS symbol outline (#39797)

Daniel Wargh created

Added more granular symbols for ts and js in outline panel. This is a
bit closer to what vscode offers.

<details><summary>Screenshots of current vs new</summary>
<p>
New:
<img width="1723" height="1221" alt="image"
src="https://github.com/user-attachments/assets/796d3b59-fffa-4a66-9986-f7c75e618103"
/>

Current:
<img width="1714" height="1347" alt="image"
src="https://github.com/user-attachments/assets/f7cff463-de2a-4d86-b1a6-e19f4fd8dc6e"
/>

Current vscode (cursor):
<img width="1710" height="1177" alt="image"
src="https://github.com/user-attachments/assets/31902d52-becf-4d3f-960d-7e054e00e32d"
/>

</p>
</details> 

I have never touched scheme before, and pair-programmed this with ai, so
please let me know if there's any glaring issues with the
implementation. I just miss the outline panel in vscode very much, and
would love to see this land.
Happy to help with tsx/jsx as well if this is the direction you guys
were thinking of taking the outline impl.

Doesn't fully close https://github.com/zed-industries/zed/issues/20964
as there is no support for chained class method callbacks or
`Class.prototype.method = ...` as mentioned in
https://github.com/zed-industries/zed/issues/21243, but this is a step
forward.

Release Notes:

- Improved typescript and javascript symbol outline panel

Change summary

crates/languages/src/javascript/outline.scm | 146 +++++++++++++--
crates/languages/src/typescript.rs          | 220 ++++++++++++++++++++++
crates/languages/src/typescript/outline.scm | 145 ++++++++++++--
3 files changed, 468 insertions(+), 43 deletions(-)

Detailed changes

crates/languages/src/javascript/outline.scm 🔗

@@ -31,38 +31,103 @@
     (export_statement
         (lexical_declaration
             ["let" "const"] @context
-            ; Multiple names may be exported - @item is on the declarator to keep
-            ; ranges distinct.
             (variable_declarator
-                name: (_) @name) @item)))
+                name: (identifier) @name) @item)))
+
+; Exported array destructuring
+(program
+    (export_statement
+        (lexical_declaration
+            ["let" "const"] @context
+            (variable_declarator
+                name: (array_pattern
+                    [
+                        (identifier) @name @item
+                        (assignment_pattern left: (identifier) @name @item)
+                        (rest_pattern (identifier) @name @item)
+                    ])))))
+
+; Exported object destructuring
+(program
+    (export_statement
+        (lexical_declaration
+            ["let" "const"] @context
+            (variable_declarator
+                name: (object_pattern
+                    [(shorthand_property_identifier_pattern) @name @item
+                     (pair_pattern
+                         value: (identifier) @name @item)
+                     (pair_pattern
+                         value: (assignment_pattern left: (identifier) @name @item))
+                     (rest_pattern (identifier) @name @item)])))))
 
 (program
     (lexical_declaration
         ["let" "const"] @context
-        ; Multiple names may be defined - @item is on the declarator to keep
-        ; ranges distinct.
         (variable_declarator
-            name: (_) @name) @item))
+            name: (identifier) @name) @item))
+
+; Top-level array destructuring
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Top-level object destructuring
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern
+                     value: (identifier) @name @item)
+                 (pair_pattern
+                     value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
 
 (class_declaration
     "class" @context
     name: (_) @name) @item
 
-(method_definition
-    [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-    ]* @context
-    name: (_) @name
-    parameters: (formal_parameters
-      "(" @context
-      ")" @context)) @item
+; Method definitions in classes (not in object literals)
+(class_body
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "readonly"
+            "static"
+            (override_modifier)
+            (accessibility_modifier)
+        ]* @context
+        name: (_) @name
+        parameters: (formal_parameters
+          "(" @context
+          ")" @context)) @item)
+
+; Object literal methods
+(variable_declarator
+    value: (object
+        (method_definition
+            [
+                "get"
+                "set"
+                "async"
+                "*"
+            ]* @context
+            name: (_) @name
+            parameters: (formal_parameters
+              "(" @context
+              ")" @context)) @item))
 
 (public_field_definition
     [
@@ -116,4 +181,43 @@
     )
 ) @item
 
+; Object properties
+(pair
+    key: [
+        (property_identifier) @name
+        (string (string_fragment) @name)
+        (number) @name
+        (computed_property_name) @name
+    ]) @item
+
+; Nested variables in function bodies
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (identifier) @name) @item))
+
+; Nested array destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Nested object destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern value: (identifier) @name @item)
+                 (pair_pattern value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
+
 (comment) @annotation

crates/languages/src/typescript.rs 🔗

@@ -1110,7 +1110,7 @@ mod tests {
 
         let text = r#"
             function a() {
-              // local variables are omitted
+              // local variables are included
               let a1 = 1;
               // all functions are included
               async function a2() {}
@@ -1133,6 +1133,7 @@ mod tests {
                 .collect::<Vec<_>>(),
             &[
                 ("function a()", 0),
+                ("let a1", 1),
                 ("async function a2()", 1),
                 ("let b", 0),
                 ("function getB()", 0),
@@ -1141,6 +1142,223 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_outline_with_destructuring(cx: &mut TestAppContext) {
+        let language = crate::language(
+            "typescript",
+            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+        );
+
+        let text = r#"
+            // Top-level destructuring
+            const { a1, a2 } = a;
+            const [b1, b2] = b;
+
+            // Defaults and rest
+            const [c1 = 1, , c2, ...rest1] = c;
+            const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d;
+
+            function processData() {
+              // Nested object destructuring
+              const { c1, c2 } = c;
+              // Nested array destructuring
+              const [d1, d2, d3] = d;
+              // Destructuring with renaming
+              const { f1: g1 } = f;
+              // With defaults
+              const [x = 10, y] = xy;
+            }
+
+            class DataHandler {
+              method() {
+                // Destructuring in class method
+                const { a1, a2 } = a;
+                const [b1, ...b2] = b;
+              }
+            }
+        "#
+        .unindent();
+
+        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("const a1", 0),
+                ("const a2", 0),
+                ("const b1", 0),
+                ("const b2", 0),
+                ("const c1", 0),
+                ("const c2", 0),
+                ("const rest1", 0),
+                ("const d1", 0),
+                ("const e1", 0),
+                ("const h1", 0),
+                ("const rest2", 0),
+                ("function processData()", 0),
+                ("const c1", 1),
+                ("const c2", 1),
+                ("const d1", 1),
+                ("const d2", 1),
+                ("const d3", 1),
+                ("const g1", 1),
+                ("const x", 1),
+                ("const y", 1),
+                ("class DataHandler", 0),
+                ("method()", 1),
+                ("const a1", 2),
+                ("const a2", 2),
+                ("const b1", 2),
+                ("const b2", 2),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_outline_with_object_properties(cx: &mut TestAppContext) {
+        let language = crate::language(
+            "typescript",
+            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+        );
+
+        let text = r#"
+            // Object with function properties
+            const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} };
+
+            // Object with primitive properties
+            const p = { p1: 1, p2: "hello", p3: true };
+
+            // Nested objects
+            const q = {
+                r: {
+                    // won't be included due to one-level depth limit
+                    s: 1
+                },
+                t: 2
+            };
+
+            function getData() {
+                const local = { x: 1, y: 2 };
+                return local;
+            }
+        "#
+        .unindent();
+
+        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("const o", 0),
+                ("m()", 1),
+                ("async n()", 1),
+                ("g", 1),
+                ("h", 1),
+                ("k", 1),
+                ("const p", 0),
+                ("p1", 1),
+                ("p2", 1),
+                ("p3", 1),
+                ("const q", 0),
+                ("r", 1),
+                ("s", 2),
+                ("t", 1),
+                ("function getData()", 0),
+                ("const local", 1),
+                ("x", 2),
+                ("y", 2),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
+        let language = crate::language(
+            "typescript",
+            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+        );
+
+        let text = r#"
+            // Symbols as object keys
+            const sym = Symbol("test");
+            const obj1 = {
+                [sym]: 1,
+                [Symbol("inline")]: 2,
+                normalKey: 3
+            };
+
+            // Enums as object keys
+            enum Color { Red, Blue, Green }
+
+            const obj2 = {
+                [Color.Red]: "red value",
+                [Color.Blue]: "blue value",
+                regularProp: "normal"
+            };
+
+            // Mixed computed properties
+            const key = "dynamic";
+            const obj3 = {
+                [key]: 1,
+                ["string" + "concat"]: 2,
+                [1 + 1]: 3,
+                static: 4
+            };
+
+            // Nested objects with computed properties
+            const obj4 = {
+                [sym]: {
+                    nested: 1
+                },
+                regular: {
+                    [key]: 2
+                }
+            };
+        "#
+        .unindent();
+
+        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("const sym", 0),
+                ("const obj1", 0),
+                ("[sym]", 1),
+                ("[Symbol(\"inline\")]", 1),
+                ("normalKey", 1),
+                ("enum Color", 0),
+                ("const obj2", 0),
+                ("[Color.Red]", 1),
+                ("[Color.Blue]", 1),
+                ("regularProp", 1),
+                ("const key", 0),
+                ("const obj3", 0),
+                ("[key]", 1),
+                ("[\"string\" + \"concat\"]", 1),
+                ("[1 + 1]", 1),
+                ("static", 1),
+                ("const obj4", 0),
+                ("[sym]", 1),
+                ("nested", 2),
+                ("regular", 1),
+                ("[key]", 2),
+            ]
+        );
+    }
+
     #[gpui::test]
     async fn test_generator_function_outline(cx: &mut TestAppContext) {
         let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());

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

@@ -34,18 +34,64 @@
 (export_statement
     (lexical_declaration
         ["let" "const"] @context
-        ; Multiple names may be exported - @item is on the declarator to keep
-        ; ranges distinct.
         (variable_declarator
-            name: (_) @name) @item))
+            name: (identifier) @name) @item))
 
+; Exported array destructuring
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Exported object destructuring
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern
+                     value: (identifier) @name @item)
+                 (pair_pattern
+                     value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
+
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (identifier) @name) @item))
+
+; Top-level array destructuring
 (program
     (lexical_declaration
         ["let" "const"] @context
-        ; Multiple names may be defined - @item is on the declarator to keep
-        ; ranges distinct.
         (variable_declarator
-            name: (_) @name) @item))
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Top-level object destructuring
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern
+                     value: (identifier) @name @item)
+                 (pair_pattern
+                     value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
 
 (class_declaration
     "class" @context
@@ -56,21 +102,38 @@
     "class" @context
     name: (_) @name) @item
 
-(method_definition
-    [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-    ]* @context
-    name: (_) @name
-    parameters: (formal_parameters
-      "(" @context
-      ")" @context)) @item
+; Method definitions in classes (not in object literals)
+(class_body
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "readonly"
+            "static"
+            (override_modifier)
+            (accessibility_modifier)
+        ]* @context
+        name: (_) @name
+        parameters: (formal_parameters
+          "(" @context
+          ")" @context)) @item)
+
+; Object literal methods
+(variable_declarator
+    value: (object
+        (method_definition
+            [
+                "get"
+                "set"
+                "async"
+                "*"
+            ]* @context
+            name: (_) @name
+            parameters: (formal_parameters
+              "(" @context
+              ")" @context)) @item))
 
 (public_field_definition
     [
@@ -124,4 +187,44 @@
     )
 ) @item
 
+; Object properties
+(pair
+    key: [
+        (property_identifier) @name
+        (string (string_fragment) @name)
+        (number) @name
+        (computed_property_name) @name
+    ]) @item
+
+
+; Nested variables in function bodies
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (identifier) @name) @item))
+
+; Nested array destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Nested object destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern value: (identifier) @name @item)
+                 (pair_pattern value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
+
 (comment) @annotation