languages: Fix nested object methods missing from outline panel (#50754)

Emamul Andalib created

Shorthand methods (`methodName() {}`) inside nested object literals were
not shown in the Outline panel. The outline query only captured
method_definition when it was a direct child of a variable_declarator's
object, so nested objects (e.g. `deep: { subFn() {} }`) were missed.

Extended the outline query to match method_definition in any object
node, not just top-level variable_declarator objects. Applied the fix to
TypeScript, JavaScript, and TSX outline definitions. Added tests for the
issue reproduction and edge cases.

Supporting evidence:
<img width="1352" height="812" alt="image"
src="https://github.com/user-attachments/assets/64868a70-abd3-4935-9c03-4c809b55262b"
/>

Fixes #48711

Release Notes:

- Fixed nested object methods not appearing in the Outline panel for
JavaScript and TypeScript files

Change summary

crates/grammars/src/javascript/outline.scm |  27 +-
crates/grammars/src/tsx/outline.scm        |  27 +-
crates/grammars/src/typescript/outline.scm |  27 +-
crates/languages/src/typescript.rs         | 207 ++++++++++++++++++++++++
4 files changed, 246 insertions(+), 42 deletions(-)

Detailed changes

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

@@ -144,20 +144,19 @@
       "(" @context
       ")" @context)) @item)
 
