Skip to content

[ty] Emit folding ranges from the language server for multi-line block headers. - #24978

Merged
lerebear merged 3 commits into
mainfrom
lerebear/push-pwlqvypsrllr
May 7, 2026
Merged

lerebear merged 3 commits into
mainfrom
lerebear/push-pwlqvypsrllr

Conversation

@lerebear

@lerebear lerebear commented May 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

As the final step of astral-sh/ty#3320, this builds on #24917 to emit folding ranges for multi-line block headers in addition the the header-preserving block body ranges that we emitted previously.

The result is that a method like this one:

def foo(
    x,
    y,
):
    z = x + y
    return z

Will no longer fold like:

def foo(
    x,
    y,
):
    ...

But will instead fold like:

def foo(...): ...

Closes astral-sh/ty#3320.

Approach

  • I suspect it will be helpful to review this diff commit-by-commit.
  • I added a new add_block_header_ranges helper which emits folding ranges that are contained within a block header. That method, and the previous add_block_body_range method, are now both called by a method called add_block_ranges which we use at most callsites1.
  • I also added an add_expression_range helper which ensures that the normal folding ranges that we emit for expressions (e.g. multi-line lists) do not duplicate or overlap with the new folds that we emit for block headers.
    • Note that we will now fold expressions within block headers slightly differently than we do outside them. Within a block header, an the delimiters of an expression will be retained (e.g., x = [...]), whereas outside a block header they will be elided (e.g., x = ...). This was an intentional choice to bias in the direction that I think we should be moving for these new folds: folds are more expressive with delimiters than without. I plan to also adjust the folding behaviour outside of block headers in a follow-up.

Test Plan

Please see added/updated tests.

Footnotes

  1. The remaining callsites also call add_block_header_ranges and add_block_body_range, but do so via add_block_body_range_after_keyword, which does some additional token scanning to account for block bodies that do not have a corresponding AST node. ↩

@astral-sh-bot astral-sh-bot Bot added server Related to the Ruff or ty LSP server ty The ty type checker labels May 3, 2026
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch from b63c191 to 24deab6 Compare May 4, 2026 04:00
@lerebear
lerebear force-pushed the lerebear/push-ouwoxyvtlyks branch from 9929fe5 to fc48147 Compare May 4, 2026 21:50
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch from 24deab6 to a471d29 Compare May 4, 2026 21:50
@lerebear
lerebear force-pushed the lerebear/push-ouwoxyvtlyks branch from fc48147 to 196607d Compare May 4, 2026 21:54
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch 2 times, most recently from 22e1e3a to e7c4370 Compare May 5, 2026 02:57
@codspeed

codspeed Bot commented May 5, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 57 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing lerebear/push-pwlqvypsrllr (0c8981e) with main (f116ee0)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@lerebear
lerebear force-pushed the lerebear/push-ouwoxyvtlyks branch from 196607d to aa1041d Compare May 5, 2026 03:11
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch from e7c4370 to 5c640b9 Compare May 5, 2026 03:11
@lerebear
lerebear marked this pull request as ready for review May 5, 2026 03:33

@MichaReiser MichaReiser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice. I left a few questions where I'm not sure how folding ranges is intended to behave.

