Enable Inner Scopes Option for Sticky Scroll#273312
Enable Inner Scopes Option for Sticky Scroll#273312benvillalobos wants to merge 12 commits intomicrosoft:mainfrom
Conversation
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
scopePreferenceconfiguration option with valuesouterScopesandinnerScopes - Modified sticky scroll rendering logic to shift out oldest scopes when
innerScopesis 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 |
aiday-mar
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
bottomOfElement used to mean "height of the sticky scroll widget", but it's not reliable WRT inner scopes.
- See that
bottomOfElementwas constructed withrange.top+range.height. range.topcomes from here: https://github.com/benvillalobos/vscode/blob/7ae5be88cb262b513ee08f05b4edfc38b60a28eb/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts#L169- Notice that
topstarts at 0, and every recursive call of getCandidateStickyLinesIntersectingFromStickyModel passestop + heightfor the value oftop, whereheightis the lineHeight.
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
- top:
- function2
- top:
19(top + height) - height:
19
- top:
- function3
- top:
38 - height:
19
- top:
- function4 (once this stickies, function1 gets pushed out)
- top:
57 - height:
19
- top:
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
There was a problem hiding this comment.
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.
|
Pushing back as Nov. is a shorter sprint and Dec. is issue grooming. |
Fixes #208406
Changes Made
scopePreferencesetting. Values areinnerScopeandouterScope.stickyScrollControllerto support inner scopesTo Test
Scope Preference: innerScopesDefault Model: foldingProviderModelto help test.Video (Updated 10/31 🎃)
sticky4.mp4
I tested the logic on the following files: