Skip to content

feat: Introduce FoldingPreferences foundation and 'includeClosures' compatibility layer#298676

Open
n-gist wants to merge 6 commits intomicrosoft:mainfrom
n-gist:feat-foldingPreferences
Open

feat: Introduce FoldingPreferences foundation and 'includeClosures' compatibility layer#298676
n-gist wants to merge 6 commits intomicrosoft:mainfrom
n-gist:feat-foldingPreferences

Conversation

@n-gist
Copy link
Contributor

@n-gist n-gist commented Mar 2, 2026

This change lays the groundwork for making folding providers aware of user-defined folding preferences.

Closes #3352 and redirects future related issues to language folding providers or to the includeClosures compatibility layer.
Clarifies responsibility for issues related to folding behavior.


Changes

  • editor.foldingPreferences option
    Adds a structured API for specifying user folding preferences.

  • FoldingPreferencesCapabilities interface
    Allows folding providers to indicate which preferences they natively support.

  • RangeProvider update
    Extends the interface to include the new capabilities property.

  • IndentRangeProvider and SyntaxRangeProvider updates
    Adds placeholder capabilities objects to conform to the updated interface.

  • FoldingPreferencesCompatibility layer
    Implements a compatibility layer to adjust folding behavior for preferences not natively supported by providers.

  • FoldingController integration
    Injects the compatibility layer into folding model computation and updates.

  • CompatibilityAdjusterIncludeClosures
    Adds initial support for the includeClosures preference in the compatibility pipeline.


Backward and future compatibility

  • Existing folding behavior remains unchanged unless editor.foldingPreferences is explicitly set.
  • Providers without native support for preferences continue to function through the compatibility layer.
  • When providers declare native support for a preference, the corresponding compatibility layer adjustment is disabled, allowing the provider to take full control of the user preference's effect.

Reasons
tl;dr
Initially, this change aimed to solve #3352. The plan was to add a single setting and implement support for it directly in VS Code built-in language folding providers. CSS was chosen first due to its simple syntax, with blocks always defined by `{ }` braces. However, this immediately became a roadblock, as its implementation resides in a separate repository and would require introducing an agreed-upon API in the https://github.com/microsoft/vscode-css-languageservice.git. While possible in the future, it is not a good starting point for changes.

The next idea was to develop a post-processing layer applied on top of incoming folding regions as a temporary patch for a whitelist of languages whose folding region implementation is suitable for this. This was done, and the plan was to iterate over languages and popular extensions to define the whitelist. The idea was to keep a language on the list until its provider adopted the user setting. However, the list would be large and permanent, since the likelihood of all existing providers supporting the preference is zero. This also adds maintenance costs, as any addition of native support by providers would require removal from the whitelist.

Switching to a blacklist approach, starting from an empty list and adding languages as their folding providers evolve, is a better strategy. But this blacklist should track extensions rather than languages. Additionally, new preferences may be added later. Maintaining separate lists for each preference is not viable. The original plan aimed to safeguard users from incorrect folding adjustments, but it relied on maintaining extensive language or extension lists, which proved to be an overengineered solution. A cleaner approach is to have folding providers be aware of user preferences and indicate which preferences they support. When a provider declares support for a specific preference, the compatibility patch should not be applied, even if the preference is set.

The correct approach seems to be the following:
The responsibility for enabling preference-related compatibility adjustments for providers that lack native support ultimately lies with the user.
No predefined language lists or unnecessary maintenance costs are needed, and such preferences should be marked with * in their descriptions.

Ultimately, the effect is intentionally activated by the user, and if it produces an undesired result, the user can:
a) Not use the preference for the given language.
b) Adapt the code layout.
c) Request native support for the preference from language extension developers, provided VS Code offers an API for that.

After these conclusions, the idea crystallized into the architectural modifications presented here. This explains why the original issue persisted so long despite appearing simple to fix.

includeClosures preference, or why folding provider outputs actually need to be improved

Consider some simple cases and how different providers handle inclusion of closure delimiters in folding regions, comparing css vs html (CSS closures are }; HTML closures are tags like </div>):

examples
.unfolded1 {
    height: 0;
}

.folded1 {
}
<div>
    text
</div>

<div>
</div>

