diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 41018dd1e507a..e7a31e8dcd079 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -225,18 +225,14 @@ interface Collection { class EventCollection implements Collection { - private disposables = new DisposableStore(); + readonly onDidChange: Event; get elements(): T[] { return this._elements; } - constructor(readonly onDidChange: Event, private _elements: T[] = []) { - onDidChange(e => this._elements = e, null, this.disposables); - } - - dispose() { - this.disposables.dispose(); + constructor(onDidChange: Event, private _elements: T[] = []) { + this.onDidChange = Event.forEach(onDidChange, elements => this._elements = elements); } } @@ -249,7 +245,7 @@ class TreeRenderer implements IListRenderer private renderedNodes = new Map, IRenderData>(); private indent: number = TreeRenderer.DefaultIndent; - private _renderIndentGuides: RenderIndentGuides = RenderIndentGuides.None; + private shouldRenderIndentGuides: boolean = false; private renderedIndentGuides = new SetMap, HTMLDivElement>(); private activeIndentNodes = new Set>(); private indentGuidesDisposable: IDisposable = Disposable.None; @@ -279,19 +275,18 @@ class TreeRenderer implements IListRenderer } if (typeof options.renderIndentGuides !== 'undefined') { - const renderIndentGuides = options.renderIndentGuides; + const shouldRenderIndentGuides = options.renderIndentGuides !== RenderIndentGuides.None; - if (renderIndentGuides !== this._renderIndentGuides) { - this._renderIndentGuides = renderIndentGuides; + if (shouldRenderIndentGuides !== this.shouldRenderIndentGuides) { + this.shouldRenderIndentGuides = shouldRenderIndentGuides; + this.indentGuidesDisposable.dispose(); - if (renderIndentGuides) { + if (shouldRenderIndentGuides) { const disposables = new DisposableStore(); this.activeNodes.onDidChange(this._onDidChangeActiveNodes, this, disposables); this.indentGuidesDisposable = disposables; this._onDidChangeActiveNodes(this.activeNodes.elements); - } else { - this.indentGuidesDisposable.dispose(); } } } @@ -384,7 +379,7 @@ class TreeRenderer implements IListRenderer clearNode(templateData.indent); templateData.indentGuidesDisposable.dispose(); - if (this._renderIndentGuides === RenderIndentGuides.None) { + if (!this.shouldRenderIndentGuides) { return; } @@ -424,7 +419,7 @@ class TreeRenderer implements IListRenderer } private _onDidChangeActiveNodes(nodes: ITreeNode[]): void { - if (this._renderIndentGuides === RenderIndentGuides.None) { + if (!this.shouldRenderIndentGuides) { return; } @@ -1001,7 +996,6 @@ class Trait { insertedNodes.forEach(node => dfs(node, insertedNodesVisitor)); const nodes: ITreeNode[] = []; - let silent = true; for (const node of this.nodes) { const id = this.identityProvider.getId(node.element).toString(); @@ -1014,13 +1008,11 @@ class Trait { if (insertedNode) { nodes.push(insertedNode); - } else { - silent = false; } } } - this._set(nodes, silent); + this._set(nodes, true); } private createNodeSet(): Set> { @@ -1228,9 +1220,8 @@ export abstract class AbstractTree implements IDisposable const treeDelegate = new ComposedTreeDelegate>(delegate); const onDidChangeCollapseStateRelay = new Relay>(); - const onDidChangeActiveNodes = new Emitter[]>(); + const onDidChangeActiveNodes = new Relay[]>(); const activeNodes = new EventCollection(onDidChangeActiveNodes.event); - this.disposables.push(activeNodes); this.renderers = renderers.map(r => new TreeRenderer(r, () => this.model, onDidChangeCollapseStateRelay.event, activeNodes, _options)); this.disposables.push(...this.renderers); @@ -1250,24 +1241,32 @@ export abstract class AbstractTree implements IDisposable this.model = this.createModel(user, this.view, _options); onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; - this.model.onDidSplice(e => { + const onDidModelSplice = Event.forEach(this.model.onDidSplice, e => { this.eventBufferer.bufferEvents(() => { this.focus.onDidModelSplice(e); this.selection.onDidModelSplice(e); }); + }); - const set = new Set>(); - - for (const node of this.focus.getNodes()) { - set.add(node); - } + // Active nodes can change when the model changes or when focus or selection change. + // We debouce it with 0 delay since these events may fire in the same stack and we only + // want to run this once. It also doesn't matter if it runs on the next tick since it's only + // a nice to have UI feature. + onDidChangeActiveNodes.input = Event.chain(Event.any(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange)) + .debounce(() => null, 0) + .map(() => { + const set = new Set>(); + + for (const node of this.focus.getNodes()) { + set.add(node); + } - for (const node of this.selection.getNodes()) { - set.add(node); - } + for (const node of this.selection.getNodes()) { + set.add(node); + } - onDidChangeActiveNodes.fire(fromSet(set)); - }, null, this.disposables); + return fromSet(set); + }).event; if (_options.keyboardSupport !== false) { const onKeyDown = Event.chain(this.view.onKeyDown) diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index d09206cd9d926..787b40ec47700 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -271,6 +271,8 @@ export namespace Event { filter(fn: (e: T) => boolean): IChainableEvent; reduce(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent; latch(): IChainableEvent; + debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; + debounce(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable; once(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } @@ -299,6 +301,12 @@ export namespace Event { return new ChainableEvent(latch(this.event)); } + debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; + debounce(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; + debounce(merge: (last: R | undefined, event: T) => R, delay: number = 100, leading = false, leakWarningThreshold?: number): IChainableEvent { + return new ChainableEvent(debounce(this.event, merge, delay, leading, leakWarningThreshold)); + } + on(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[] | DisposableStore) { return this.event(listener, thisArgs, disposables); }