Skip to content

[ty] Add code folding support#23393

Merged
BurntSushi merged 1 commit intomainfrom
ag/code-folding
Feb 19, 2026
Merged

[ty] Add code folding support#23393
BurntSushi merged 1 commit intomainfrom
ag/code-folding

Conversation

@BurntSushi
Copy link
Member

This PR implements the textDocument/foldingRange LSP request,
enabling code folding in editors. We also support tagging each
folding range with its "kind." So for example, this enables one
to ask your editor to "collapse all block comments."

The implementation works by doing a simple AST traversal to identify
"blocks" in a Python program. We also do a line oriented search to
extract ranges that are more difficult to do from the AST: blocks of
comments, blocks of imports and special custom "regions."

Closes astral-sh/ty#2588

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 18, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@ntBre ntBre added the ty Multi-file analysis & type inference label Feb 18, 2026
@AlexWaygood AlexWaygood added the server Related to the LSP server label Feb 18, 2026
@carljm carljm removed their request for review February 18, 2026 23:51
Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(lunch break, will continue with the review after that.)

Comment on lines 98 to 101
fn add_import_ranges(&mut self, stmts: &[Stmt]) {
let mut import_range: Option<TextRange> = None;

for stmt in stmts {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will only create the range for top-level import blocks as it doesn't traverse into the stmt, so the following won't create the range:

def foo():
    import sys
    from math import prod

    def bar():
        pass

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the single visitor with state handling instead? That would also means that we do one pass over the AST instead of multiple.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I made the nested case work without moving state handling into the visitor. I added a TODO to consider moving it into the visitor, but I'm not totally convinced that it would be meaningfully faster. In particular, this is just doing a shallow scan across a sequence of statements (now all sequences of statements to handle the nested case), so it's not re-doing all of the AST visiting work. And if this were moved into the AST visitor, there would need to be more state tracking.

To be clear, I don't feel strong about this. My instincts here might be wrong!

}

/// Add a folding range for a docstring if present at the start of a body.
fn add_docstring_range(&mut self, body: &[Stmt]) {
Copy link
Member

@dhruvmanila dhruvmanila Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not need this function? Because the visitor should visit these string expressions and it already has branch for strings and f-strings. We can expand them to cover bytes and t-strings as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my thought too. But we ideally need to distinguish between "normal" string literals and string literals that serve as documentation. In the latter case, we can attach the FoldingRangeKind::Comment kind. And that enables things like "collapse all comment blocks."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting. Is "collapse all comment blocks" an editor feature? This makes sense but I think I'd find it a bit confusing as to why did a function doc gets collapsed along with other comments but we can iterate on this based on user feedback.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! I'm just realizing that I forgot to post my demo video. Whoops.

Before:

2026-02-18T11.33.01-05.00.mp4

After:

2026-02-18T11.34.17-05.00.mp4

I think I'd be more surprised if "fold all comment blocks" didn't include docstrings personally. But yeah, happy to iterate in response to user feedback.

Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 98 to 101
fn add_import_ranges(&mut self, stmts: &[Stmt]) {
let mut import_range: Option<TextRange> = None;

for stmt in stmts {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the single visitor with state handling instead? That would also means that we do one pass over the AST instead of multiple.

@sharkdp sharkdp removed their request for review February 19, 2026 08:29
@AlexWaygood AlexWaygood removed their request for review February 19, 2026 15:48
This PR implements the `textDocument/foldingRange` LSP request,
enabling code folding in editors. We also support tagging each
folding range with its "kind." So for example, this enables one
to ask your editor to "collapse all block comments."

The implementation works by doing a simple AST traversal to identify
"blocks" in a Python program. We also do a line oriented search to
extract ranges that are more difficult to do from the AST: blocks of
comments, blocks of imports and special custom "regions."

Closes astral-sh/ty#2588
@BurntSushi BurntSushi merged commit 1425c18 into main Feb 19, 2026
46 checks passed
@BurntSushi BurntSushi deleted the ag/code-folding branch February 19, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the LSP server ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server: Code folding support

4 participants