This is perfectly consistent.

.noregion2 {
}
<div>
</div>

Consistent — does not produce folding regions. However:

.unfolded3 {
height:0;}
<div>
text</div>

CSS produces a folding region, while HTML does not — inconsistent across languages, but more significantly:

.unfolded1 {
    height: 0;
}

.folded1 {
}

vs.

.unfolded3 {
height:0;}

.folded3 {

This is inconsistent even within CSS regarding inclusion of the closure delimiter.

This example may represent poor code layout, and it is unclear whether CSS handles this intentionally. However, it indicates that user preference can be meaningful. Current behavior can be mapped to auto and exists for a reason, but the user may want to always include or always exclude the closure from regions.

The current includeClosures compatibility implementation is simple. It adds one line to a region if doing so does not overlap another region's start. Clearly, it does not handle all cases correctly (e.g. case3). However, the compatibility layer cannot provide perfect behavior for all languages and providers; correct behavior is achievable only through native support.


What's next

If this change is confirmed as viable, I would like to try to implement native support for the includeClosures preference in IndentRangeProvider and explore adding it to the TypeScript provider. This would allow folding providers to access user preferences directly and to declare which preferences they natively support.

@aeschli
Copy link
Contributor

aeschli commented Mar 2, 2026

It's already possible for an extension to implement custom folding providers such as a folding provider that also includes the line of the closing }.
I think https://marketplace.visualstudio.com/items?itemName=MohammadBaqer.better-folding already does that.

We have currently no plans to invest in the built-in indentation based folding support.

@n-gist
Copy link
Contributor Author

n-gist commented Mar 3, 2026

@aeschli

It’s true that extensions can implement custom folding providers.

However, in practice this approach has limitations. Better Folding appears to be unmaintained and currently has open issues preventing it from working reliably in recent versions of VS Code. There are also other extensions (e.g. Explicit Folding) that attempt to solve similar problems using pattern-based approaches.

The broader issue is that these extensions replace the language’s native folding provider and implement their own region detection logic. This typically means:

  • Re-implementing folding logic per language
  • Relying on indentation or pattern matching rather than syntax-aware parsing
  • Losing tight integration with language services

As a result, while such extensions can help in specific scenarios, they cannot fully preserve syntax-aware folding behavior across languages and edge cases.

The goal of this PR is different: it does not aim to replace folding providers, but to provide a structured mechanism for providers to become aware of user preferences, while preserving native syntax-aware folding. The compatibility layer exists only as a fallback when providers do not declare native support.

At a higher level, this PR introduces a structured flow for preference-aware folding:

  • A language folding provider produces fold ranges according to its existing logic.

  • If a user explicitly enables a folding preference, that preference becomes part of the folding configuration.

  • The preference is made available to the folding provider.

  • During fold range recomputation, FoldingController passes the produced ranges through a compatibility layer.

  • The compatibility layer checks whether the provider declares native support for the given preference.

    • If native support is declared, the ranges are left untouched.
    • If not, and if a compatibility adjuster exists for that preference, it applies a corresponding post-processing adjustment.
  • If no folding preferences are explicitly set, behavior remains unchanged.

@n-gist
Copy link
Contributor Author

n-gist commented Mar 3, 2026

IndentRangeProvider is not the primary target of this change. I mentioned it only to indicate that I am ready to implement native support for the includeClosures preference there as a follow-up. If such changes are not desired, it may be left without native support.

Update: after testing, it appears that the compatibility layer’s includeClosures: true adjustment already works correctly for IndentRangeProvider, so adding native support there is not strictly necessary. Native handling would mainly avoid an extra ranges copy in this case, but it does not affect the resulting behavior.

@n-gist
Copy link
Contributor Author

n-gist commented Mar 5, 2026

@aeschli, English is not my native language, so I apologize if my explanations were unclear. I would like to try to rephrase the main idea of the PR in case it is still not clear.

This PR is not specifically about the } inclusion case. Its goal is to allow users to express folding preferences in a common, language-agnostic way, which may cover broader scenarios than this single case.

It also introduces the foundation for an API that folding providers can use to consume these preferences in their logic. Existing built-in or extension providers are not required to support them, but the possibility is added. At the moment this part is only partially implemented and represented by placeholders. Completing it would involve passing preferences to providers at instantiation and wiring preference updates, or just passing them along with ranges recomputation requests.

