@@ -1275,6 +1275,16 @@ export interface StickyScrollNode<T, TFilterData> {
12751275 readonly endIndex : number ;
12761276 readonly height : number ;
12771277 readonly position : number ;
1278+ readonly sourceNodeEnd : number ;
1279+ readonly sourceNodePartiallyVisible : boolean ;
1280+ readonly hasExplicitHeight : boolean ;
1281+ }
1282+
1283+ export interface IStickyScrollNodeSourceRange {
1284+ readonly start : number ;
1285+ readonly end : number ;
1286+ readonly stickyNodeHeight ?: number ;
1287+ readonly estimated ?: boolean ;
12781288}
12791289
12801290function stickyScrollNodeStateEquals < T , TFilterData > ( node1 : StickyScrollNode < T , TFilterData > , node2 : StickyScrollNode < T , TFilterData > ) {
@@ -1285,7 +1295,10 @@ function stickyScrollNodeEquals<T, TFilterData>(node1: StickyScrollNode<T, TFilt
12851295 return node1 . node . element === node2 . node . element &&
12861296 node1 . startIndex === node2 . startIndex &&
12871297 node1 . height === node2 . height &&
1288- node1 . endIndex === node2 . endIndex ;
1298+ node1 . endIndex === node2 . endIndex &&
1299+ node1 . sourceNodeEnd === node2 . sourceNodeEnd &&
1300+ node1 . sourceNodePartiallyVisible === node2 . sourceNodePartiallyVisible &&
1301+ node1 . hasExplicitHeight === node2 . hasExplicitHeight ;
12891302}
12901303
12911304class StickyScrollState < T , TFilterData , TRef > {
@@ -1499,28 +1512,43 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
14991512 return undefined ;
15001513 }
15011514
1502- if ( nextStickyNode === firstVisibleNodeUnderWidget ) {
1503- if ( ! this . nodeIsUncollapsedParent ( firstVisibleNodeUnderWidget ) ) {
1504- return undefined ;
1505- }
1515+ if ( nextStickyNode === firstVisibleNodeUnderWidget && ! this . nodeIsUncollapsedParent ( firstVisibleNodeUnderWidget ) ) {
1516+ return undefined ;
1517+ }
15061518
1507- if ( this . tree . options . stickyScrollShowOnlyWhenNodeFullyHidden ) {
1508- return undefined ;
1509- }
1519+ const sourceRange = this . getStickyScrollNodeSourceRange ( nextStickyNode ) ;
1520+ if ( ! sourceRange || ! Number . isFinite ( sourceRange . start ) || ! Number . isFinite ( sourceRange . end ) || sourceRange . end <= sourceRange . start ) {
1521+ return undefined ;
1522+ }
1523+ const sourceNodeTop = this . view . getElementTop ( this . getNodeIndex ( nextStickyNode ) ) ;
1524+ const stickyViewportBottom = this . view . scrollTop + stickyNodesHeight ;
1525+ if ( stickyViewportBottom <= sourceNodeTop + sourceRange . start ) {
1526+ return undefined ;
1527+ }
1528+ const height = this . getStickyScrollNodeHeight ( sourceRange , stickyNodesHeight ) ;
1529+ const sourceNodePartiallyVisible = stickyViewportBottom + height < sourceNodeTop + sourceRange . end ;
15101530
1511- if ( this . nodeTopAlignsWithStickyNodesBottom ( firstVisibleNodeUnderWidget , stickyNodesHeight ) ) {
1512- return undefined ;
1513- }
1531+ return this . createStickyScrollNode ( nextStickyNode , stickyNodesHeight , height , sourceRange . end , sourceNodePartiallyVisible , sourceRange . stickyNodeHeight !== undefined ) ;
1532+ }
1533+
1534+ private getStickyScrollNodeSourceRange ( node : ITreeNode < T , TFilterData > ) : IStickyScrollNodeSourceRange | undefined {
1535+ const defaultRange = { start : 0 , end : this . getNodeHeight ( node ) } ;
1536+ const sourceRangeProvider = this . tree . options . stickyScrollNodeSourceRangeProvider ;
1537+ if ( sourceRangeProvider ) {
1538+ return sourceRangeProvider ( node . element , defaultRange ) ;
15141539 }
15151540
1516- return this . createStickyScrollNode ( nextStickyNode , stickyNodesHeight ) ;
1541+ return defaultRange ;
15171542 }
15181543
1519- private nodeTopAlignsWithStickyNodesBottom ( node : ITreeNode < T , TFilterData > , stickyNodesHeight : number ) : boolean {
1520- const nodeIndex = this . getNodeIndex ( node ) ;
1521- const elementTop = this . view . getElementTop ( nodeIndex ) ;
1522- const stickyPosition = stickyNodesHeight ;
1523- return this . view . scrollTop === elementTop - stickyPosition ;
1544+ private getStickyScrollNodeHeight ( sourceRange : IStickyScrollNodeSourceRange , currentStickyNodesHeight : number ) : number {
1545+ const height = this . clampNodeHeight ( sourceRange . stickyNodeHeight ?? sourceRange . end - sourceRange . start ) ;
1546+ if ( ! sourceRange . estimated ) {
1547+ return height ;
1548+ }
1549+
1550+ const availableHeight = Math . max ( 1 , this . view . renderHeight * this . maxWidgetViewRatio - currentStickyNodesHeight ) ;
1551+ return Math . min ( height , availableHeight ) ;
15241552 }
15251553
15261554 private getNodeHeight ( node : ITreeNode < T , TFilterData > ) : number {
@@ -1537,13 +1565,12 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
15371565 return max !== undefined ? Math . min ( height , max ) : height ;
15381566 }
15391567
1540- private createStickyScrollNode ( node : ITreeNode < T , TFilterData > , currentStickyNodesHeight : number ) : StickyScrollNode < T , TFilterData > {
1541- const height = this . clampNodeHeight ( this . getNodeHeight ( node ) ) ;
1568+ private createStickyScrollNode ( node : ITreeNode < T , TFilterData > , currentStickyNodesHeight : number , height : number , sourceNodeEnd : number , sourceNodePartiallyVisible : boolean , hasExplicitHeight : boolean ) : StickyScrollNode < T , TFilterData > {
15421569 const { startIndex, endIndex } = this . getNodeRange ( node ) ;
15431570
15441571 const position = this . calculateStickyNodePosition ( endIndex , currentStickyNodesHeight , height ) ;
15451572
1546- return { node, position, height, startIndex, endIndex } ;
1573+ return { node, position, height, startIndex, endIndex, sourceNodeEnd , sourceNodePartiallyVisible , hasExplicitHeight } ;
15471574 }
15481575
15491576 private getAncestorUnderPrevious ( node : ITreeNode < T , TFilterData > , previousAncestor : ITreeNode < T , TFilterData > | undefined = undefined ) : ITreeNode < T , TFilterData > | undefined {
@@ -1566,23 +1593,8 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
15661593 }
15671594
15681595 private calculateStickyNodePosition ( lastDescendantIndex : number , stickyRowPositionTop : number , stickyNodeHeight : number ) : number {
1569- let lastChildRelativeTop = this . view . getRelativeTop ( lastDescendantIndex ) ;
1570-
1571- // If the last descendant is only partially visible at the top of the view, getRelativeTop() returns null
1572- // In that case, utilize the next node's relative top to calculate the sticky node's position
1573- if ( lastChildRelativeTop === null && this . view . firstVisibleIndex === lastDescendantIndex && lastDescendantIndex + 1 < this . view . length ) {
1574- const nodeHeight = this . view . getElementHeight ( lastDescendantIndex ) ;
1575- const nextNodeRelativeTop = this . view . getRelativeTop ( lastDescendantIndex + 1 ) ;
1576- lastChildRelativeTop = nextNodeRelativeTop ? nextNodeRelativeTop - nodeHeight / this . view . renderHeight : null ;
1577- }
1578-
1579- if ( lastChildRelativeTop === null ) {
1580- return stickyRowPositionTop ;
1581- }
1582-
15831596 const lastChildHeight = this . view . getElementHeight ( lastDescendantIndex ) ;
1584- const topOfLastChild = lastChildRelativeTop * this . view . renderHeight ;
1585- const bottomOfLastChild = topOfLastChild + lastChildHeight ;
1597+ const bottomOfLastChild = this . view . getElementTop ( lastDescendantIndex ) + lastChildHeight - this . view . scrollTop ;
15861598
15871599 if ( stickyRowPositionTop + stickyNodeHeight > bottomOfLastChild && stickyRowPositionTop <= bottomOfLastChild ) {
15881600 return bottomOfLastChild - stickyNodeHeight ;
@@ -1660,7 +1672,10 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
16601672
16611673 let widgetHeight = 0 ;
16621674 for ( let i = 0 ; i < ancestors . length && i < this . stickyScrollMaxItemCount ; i ++ ) {
1663- widgetHeight += this . clampNodeHeight ( this . getNodeHeight ( ancestors [ i ] ) ) ;
1675+ const sourceRange = this . getStickyScrollNodeSourceRange ( ancestors [ i ] ) ;
1676+ if ( sourceRange ) {
1677+ widgetHeight += this . getStickyScrollNodeHeight ( sourceRange , widgetHeight ) ;
1678+ }
16641679 }
16651680 return widgetHeight ;
16661681 }
@@ -1692,6 +1707,15 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
16921707 }
16931708 }
16941709
1710+ refresh ( ) : void {
1711+ this . update ( ) ;
1712+ }
1713+
1714+ rerender ( ) : void {
1715+ this . _widget . rerender ( ) ;
1716+ this . update ( ) ;
1717+ }
1718+
16951719 validateStickySettings ( options : IAbstractTreeOptionsUpdate < T > ) : { stickyScrollMaxItemCount : number } {
16961720 let stickyScrollMaxItemCount = 7 ;
16971721 if ( typeof options . stickyScrollMaxItemCount === 'number' ) {
@@ -1706,6 +1730,7 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
17061730 private readonly _rootDomNode : HTMLElement ;
17071731 private _previousState : StickyScrollState < T , TFilterData , TRef > | undefined ;
17081732 private _previousElements : HTMLElement [ ] = [ ] ;
1733+ private _previousElementHeights : number [ ] = [ ] ;
17091734 private readonly _previousStateDisposables : DisposableStore = new DisposableStore ( ) ;
17101735 get state ( ) : StickyScrollState < T , TFilterData , TRef > | undefined { return this . _previousState ; }
17111736
@@ -1758,6 +1783,7 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
17581783
17591784 // If state has not changed, do nothing
17601785 if ( ( ! wasVisible && ! isVisible ) || ( wasVisible && isVisible && this . _previousState ! . equal ( state ) ) ) {
1786+ this . updateSourceNodeVisibility ( state ) ;
17611787 return ;
17621788 }
17631789
@@ -1767,8 +1793,10 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
17671793 }
17681794
17691795 if ( ! isVisible ) {
1796+ this . updateSourceNodeVisibility ( undefined ) ;
17701797 this . _previousState = undefined ;
17711798 this . _previousElements = [ ] ;
1799+ this . _previousElementHeights = [ ] ;
17721800 this . _previousStateDisposables . clear ( ) ;
17731801 return ;
17741802 }
@@ -1786,9 +1814,28 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
17861814
17871815 this . _previousState = state ;
17881816
1817+ this . updateSourceNodeVisibility ( state ) ;
17891818 this . updateRootHeight ( state ) ;
17901819 }
17911820
1821+ private updateSourceNodeVisibility ( state : StickyScrollState < T , TFilterData , TRef > | undefined ) : void {
1822+ let sourceNodePartiallyVisible = false ;
1823+ for ( let i = 0 ; state && i < state . count ; i ++ ) {
1824+ const stickyNode = state . stickyNodes [ i ] ;
1825+ const stickyElement = this . _previousElements [ i ] ;
1826+ let nodePartiallyVisible = stickyNode . sourceNodePartiallyVisible ;
1827+ if ( stickyElement ) {
1828+ const sourceNodeBottom = this . view . getElementTop ( stickyNode . startIndex ) + stickyNode . sourceNodeEnd ;
1829+ const stickyElementHeight = this . getRenderedNodeHeight ( stickyNode , i ) ;
1830+ const stickyNodeBottom = this . view . scrollTop + stickyNode . position + stickyElementHeight ;
1831+ nodePartiallyVisible = stickyNodeBottom < sourceNodeBottom ;
1832+ }
1833+ stickyElement ?. classList . toggle ( 'source-node-partially-visible' , nodePartiallyVisible ) ;
1834+ sourceNodePartiallyVisible ||= nodePartiallyVisible ;
1835+ }
1836+ this . _rootDomNode . classList . toggle ( 'source-node-partially-visible' , sourceNodePartiallyVisible ) ;
1837+ }
1838+
17921839 private renderState ( state : StickyScrollState < T , TFilterData , TRef > ) : void {
17931840 this . _previousStateDisposables . clear ( ) ;
17941841
@@ -1807,13 +1854,13 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
18071854
18081855 this . _previousElements = elements ;
18091856
1810- // Probe dynamic heights after rendering into DOM
1811- this . probeDynamicHeights ( state , elements ) ;
1857+ this . _previousElementHeights = this . probeDynamicHeights ( state , elements ) ;
18121858 }
18131859
18141860 rerender ( ) : void {
18151861 if ( this . _previousState ) {
18161862 this . renderState ( this . _previousState ) ;
1863+ this . updateSourceNodeVisibility ( this . _previousState ) ;
18171864 this . updateRootHeight ( this . _previousState ) ;
18181865 }
18191866 }
@@ -1824,17 +1871,25 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
18241871
18251872 private getRootHeight ( state : StickyScrollState < T , TFilterData , TRef > ) : number {
18261873 const lastStickyNode = state . stickyNodes [ state . count - 1 ] ;
1827- const lastStickyElement = this . _previousElements [ state . count - 1 ] ;
1828- const lastStickyElementHeight = lastStickyElement ?. offsetHeight ?? lastStickyNode . height ;
1829- return lastStickyNode . position + lastStickyElementHeight ;
1874+ const lastStickyElementHeight = this . getRenderedNodeHeight ( lastStickyNode , state . count - 1 ) ;
1875+ return Math . max ( 0 , lastStickyNode . position + lastStickyElementHeight ) ;
18301876 }
18311877
1832- private probeDynamicHeights ( state : StickyScrollState < T , TFilterData , TRef > , elements : HTMLElement [ ] ) : void {
1878+ private getRenderedNodeHeight ( stickyNode : StickyScrollNode < T , TFilterData > , index : number ) : number {
1879+ return Math . min ( this . _previousElementHeights [ index ] ?? stickyNode . height , stickyNode . height ) ;
1880+ }
1881+
1882+ private probeDynamicHeights ( state : StickyScrollState < T , TFilterData , TRef > , elements : HTMLElement [ ] ) : number [ ] {
18331883 const heightChanges : { index : number ; height : number } [ ] = [ ] ;
1884+ const elementHeights = state . stickyNodes . map ( node => node . height ) ;
18341885
18351886 for ( let i = 0 ; i < state . count ; i ++ ) {
18361887 const stickyNode = state . stickyNodes [ i ] ;
18371888 if ( ! this . treeDelegate . hasDynamicHeight || ! this . treeDelegate . hasDynamicHeight ( stickyNode . node ) ) {
1889+ const measuredHeight = elements [ i ] . offsetHeight ;
1890+ if ( measuredHeight > 0 ) {
1891+ elementHeights [ i ] = measuredHeight ;
1892+ }
18381893 continue ;
18391894 }
18401895
@@ -1850,25 +1905,28 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
18501905 }
18511906 const maxNodeHeight = this . tree . options . stickyScrollMaxNodeHeight ;
18521907 const clampedMeasuredHeight = maxNodeHeight !== undefined ? Math . min ( measuredHeight , maxNodeHeight ) : measuredHeight ;
1908+ const renderedHeight = stickyNode . hasExplicitHeight ? stickyNode . height : clampedMeasuredHeight ;
1909+ elementHeights [ i ] = renderedHeight ;
18531910
18541911 // Always update the sticky element's visual height to match the measured content
18551912 if ( this . tree . options . setRowHeight !== false ) {
1856- element . style . height = `${ clampedMeasuredHeight } px` ;
1913+ element . style . height = `${ renderedHeight } px` ;
18571914 }
18581915 if ( this . tree . options . setRowLineHeight !== false ) {
1859- element . style . lineHeight = `${ clampedMeasuredHeight } px` ;
1916+ element . style . lineHeight = `${ renderedHeight } px` ;
18601917 }
18611918
1862- // Only propagate height increases to the real row — never shrink it,
1863- // since sticky elements may have CSS truncation (e.g. line-clamp).
1864- if ( clampedMeasuredHeight > stickyNode . height ) {
1919+ // A sticky row may represent only part of its source, so never shrink the source row.
1920+ if ( ! stickyNode . hasExplicitHeight && clampedMeasuredHeight > this . view . getElementHeight ( stickyNode . startIndex ) ) {
18651921 heightChanges . push ( { index : stickyNode . startIndex , height : clampedMeasuredHeight } ) ;
18661922 }
18671923 }
18681924
18691925 if ( heightChanges . length > 0 ) {
18701926 this . _onDidChangeHeight . fire ( heightChanges ) ;
18711927 }
1928+
1929+ return elementHeights ;
18721930 }
18731931
18741932 private createElement ( stickyNode : StickyScrollNode < T , TFilterData > , stickyIndex : number , stickyNodesTotal : number ) : { element : HTMLElement ; disposable : IDisposable } {
@@ -1894,6 +1952,7 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
18941952
18951953 stickyElement . classList . add ( 'monaco-tree-sticky-row' ) ;
18961954 stickyElement . classList . add ( 'monaco-list-row' ) ;
1955+ stickyElement . classList . toggle ( 'source-node-partially-visible' , stickyNode . sourceNodePartiallyVisible ) ;
18971956
18981957 stickyElement . setAttribute ( 'data-index' , `${ nodeIndex } ` ) ;
18991958 stickyElement . setAttribute ( 'data-parity' , nodeIndex % 2 === 0 ? 'even' : 'odd' ) ;
@@ -2311,7 +2370,6 @@ export interface IAbstractTreeOptionsUpdate<T> extends ITreeRendererOptions<T> {
23112370 readonly enableStickyScroll ?: boolean ;
23122371 readonly stickyScrollMaxItemCount ?: number ;
23132372 readonly stickyScrollMaxNodeHeight ?: number ;
2314- readonly stickyScrollShowOnlyWhenNodeFullyHidden ?: boolean ;
23152373 readonly paddingTop ?: number ;
23162374}
23172375
@@ -2327,6 +2385,7 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
23272385 readonly findWidgetContainer ?: HTMLElement ;
23282386 readonly defaultFindVisibility ?: TreeVisibility | ( ( e : T ) => TreeVisibility ) ;
23292387 readonly stickyScrollDelegate ?: IStickyScrollDelegate < T , TFilterData > ;
2388+ readonly stickyScrollNodeSourceRangeProvider ?: ( element : T , defaultRange : IStickyScrollNodeSourceRange ) => IStickyScrollNodeSourceRange | undefined ;
23302389 readonly disableExpandOnSpacebar ?: boolean ; // defaults to false
23312390}
23322391
@@ -2909,6 +2968,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
29092968 return this . view . renderHeight ;
29102969 }
29112970
2971+ refreshStickyScroll ( ) : void {
2972+ this . stickyScrollController ?. refresh ( ) ;
2973+ }
2974+
2975+ rerenderStickyScroll ( ) : void {
2976+ this . stickyScrollController ?. rerender ( ) ;
2977+ }
2978+
29122979 get firstVisibleElement ( ) : T | undefined {
29132980 let index = this . view . firstVisibleIndex ;
29142981
0 commit comments