Improve TS and JS symbol outline (#39797)

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
This commit is contained in:
Daniel Wargh 2025-10-20 13:22:07 +03:00 committed by GitHub
parent 36210e72af
commit 94c28ba14a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 468 additions and 43 deletions

View file

@ -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

View file

@ -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());

View file

@ -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
; 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
@ -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