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

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
This commit is contained in:
Emamul Andalib authored and GitHub committed 2026-04-21 00:36:39 +02:00
1 parent b8bec905de
commit f2cead49b1
4 files changed
+246 -42

No files matched your search

+13 -14
View File
@@ -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
[
+13 -14
View File
@@ -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
[
+13 -14
View File
@@ -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
[
+207
View File
@@ -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 [