Having a common way to expose user preferences also enables a folding controller compatibility layer to exist. Its purpose is to compensate for providers that do not natively support certain preferences, allowing language-agnostic adjustments to approximate the desired behavior when users explicitly enable them.

Because these adjustments must remain generic and performant, they may not produce perfect results for all languages or layouts. This limitation is made explicit to users by marking such preferences accordingly in their descriptions.

I tried to document the change clearly to keep the design understandable and maintainable, and I’m happy to adjust the approach if needed.

@aeschli
Copy link
Contributor

aeschli commented Mar 5, 2026

I claim there's no language agnostic way of describing what you want. Each language is different. Such a general setting will just cause more confusion than benefits

  • Its doesn't help if only a few providers support it.
  • what if the concept is not existing for that language, or has a different meaning. What if it needs refinement?

That's why I think the right answer is specific settings per folding provider. If you want the feature for typescript folding strategy, ask them to support it (or look into a PR, or come up with your own folding provider) and provide a setting. If you need something similar for HTML, go there. Markdown? Solve it there. Its the same with formatters and linters.

@n-gist
Copy link
Contributor Author

n-gist commented Mar 5, 2026

@aeschli thank you for the feedback. I understand the concerns. The question of whether such architectural improvements are needed is exactly what I was hoping to clarify before completing this work. I would also like to point out that this was done taking into account all your clarifications in #3352 before starting.


Each language is different / such a general setting may cause confusion

That's true. However, the intention of this proposal is not to replace provider-specific logic, but to allow providers to optionally consume a set of generic user preferences if they find them meaningful.

A provider can:

  • ignore a preference entirely, or
  • declare native support for it and interpret it in a language-specific way.

The proposal does not attempt to enforce a single interpretation across languages; it only provides a shared mechanism for expressing user intent.

At the same time, there are concepts that appear across many languages. Closure delimiters are one example, but there are others as well, such as:

  • whether comments should produce folding regions
  • how branching constructs should fold (single region, per branch, or no region)
  • maximum folding depth
  • custom regions

I think it is possible to imagine other aspects. I mentioned several possible future preferences in the interface description IEditorFoldingPreferencesShape

Providers would remain free to interpret or ignore these preferences depending on the language.


It doesn't help if only a few providers support it

Even partial support could still be useful. In some cases it may help even when no providers implement native support.

For example, the includeClosures adjustment implemented in the folding controller compatibility layer can already produce usable results in many cases. This may eliminate the need to modify existing providers or create separate providers just to achieve this behavior.

If modifying provider outputs in this way is not the correct approach, I would be interested to know what the preferred solution would be. Currently there does not seem to be a way to implement a provider that takes another provider’s output and post-processes it.

At the same time, if a provider later decides to support the preference natively, it can declare support for it and the corresponding compatibility adjustment will be skipped.

This also provides an easy path for providers under development to adopt an existing preference instead of introducing a new provider-specific setting.


What if the concept doesn't exist or needs refinement

Preferences are not applied as a single generalized concept; support is granular per preference.

Changing existing preferences would require some care, but adding new values to a preference or introducing new preferences should not break existing implementations. This is a good point, though — it would be worth documenting that preference domains may evolve over time by adding new values, so providers should implement value branching in a way that remains forward-compatible.

I would have considered adding an unknown value to the domains for this reason, but that would likely overlap with existing values and break useful TypeScript hints.


The right answer is specific settings per provider

Provider-specific settings are indeed often the best solution.

However, creating provider-specific settings also has a cost. When a preference can reasonably be expressed in a language-agnostic way, defining it once in VS Code makes it easier for providers to adopt it simply by consuming an existing flag.

This can make feature requests less ad-hoc and more predictable for extension developers, while still leaving them completely free to ignore or decline support if it does not make sense for their language.


If this direction still does not align with the intended architecture of folding in VS Code, I completely understand. My goal with this PR was mainly to explore whether a shared preference mechanism could help reduce duplicated solutions while keeping the responsibility for semantics with folding providers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[folding] Collapse ending brace to the same line

2 participants