-; Object literal methods
-(variable_declarator
-  value: (object
-    (method_definition
-      [
-        "get"
-        "set"
-        "async"
-        "*"
-      ]* @context
-      name: (_) @name
-      parameters: (formal_parameters
-        "(" @context
-        ")" @context)) @item))
+; Object literal methods (including nested objects)
+(object
+  (method_definition
+    [
+      "get"
+      "set"
+      "async"
+      "*"
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item)
 
 (public_field_definition
   [

crates/grammars/src/tsx/outline.scm 🔗

@@ -150,20 +150,19 @@
       "(" @context
       ")" @context)) @item)
 
-; Object literal methods
-(variable_declarator
-  value: (object
-    (method_definition
-      [
-        "get"
-        "set"
-        "async"
-        "*"
-      ]* @context
-      name: (_) @name
-      parameters: (formal_parameters
-        "(" @context
-        ")" @context)) @item))
+; Object literal methods (including nested objects)
+(object
+  (method_definition
+    [
+      "get"
+      "set"
+      "async"
+      "*"
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item)
 
 (public_field_definition
   [

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

@@ -150,20 +150,19 @@
       "(" @context
       ")" @context)) @item)
 
-; Object literal methods
-(variable_declarator
-  value: (object
-    (method_definition
-      [
-        "get"
-        "set"
-        "async"
-        "*"
-      ]* @context
-      name: (_) @name
-      parameters: (formal_parameters
-        "(" @context
-        ")" @context)) @item))
+; Object literal methods (including nested objects)
+(object
+  (method_definition
+    [
+      "get"
+      "set"
+      "async"
+      "*"
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item)
 
 (public_field_definition
   [

crates/languages/src/typescript.rs 🔗

@@ -1087,6 +1087,213 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_outline_with_nested_object_methods(cx: &mut TestAppContext) {
+        for language in [
+            crate::language(
+                "typescript",
+                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+            ),
+            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
+            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
+        ] {
+            let text = r#"
+            // Reproduction from https://github.com/zed-industries/zed/issues/48711
+            const a = {
+              p01: '01',
+              fn01: () => {},
+              fn02() {},
+              deep: {
+                subFn01: () => {},
+                subFn02() {},
+                subP03: '03',
+                deep2: {
+                  subFn01: () => {},
+                  subFn02() {},
+                  subP03: '03',
+                },
+              },
+            };
+
+            // Edge case: async methods in nested objects
+            const b = {
+              async topAsync() {},
+              nested: { async nestedAsync() {} },
+            };
+
+            // Edge case: object literal in function argument
+            foo({ bar() {}, inner: { baz() {} } });
+        "#
+            .unindent();
+
+            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+            cx.run_until_parked();
+            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+
+            let items: Vec<_> = outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect();
+
+            assert_eq!(
+                items,
+                &[
+                    ("const a", 0),
+                    ("p01", 1),
+                    ("fn01", 1),
+                    ("fn02()", 1),
+                    ("deep", 1),
+                    ("subFn01", 2),
+                    ("subFn02()", 2),
+                    ("subP03", 2),
+                    ("deep2", 2),
+                    ("subFn01", 3),
+                    ("subFn02()", 3),
+                    ("subP03", 3),
+                    ("const b", 0),
+                    ("async topAsync()", 1),
+                    ("nested", 1),
+                    ("async nestedAsync()", 2),
+                    ("bar()", 0),
+                    ("inner", 0),
+                    ("baz()", 1),
+                ]
+            );
+        }
+    }
+
+    #[gpui::test]
+    async fn test_outline_with_complex_nested_objects(cx: &mut TestAppContext) {
+        for language in [
+            crate::language(
+                "typescript",
+                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+            ),
+            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
+            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
+        ] {
+            let text = r#"
+            const config = {
+              init() {},
+              destroy() {},
+              api: {
+                baseUrl: "x",
+                fetchData() {},
+                async submitForm() {},
+                errorHandler() {},
+              },
+              features: {
+                auth: {
+                  login() {},
+                  logout() {},
+                  refreshToken() {},
+                },
+                cache: {
+                  get() {},
+                  set() {},
+                  invalidate() {},
+                },
+              },
+              watch: {
+                value() {},
+              },
+              computed: {
+                fullName() {},
+                displayValue() {},
+              },
+            };
+
+            registerPlugin({
+              name: "my-plugin",
+              setup() {},
+              teardown() {},
+              hooks: {
+                beforeMount() {},
+                mounted() {},
+                beforeUnmount() {},
+              },
+            });
+
+            export const store = {
+              state: {},
+              mutations: {
+                setUser() {},
+                clearUser() {},
+              },
+              actions: {
+                async fetchUser() {},
+                logout() {},
+              },
+              getters: {
+                currentUser() {},
+                isAuthenticated() {},
+              },
+            };
+
+            function registerPlugin(_plugin: unknown) {}
+        "#
+            .unindent();
+
+            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+            cx.run_until_parked();
+            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+
+            let items: Vec<_> = outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect();
+
+            assert_eq!(
+                items,
+                &[
+                    ("const config", 0),
+                    ("init()", 1),
+                    ("destroy()", 1),
+                    ("api", 1),
+                    ("baseUrl", 2),
+                    ("fetchData()", 2),
+                    ("async submitForm()", 2),
+                    ("errorHandler()", 2),
+                    ("features", 1),
+                    ("auth", 2),
+                    ("login()", 3),
+                    ("logout()", 3),
+                    ("refreshToken()", 3),
+                    ("cache", 2),
+                    ("get()", 3),
+                    ("set()", 3),
+                    ("invalidate()", 3),
+                    ("watch", 1),
+                    ("value()", 2),
+                    ("computed", 1),
+                    ("fullName()", 2),
+                    ("displayValue()", 2),
+                    ("name", 0),
+                    ("setup()", 0),
+                    ("teardown()", 0),
+                    ("hooks", 0),
+                    ("beforeMount()", 1),
+                    ("mounted()", 1),
+                    ("beforeUnmount()", 1),
+                    ("const store", 0),
+                    ("state", 1),
+                    ("mutations", 1),
+                    ("setUser()", 2),
+                    ("clearUser()", 2),
+                    ("actions", 1),
+                    ("async fetchUser()", 2),
+                    ("logout()", 2),
+                    ("getters", 1),
+                    ("currentUser()", 2),
+                    ("isAuthenticated()", 2),
+                    ("function registerPlugin( )", 0),
+                ]
+            );
+        }
+    }
+
     #[gpui::test]
     async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
         for language in [