Change:
Testing Repro: a node in a tree that is fairly deep. a parent node. that there's a slight flicker. Consecutive collapse/expands don't have that, only the first. ]()

Discussion:
*--------------------------------------------------------------------------------------------*/ import { IDragAndDropData } from 'vs/base/browser/dnd'; <del> import { $, addDisposableListener, append, clearNode, createStyleSheet, getWindow, h, hasParentWithClass, isActiveElement } from 'vs/base/browser/dom'; </del> <ins> import { $, append, clearNode, createStyleSheet, getWindow, h, hasParentWithClass, isActiveElement } from 'vs/base/browser/dom'; </ins> import { DomEmitter } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';

import { Emitter, Event, EventBufferer, Relay } from 'vs/base/common/event'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; import { KeyCode } from 'vs/base/common/keyCodes'; <del> import { Disposable, DisposableStore, dispose, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; </del> <ins> import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; </ins> import { clamp } from 'vs/base/common/numbers'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { ISpliceable } from 'vs/base/common/sequence';

private readonly _rootDomNode: HTMLElement; private _previousState: StickyScrollState<T, TFilterData, TRef> | undefined; <del> private _mouseUpTimerDisposable = new MutableDisposable(); </del> constructor( container: HTMLElement,

const templateData = renderer.renderTemplate(stickyElement); renderer.renderElement(nodeCopy, stickyNode.startIndex, templateData, stickyNode.height); <del> const mouseListenerDisposable = this.registerMouseListeners(stickyElement, stickyNode, currentWidgetHeight); </del> // Remove the element from the DOM when state is disposed const disposable = toDisposable(() => { renderer.disposeElement(nodeCopy, stickyNode.startIndex, templateData, stickyNode.height); renderer.disposeTemplate(templateData); <del> mouseListenerDisposable.dispose(); </del> stickyElement.remove(); }); return { element: stickyElement, disposable }; } <del> private registerMouseListeners(stickyElement: HTMLElement, stickyNode: StickyScrollNode<T, TFilterData>, currentWidgetHeight: number): IDisposable { return addDisposableListener(stickyElement, 'mouseup', (e: MouseEvent) => { const isRightClick = e.button === 2; if (isRightClick) { return; } if (isMonacoCustomToggle(e.target as HTMLElement) || isActionItem(e.target as HTMLElement)) { return; } // Timeout 0 ensures that the tree handles the click event first this._mouseUpTimerDisposable.value = disposableTimeout(() => { const elementTop = this.view.getElementTop(stickyNode.startIndex); // We can't rely on the current sticky node's position // because the node might be partially scrolled under the widget const previousStickyNodeBottom = currentWidgetHeight; this.view.scrollTop = elementTop - previousStickyNodeBottom; this.view.setFocus([stickyNode.startIndex]); this.view.setSelection([stickyNode.startIndex]); }, 0); }); } </del> private setVisible(visible: boolean): void { this._rootDomNode.style.display = visible ? 'block' : 'none'; } dispose(): void { <del> this._mouseUpTimerDisposable.dispose(); </del> this._previousState?.dispose(); this._rootDomNode.remove(); }

class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<ITreeNode<T, TFilterData>> { <del> constructor(list: TreeNodeList<T, TFilterData, TRef>, private tree: AbstractTree<T, TFilterData, TRef>) { </del> <ins> constructor( list: TreeNodeList<T, TFilterData, TRef>, private tree: AbstractTree<T, TFilterData, TRef>, private stickyScrollProvider: () => StickyScrollController<T, TFilterData, TRef> | undefined ) { </ins> super(list); }

if (!this.tree.expandOnDoubleClick && e.browserEvent.detail === 2) { return super.onViewPointer(e); } <ins> } else { this.handleStickyScrollMouseEvent(e, node); </ins> } if (node.collapsible && (!isStickyElement || onTwistie)) {

} } <ins> private handleStickyScrollMouseEvent(e: IListMouseEvent<ITreeNode<T, TFilterData>>, node: ITreeNode<T, TFilterData>): void { if (isMonacoCustomToggle(e.browserEvent.target as HTMLElement) || isActionItem(e.browserEvent.target as HTMLElement)) { return; } const stickyScrollController = this.stickyScrollProvider(); if (!stickyScrollController) { throw new Error('Sticky scroll controller not found'); } const nodeIndex = this.list.indexOf(node); const elementScrollTop = this.list.getElementTop(nodeIndex); const elementTargetViewTop = stickyScrollController.nodePositionTopBelowWidget(node); this.tree.scrollTop = elementScrollTop - elementTargetViewTop; this.list.setFocus([nodeIndex]); this.list.setSelection([nodeIndex]); } </ins> protected override onDoubleClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void { const onTwistie = (e.browserEvent.target as HTMLElement).classList.contains('monaco-tl-twistie');

interface ITreeNodeListOptions<T, TFilterData, TRef> extends IListOptions<ITreeNode<T, TFilterData>> { readonly tree: AbstractTree<T, TFilterData, TRef>; <ins> readonly stickyScrollProvider: () => StickyScrollController<T, TFilterData, TRef> | undefined; </ins> } /**

} protected override createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> { <del> return new TreeNodeListMouseController(this, options.tree); </del> <ins> return new TreeNodeListMouseController(this, options.tree, options.stickyScrollProvider); </ins> } override splice(start: number, deleteCount: number, elements: readonly ITreeNode<T, TFilterData>[] = []): void {

this.focus = new Trait(() => this.view.getFocusedElements()[0], _options.identityProvider); this.selection = new Trait(() => this.view.getSelectedElements()[0], _options.identityProvider); this.anchor = new Trait(() => this.view.getAnchorElement(), _options.identityProvider); <del> this.view = new TreeNodeList(_user, container, this.treeDelegate, this.renderers, this.focus, this.selection, this.anchor, { ...asListOptions(() => this.model, _options), tree: this }); </del> <ins> this.view = new TreeNodeList(_user, container, this.treeDelegate, this.renderers, this.focus, this.selection, this.anchor, { ...asListOptions(() => this.model, _options), tree: this, stickyScrollProvider: () => this.stickyScrollController }); </ins> this.model = this.createModel(_user, this.view, _options); onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;