Comment thread crates/ty_ide/src/folding_range.rs
Comment thread crates/ty_ide/src/folding_range.rs Outdated
Comment thread crates/ty_ide/src/folding_range.rs Outdated
Comment thread crates/ty_ide/src/folding_range.rs
Comment thread crates/ty_ide/src/folding_range.rs
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch from 5c640b9 to 757f62a Compare May 7, 2026 03:23
@lerebear
lerebear force-pushed the lerebear/push-ouwoxyvtlyks branch from aa1041d to 0f17962 Compare May 7, 2026 03:23
Base automatically changed from lerebear/push-ouwoxyvtlyks to main May 7, 2026 03:39
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch from 757f62a to d3ce232 Compare May 7, 2026 03:41
@lerebear
lerebear force-pushed the lerebear/push-pwlqvypsrllr branch from d3ce232 to 0c8981e Compare May 7, 2026 05:23
@lerebear
lerebear merged commit cdb3fc6 into main May 7, 2026
83 of 84 checks passed
@lerebear
lerebear deleted the lerebear/push-pwlqvypsrllr branch May 7, 2026 06:20
carljm added a commit that referenced this pull request May 8, 2026
* main: (23 commits)
  Lazily initialize `UnionBuilder` when transforming a `Union` (#25050)
  [ty] Speed-up find-references by using multithreading for cross-file searches (#25042)
  Revert "[ty] Lazily initialize `UnionBuilder` when transforming a union type" (#25049)
  [ty] Make text-based find-references pre-filter identifier boundary aware (#25036)
  [ty] Fix cross-file find-references for keyword arguments (#25043)
  [ty] Include TypedDict type context when inferring mixed constructors (#25039)
  [ty] Include TypedDict type context when inferring string keys (#25037)
  [ty] Lazily initialize `UnionBuilder` when transforming a union type (#24929)
  [ty] Skip global search for references if identifier is not externally visible (#25033)
  [ty] Support narrowing on `__class__` checks (#24997)
  [ty] Bump ecosystem-analyzer (#25032)
  [ty] Include error context for overload consistency diagnostics (#24950)
  [ty] Check non-generic overload implementations (#24936)
  Filter overloads based on return type for ParamSpec mapping (#24769)
  [ty] Add versioned parameters for dataclass decorator (#25029)
  [ty] Project reachability constraints before narrowing (#24982)
  [ty] Emit folding ranges from the language server for multi-line block headers. (#24978)
  [ty] Allow ParamSpec specialization through unioned generic classes (#24826)
  [ty] Adjust start of block folding range to preserve visible header for character-precise LSP clients. (#24917)
  [ty] Use `as_i64` when comparing `IntLiteralType` instances (#25023)
  ...
lerebear added a commit that referenced this pull request May 12, 2026
lerebear added a commit that referenced this pull request May 13, 2026
…ine block headers." (#25118)

This reverts #24978, introduced incorrect folding
behaviour in several LSP clients. In so doing, we restore
#24917 as the latest change to
code folding. The trouble with the code we are now reverting was that,
while it is spec-compliant to emit two distinct folding ranges that
share an end/start line, the second range is typically made inaccessible
by the first.

After this PR, the folding ranges we emit from the language server will
preserve a block header in it's entirety, but allow folding of the block
body. In other words, this code:

```py
def foo(
    bar,
    baz,
):
    qux = bar + baz
    return qux
```

Will fold like so:

```py
def foo(
    bar,
    baz,
): ...
```

I will follow this up shortly with a change that allows folding a
function in its entirety: #25113.
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
…k headers. (astral-sh#24978)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->

## Summary

As the final step of astral-sh/ty#3320, this
builds on astral-sh#24917 to emit folding
ranges for multi-line block headers in addition the the
header-preserving block body ranges that we emitted previously.

The result is that a method like this one:

```py
def foo(
    x,
    y,
):
    z = x + y
    return z
```

Will no longer fold like:

```py
def foo(
    x,
    y,
):
    ...
```

But will instead fold like:

```py
def foo(...): ...
```

<!-- What's the purpose of the change? What does it do, and why? -->
Closes astral-sh/ty#3320.

## Approach

- I suspect it will be helpful to review this diff commit-by-commit.
- I added a new `add_block_header_ranges` helper which emits folding
ranges that are contained within a block header. That method, and the
previous `add_block_body_range` method, are now both called by a method
called `add_block_ranges` which we use at most callsites[^1].
- I also added an `add_expression_range` helper which ensures that the
normal folding ranges that we emit for expressions (e.g. multi-line
lists) do not duplicate or overlap with the new folds that we emit for
block headers.
- Note that we will now fold expressions within block headers slightly
differently than we do outside them. Within a block header, an the
delimiters of an expression will be retained (e.g., `x = [...]`),
whereas outside a block header they will be elided (e.g., `x = ...`).
This was an intentional choice to bias in the direction that I think we
should be moving for these new folds: folds are more expressive with
delimiters than without. I plan to also adjust the folding behaviour
outside of block headers in a follow-up.

[^1]: The remaining callsites also call `add_block_header_ranges` and
`add_block_body_range`, but do so via
`add_block_body_range_after_keyword`, which does some additional token
scanning to account for block bodies that do not have a corresponding
AST node.

## Test Plan

Please see added/updated tests.

<!-- How was it tested? -->
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
…ine block headers." (astral-sh#25118)

This reverts astral-sh#24978, introduced incorrect folding
behaviour in several LSP clients. In so doing, we restore
astral-sh#24917 as the latest change to
code folding. The trouble with the code we are now reverting was that,
while it is spec-compliant to emit two distinct folding ranges that
share an end/start line, the second range is typically made inaccessible
by the first.

After this PR, the folding ranges we emit from the language server will
preserve a block header in it's entirety, but allow folding of the block
body. In other words, this code:

```py
def foo(
    bar,
    baz,
):
    qux = bar + baz
    return qux
```

Will fold like so:

```py
def foo(
    bar,
    baz,
): ...
```

I will follow this up shortly with a change that allows folding a
function in its entirety: astral-sh#25113.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the Ruff or ty LSP server ty The ty type checker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Folding ranges start at indentation, hiding block headers in character-precise clients

2 participants