Skip to content

Enable Inner Scopes Option for Sticky Scroll#273312

Open
benvillalobos wants to merge 12 commits intomicrosoft:mainfrom
benvillalobos:bv/sticky-scroll3
Open

Enable Inner Scopes Option for Sticky Scroll#273312
benvillalobos wants to merge 12 commits intomicrosoft:mainfrom
benvillalobos:bv/sticky-scroll3

Conversation

@benvillalobos
Copy link
Member

@benvillalobos benvillalobos commented Oct 25, 2025

Fixes #208406

Changes Made

  • Added a scopePreference setting. Values are innerScope and outerScope.
    • Note on the naming: VS has the same feature, they use the same naming
  • Modified stickyScrollController to support inner scopes

To Test

  1. Settings (UI) > Search "Sticky scroll" > Scope Preference: innerScopes
  2. Consider: Default Model: foldingProviderModel to help test.
  3. Open a file that contains lots of nesting (functions, classes, html nodes, etc.) Optionally, create an html file and use the contents I've provided below.
  4. Scroll around and observe the sticky behavior

Video (Updated 10/31 🎃)

sticky4.mp4

I tested the logic on the following files:

<html>




    <head>



        <h1>



            <h2>



                <h3>
                    
                    <h1>

                        <h2>

                            <h3>

                            </h3>
                        </h2>   
                    </h1>

                </h3>           

                
            </h2>
        </h1>
    </head>

</html>
export function level1() {



    function level2() {


    
        function level3() {
            
            function level4() {


                function level5() {
                    function level6() {
                        function level7() {
                            function level8() {
                                function level9() {
                                    function level10() {
                                        // Deepest level reached
                                    }
                                    level10();
                                }
                                level9();
                            }
                            level8();
                        }
                        level7();
                    }
                    level6();
                }
                level5();
            }
            level4();
        }
        level3();
    }
    level2();
}



@benvillalobos benvillalobos requested review from aiday-mar and Copilot and removed request for Copilot October 25, 2025 18:57
@vs-code-engineering vs-code-engineering bot added this to the October 2025 milestone Oct 25, 2025
@benvillalobos benvillalobos requested a review from Copilot October 25, 2025 19:06
const rebuildFromIndex = this._findIndexToRebuildFrom(previousLineNumbers, this._lineNumbers, rebuildFromIndexCandidate);
const innerScopes = this._editor.getOption(EditorOption.stickyScroll).scopePreference === 'innerScopes';
const indexCandidate = innerScopes ? 0 : rebuildFromIndexCandidate;
const rebuildFromIndex = this._findIndexToRebuildFrom(previousLineNumbers, this._lineNumbers, indexCandidate);
Copy link
Member Author

Choose a reason for hiding this comment

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

This approach prioritizes a minimal diff over performance.
By forcing stickyScrollWidget to re-render every line on each update, there's a perf cost (scoped only to this innerScopes setting). The upside is this avoids a more extensive refactor of the widget’s rendering logic to properly cache rendered lines.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a new scopePreference setting to the sticky scroll feature, allowing users to choose between showing outer scopes (default behavior) or inner scopes when code nesting exceeds the maximum number of sticky lines.

Key changes:

  • Added scopePreference configuration option with values outerScopes and innerScopes
  • Modified sticky scroll rendering logic to shift out oldest scopes when innerScopes is selected and max lines are reached
  • Changed scope visibility calculation to track cumulative widget height instead of individual element positions

Reviewed Changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated no comments.

File Description
src/vs/editor/common/config/editorOptions.ts Added scopePreference configuration option with validation and localization
src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts Updated scope selection logic to support inner scopes by shifting out oldest entries and tracking cumulative height
src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts Modified rebuild index calculation to force full rebuild when inner scopes mode is active

Copy link
Contributor

@aiday-mar aiday-mar left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. I was tesing this PR and saw the following behavior on a nested function in the screen recording below. Notice how the last sticky scroll line appears even before we hit the line. I think there is a bug in the PR.

Screen.Recording.2025-10-29.at.15.46.11.mov

endLineNumbers.push(end + 1);
if (bottomOfElement > bottomOfEndLine) {
lastLineRelativePosition = bottomOfEndLine - bottomOfElement;
stickyWidgetHeight += range.height;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi Ben, thank for the PR. I am not sure I quite understand this logic. I was wondering if you could help me understand it using a diagram?

Copy link
Member Author

@benvillalobos benvillalobos Oct 31, 2025

Choose a reason for hiding this comment

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

bottomOfElement used to mean "height of the sticky scroll widget", but it's not reliable WRT inner scopes.

Say we have a consistent lineHeight of 19, 4 functions scoped inside of each other, and a max sticky count of 3

  • function1
    • top: 0
    • height: 19
  • function2
    • top: 19 (top + height)
    • height: 19
  • function3
    • top: 38
    • height: 19
  • function4 (once this stickies, function1 gets pushed out)
    • top: 57
    • height: 19

So the bottomOfElement for function4 will be 76, but the height of the sticky scroll widget is actually 57 because we don't append a 4th line. So bottomOfElement no longer represents "height of the sticky scroll widget" like it used to.

So the solution is to dynamically figure out how large the sticky scroll widget is by adding / subtracting to stickyWidgetHeight as we append / remove sticky lines.

getCandidateStickyLinesIntersectingFromStickyModel should probably be refactored to remove the top param. IIRC that param's use case was specifically meant for sticky scroll, so its probably a safe removal

Copy link
Contributor

@aiday-mar aiday-mar Nov 3, 2025

Choose a reason for hiding this comment

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

Hi thanks for the comment. I don't think bottomOfElement represents the height of the sticky scroll widget. The variable bottomOfElement and topOfElement are used to find the theoretical top and bottom of the sticky scroll lines relative to the top of the viewport if the sticky scroll widget did not have a cropped height. These values are compared to the actual position of the top and bottom of the corresponding editor lines, to determine if the candidate sticky lines should be stuck to the top or not. In effect, the comparison is a hypothetical comparison which is used to determine whether the line should be stuck or not.

The reason we don't use the widget height directly is because we can not know the widget height until all the lines that are stuck are computed. It's a recursive problem. To better understand, consider lines 1, 2 and 3 which are nested within each other as follows:

line 1: function f1 () {
line 2: function f2 () {
line 3: function f3 () {
line 4: }}}

If we add line 2 to sticky scroll then according to your calculation the widget would have height 38 (line1 + line2). But actually as soon as you add line 2, the widget becomes larger and it actually now touches line 3. So the truth is that the widget height at this point would be 57 (line1 + line2 + line3) not height 38 as was initially hypothesized. In essence adding lines modulates recursively the sticky scroll widget height depending on where the next lines are situated. We can only know the height at the very end.

Therefore I think we need to continue using the variables bottomOfElement and topOfElement to do the required computation you need.

@benvillalobos
Copy link
Member Author

Pushing back as Nov. is a shorter sprint and Dec. is issue grooming.

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.

Option for sticky scroll to prioritize inner blocks

3 participants