From 7cc24e4694045bdaf9aa2a5c8125d2d16a3470f9 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 26 Oct 2023 02:24:10 -0400 Subject: [PATCH] Fixes view node memory leaks - Adds weakEvent function to subscribe to events that will be disposed on GC - Reworks children handling and dispose across many view nodes --- src/system/event.ts | 24 ++++ src/views/nodes/autolinkedItemsNode.ts | 23 +--- src/views/nodes/branchNode.ts | 36 ++++-- src/views/nodes/branchesNode.ts | 17 ++- src/views/nodes/commitNode.ts | 37 ++++-- src/views/nodes/compareBranchNode.ts | 27 ++--- src/views/nodes/compareResultsNode.ts | 32 +++--- src/views/nodes/contributorsNode.ts | 27 ++--- src/views/nodes/fileHistoryNode.ts | 20 ++-- src/views/nodes/fileHistoryTrackerNode.ts | 35 +++--- src/views/nodes/lineHistoryNode.ts | 6 +- src/views/nodes/lineHistoryTrackerNode.ts | 45 ++++---- src/views/nodes/reflogNode.ts | 22 ++-- src/views/nodes/remotesNode.ts | 17 ++- src/views/nodes/repositoriesNode.ts | 64 ++++------- src/views/nodes/repositoryNode.ts | 37 +++--- src/views/nodes/stashesNode.ts | 17 ++- src/views/nodes/tagsNode.ts | 17 ++- src/views/nodes/viewNode.ts | 131 ++++++++++++++-------- src/views/nodes/workspaceNode.ts | 65 ++++------- src/views/nodes/worktreeNode.ts | 25 ++--- src/views/nodes/worktreesNode.ts | 17 ++- src/views/searchAndCompareView.ts | 37 ++++-- src/views/viewBase.ts | 61 ++++++++++ src/views/workspacesView.ts | 11 +- 25 files changed, 486 insertions(+), 364 deletions(-) diff --git a/src/system/event.ts b/src/system/event.ts index e3d9120f74161..7395ed0b247b9 100644 --- a/src/system/event.ts +++ b/src/system/event.ts @@ -103,3 +103,27 @@ export function promisifyDeferred( cancel: () => cancel?.(), }; } + +export function weakEvent( + event: Event, + listener: (e: T) => any, + thisArg: U, + disposables?: Disposable[], +): Disposable { + const ref = new WeakRef(thisArg); + + const disposable = event( + (e: T) => { + const obj = ref.deref(); + if (obj != null) { + listener.call(obj, e); + } else { + disposable.dispose(); + } + }, + null, + disposables, + ); + + return disposable; +} diff --git a/src/views/nodes/autolinkedItemsNode.ts b/src/views/nodes/autolinkedItemsNode.ts index e005b0c847d86..d7e0ed74ffb10 100644 --- a/src/views/nodes/autolinkedItemsNode.ts +++ b/src/views/nodes/autolinkedItemsNode.ts @@ -3,18 +3,17 @@ import { GitUri } from '../../git/gitUri'; import type { GitLog } from '../../git/models/log'; import { PullRequest } from '../../git/models/pullRequest'; import { pauseOnCancelOrTimeoutMapTuple } from '../../system/cancellation'; -import { gate } from '../../system/decorators/gate'; -import { debug } from '../../system/decorators/log'; import { getSettledValue } from '../../system/promise'; import type { ViewsWithCommits } from '../viewBase'; import { AutolinkedItemNode } from './autolinkedItemNode'; import { LoadMoreNode, MessageNode } from './common'; import { PullRequestNode } from './pullRequestNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; let instanceId = 0; -export class AutolinkedItemsNode extends ViewNode<'autolinks', ViewsWithCommits> { +export class AutolinkedItemsNode extends CacheableChildrenViewNode<'autolinks', ViewsWithCommits> { private _instanceId: number; constructor( @@ -35,10 +34,8 @@ export class AutolinkedItemsNode extends ViewNode<'autolinks', ViewsWithCommits> return this._uniqueId; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const commits = [...this.log.commits.values()]; let children: ViewNode[] | undefined; @@ -92,9 +89,9 @@ export class AutolinkedItemsNode extends ViewNode<'autolinks', ViewsWithCommits> ); } - this._children = children; + this.children = children; } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -107,12 +104,4 @@ export class AutolinkedItemsNode extends ViewNode<'autolinks', ViewsWithCommits> return item; } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (!reset) return; - - this._children = undefined; - } } diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index f685e79848502..f0aa4475429dd 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -18,6 +18,7 @@ import type { Deferred } from '../../system/promise'; import { defer, getSettledValue } from '../../system/promise'; import { pad } from '../../system/string'; import type { ViewsWithBranches } from '../viewBase'; +import { disposeChildren } from '../viewBase'; import { BranchTrackingStatusNode } from './branchTrackingStatusNode'; import { CommitNode } from './commitNode'; import { LoadMoreNode, MessageNode } from './common'; @@ -94,6 +95,12 @@ export class BranchNode }; } + @debug() + override dispose() { + super.dispose(); + this.children = undefined; + } + override get id(): string { return this._uniqueId; } @@ -135,9 +142,18 @@ export class BranchNode } private _children: ViewNode[] | undefined; + protected get children(): ViewNode[] | undefined { + return this._children; + } + protected set children(value: ViewNode[] | undefined) { + if (this._children === value) return; + + disposeChildren(this._children, value); + this._children = value; + } async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const branch = this.branch; let onCompleted: Deferred | undefined; @@ -171,9 +187,9 @@ export class BranchNode clearTimeout(timeout); // If we found a pull request, insert it into the children cache (if loaded) and refresh the node - if (pr != null && this._children != null) { - this._children.splice( - this._children[0] instanceof CompareBranchNode ? 1 : 0, + if (pr != null && this.children != null) { + this.children.splice( + this.children[0] instanceof CompareBranchNode ? 1 : 0, 0, new PullRequestNode(this.view, this, pr, branch), ); @@ -330,11 +346,11 @@ export class BranchNode ); } - this._children = children; + this.children = children; setTimeout(() => onCompleted?.fulfill(), 1); } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -510,10 +526,10 @@ export class BranchNode void this.view.refresh(true); } - @gate() - @debug() override refresh(reset?: boolean) { - this._children = undefined; + void super.refresh?.(reset); + + this.children = undefined; if (reset) { this._log = undefined; this.deleteState(); @@ -586,7 +602,7 @@ export class BranchNode this._log = log; this.limit = log?.count; - this._children = undefined; + this.children = undefined; void this.triggerChange(false); } } diff --git a/src/views/nodes/branchesNode.ts b/src/views/nodes/branchesNode.ts index dc3a85330d8d3..f3528abc76a88 100644 --- a/src/views/nodes/branchesNode.ts +++ b/src/views/nodes/branchesNode.ts @@ -2,15 +2,15 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GitUri } from '../../git/gitUri'; import type { Repository } from '../../git/models/repository'; import { makeHierarchical } from '../../system/array'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import type { ViewsWithBranchesNode } from '../viewBase'; import { BranchNode } from './branchNode'; import { BranchOrTagFolderNode } from './branchOrTagFolderNode'; import { MessageNode } from './common'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; -export class BranchesNode extends ViewNode<'branches', ViewsWithBranchesNode> { +export class BranchesNode extends CacheableChildrenViewNode<'branches', ViewsWithBranchesNode> { constructor( uri: GitUri, view: ViewsWithBranchesNode, @@ -31,10 +31,8 @@ export class BranchesNode extends ViewNode<'branches', ViewsWithBranchesNode> { return this.repo.path; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const branches = await this.repo.getBranches({ // only show local branches filter: b => !b.remote, @@ -74,10 +72,10 @@ export class BranchesNode extends ViewNode<'branches', ViewsWithBranchesNode> { ); const root = new BranchOrTagFolderNode(this.view, this, 'branch', hierarchy, this.repo.path, '', undefined); - this._children = root.getChildren(); + this.children = root.getChildren(); } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -96,9 +94,8 @@ export class BranchesNode extends ViewNode<'branches', ViewsWithBranchesNode> { return item; } - @gate() @debug() override refresh() { - this._children = undefined; + super.refresh(true); } } diff --git a/src/views/nodes/commitNode.ts b/src/views/nodes/commitNode.ts index 2a431e2500a29..d4b15c7bcf0b2 100644 --- a/src/views/nodes/commitNode.ts +++ b/src/views/nodes/commitNode.ts @@ -14,13 +14,14 @@ import { makeHierarchical } from '../../system/array'; import { pauseOnCancelOrTimeoutMapTuplePromise } from '../../system/cancellation'; import { configuration } from '../../system/configuration'; import { getContext } from '../../system/context'; -import { gate } from '../../system/decorators/gate'; +import { debug } from '../../system/decorators/log'; import { joinPaths, normalizePath } from '../../system/path'; import type { Deferred } from '../../system/promise'; import { defer, getSettledValue } from '../../system/promise'; import { sortCompare } from '../../system/string'; import type { FileHistoryView } from '../fileHistoryView'; import type { ViewsWithCommits } from '../viewBase'; +import { disposeChildren } from '../viewBase'; import { CommitFileNode } from './commitFileNode'; import type { FileNode } from './folderNode'; import { FolderNode } from './folderNode'; @@ -49,6 +50,12 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis this._uniqueId = getViewNodeId(this.type, this.context); } + @debug() + override dispose() { + super.dispose(); + this.children = undefined; + } + override get id(): string { return this._uniqueId; } @@ -65,13 +72,22 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis return this.commit; } - private _children: (PullRequestNode | FileNode)[] | undefined; + private _children: ViewNode[] | undefined; + protected get children(): ViewNode[] | undefined { + return this._children; + } + protected set children(value: ViewNode[] | undefined) { + if (this._children === value) return; + + disposeChildren(this._children, value); + this._children = value; + } async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const commit = this.commit; - let children: (PullRequestNode | FileNode)[] = []; + let children: ViewNode[] = []; let onCompleted: Deferred | undefined; let pullRequest; @@ -101,8 +117,8 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis clearTimeout(timeout); // If we found a pull request, insert it into the children cache (if loaded) and refresh the node - if (pr != null && this._children != null) { - this._children.unshift(new PullRequestNode(this.view, this, pr, commit)); + if (pr != null && this.children != null) { + this.children.unshift(new PullRequestNode(this.view, this, pr, commit)); } // Refresh this node to add the pull request node or remove the spinner @@ -136,11 +152,11 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis children.unshift(new PullRequestNode(this.view, this, pullRequest, commit)); } - this._children = children; + this.children = children; setTimeout(() => onCompleted?.fulfill(), 1); } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -197,9 +213,10 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis }; } - @gate() override refresh(reset?: boolean) { - this._children = undefined; + void super.refresh?.(reset); + + this.children = undefined; if (reset) { this.deleteState(); } diff --git a/src/views/nodes/compareBranchNode.ts b/src/views/nodes/compareBranchNode.ts index 6f6cbd62016d6..3ee29eae1d5a3 100644 --- a/src/views/nodes/compareBranchNode.ts +++ b/src/views/nodes/compareBranchNode.ts @@ -9,8 +9,8 @@ import { createRevisionRange, shortenRevision } from '../../git/models/reference import type { GitUser } from '../../git/models/user'; import { CommandQuickPickItem } from '../../quickpicks/items/common'; import { showReferencePicker } from '../../quickpicks/referencePicker'; -import { gate } from '../../system/decorators/gate'; import { debug, log } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import { getSettledValue } from '../../system/promise'; import { pluralize } from '../../system/string'; import type { ViewsWithBranches } from '../viewBase'; @@ -35,9 +35,9 @@ type State = { export class CompareBranchNode extends SubscribeableViewNode< 'compare-branch', ViewsWithBranches | WorktreesView, + ViewNode, State > { - private _children: ViewNode[] | undefined; private _compareWith: StoredBranchComparison | undefined; constructor( @@ -92,7 +92,7 @@ export class CompareBranchNode extends SubscribeableViewNode< } protected override subscribe(): Disposable | Promise | undefined { - return this.view.onDidChangeNodesCheckedState(this.onNodesCheckedStateChanged, this); + return weakEvent(this.view.onDidChangeNodesCheckedState, this.onNodesCheckedStateChanged, this); } private onNodesCheckedStateChanged(e: TreeCheckboxChangeEvent) { @@ -105,7 +105,7 @@ export class CompareBranchNode extends SubscribeableViewNode< async getChildren(): Promise { if (this._compareWith == null) return []; - if (this._children == null) { + if (this.children == null) { const ahead = this.ahead; const behind = this.behind; @@ -119,7 +119,7 @@ export class CompareBranchNode extends SubscribeableViewNode< forkPoint: true, })) ?? (await this.view.container.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2)); - this._children = [ + const children: ViewNode[] = [ new ResultsCommitsNode( this.view, this, @@ -166,7 +166,7 @@ export class CompareBranchNode extends SubscribeableViewNode< // Can't support showing files when commits are filtered if (!this.filterByAuthors?.length) { - this._children.push( + children.push( new ResultsFilesNode( this.view, this, @@ -179,8 +179,10 @@ export class CompareBranchNode extends SubscribeableViewNode< ), ); } + + this.children = children; } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -234,7 +236,7 @@ export class CompareBranchNode extends SubscribeableViewNode< this._compareWith = undefined; await this.updateCompareWith(undefined); - this._children = undefined; + this.children = undefined; this.view.triggerNodeChange(this); } @@ -249,10 +251,9 @@ export class CompareBranchNode extends SubscribeableViewNode< await this.compareWith(); } - @gate() @debug() - override refresh() { - this._children = undefined; + override refresh(reset?: boolean) { + super.refresh(reset); this.loadCompareWith(); } @@ -264,7 +265,7 @@ export class CompareBranchNode extends SubscribeableViewNode< this.showComparison = comparisonType; } - this._children = undefined; + this.children = undefined; this.view.triggerNodeChange(this); } @@ -296,7 +297,7 @@ export class CompareBranchNode extends SubscribeableViewNode< type: this.comparisonType, }); - this._children = undefined; + this.children = undefined; this.view.triggerNodeChange(this); } diff --git a/src/views/nodes/compareResultsNode.ts b/src/views/nodes/compareResultsNode.ts index a22d60ea459c3..e4b515437f9f3 100644 --- a/src/views/nodes/compareResultsNode.ts +++ b/src/views/nodes/compareResultsNode.ts @@ -7,6 +7,7 @@ import { createRevisionRange, shortenRevision } from '../../git/models/reference import type { GitUser } from '../../git/models/user'; import { gate } from '../../system/decorators/gate'; import { debug, log } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import { getSettledValue } from '../../system/promise'; import { pluralize } from '../../system/string'; import type { SearchAndCompareView } from '../searchAndCompareView'; @@ -24,7 +25,12 @@ type State = { filterCommits: GitUser[] | undefined; }; -export class CompareResultsNode extends SubscribeableViewNode<'compare-results', SearchAndCompareView, State> { +export class CompareResultsNode extends SubscribeableViewNode< + 'compare-results', + SearchAndCompareView, + ViewNode, + State +> { private _instanceId: number; constructor( @@ -99,7 +105,7 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', } protected override subscribe(): Disposable | Promise | undefined { - return this.view.onDidChangeNodesCheckedState(this.onNodesCheckedStateChanged, this); + return weakEvent(this.view.onDidChangeNodesCheckedState, this.onNodesCheckedStateChanged, this); } private onNodesCheckedStateChanged(e: TreeCheckboxChangeEvent) { @@ -113,10 +119,8 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', void this.remove(true); } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const ahead = this.ahead; const behind = this.behind; @@ -131,7 +135,7 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', forkPoint: true, })) ?? (await this.view.container.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2)); - this._children = [ + const children: ViewNode[] = [ new ResultsCommitsNode( this.view, this, @@ -176,7 +180,7 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', // Can't support showing files when commits are filtered if (!this.filterByAuthors?.length) { - this._children.push( + children.push( new ResultsFilesNode( this.view, this, @@ -189,8 +193,10 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', ), ); } + + this.children = children; } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -225,14 +231,6 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', return Promise.resolve<[string, string]>([this._compareWith.ref, this._ref.ref]); } - @gate() - @debug() - override refresh(reset: boolean = false) { - if (!reset) return; - - this._children = undefined; - } - @log() clearReviewed() { resetComparisonCheckedFiles(this.view, this.getStorageId()); @@ -256,7 +254,7 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results', // Remove the existing stored item and save a new one await this.replace(currentId, true); - this._children = undefined; + this.children = undefined; this.view.triggerNodeChange(this.parent); queueMicrotask(() => this.view.reveal(this, { expand: true, focus: true, select: true })); } diff --git a/src/views/nodes/contributorsNode.ts b/src/views/nodes/contributorsNode.ts index 8647c3b3a1557..a6e968988dd80 100644 --- a/src/views/nodes/contributorsNode.ts +++ b/src/views/nodes/contributorsNode.ts @@ -3,14 +3,18 @@ import type { GitUri } from '../../git/gitUri'; import { GitContributor } from '../../git/models/contributor'; import type { Repository } from '../../git/models/repository'; import { configuration } from '../../system/configuration'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import type { ViewsWithContributorsNode } from '../viewBase'; import { MessageNode } from './common'; import { ContributorNode } from './contributorNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; - -export class ContributorsNode extends ViewNode<'contributors', ViewsWithContributorsNode> { +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; + +export class ContributorsNode extends CacheableChildrenViewNode< + 'contributors', + ViewsWithContributorsNode, + ContributorNode +> { protected override splatted = true; constructor( @@ -33,10 +37,8 @@ export class ContributorsNode extends ViewNode<'contributors', ViewsWithContribu return this.repo.path; } - private _children: ContributorNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const all = configuration.get('views.contributors.showAllBranches'); let ref: string | undefined; @@ -58,7 +60,7 @@ export class ContributorsNode extends ViewNode<'contributors', ViewsWithContribu GitContributor.sort(contributors); const presenceMap = this.view.container.vsls.enabled ? await this.getPresenceMap(contributors) : undefined; - this._children = contributors.map( + this.children = contributors.map( c => new ContributorNode(this.uri, this.view, this, c, { all: all, @@ -68,7 +70,7 @@ export class ContributorsNode extends ViewNode<'contributors', ViewsWithContribu ); } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -82,19 +84,18 @@ export class ContributorsNode extends ViewNode<'contributors', ViewsWithContribu } updateAvatar(email: string) { - if (this._children == null) return; + if (this.children == null) return; - for (const child of this._children) { + for (const child of this.children) { if (child.contributor.email === email) { void child.triggerChange(); } } } - @gate() @debug() override refresh() { - this._children = undefined; + super.refresh(true); } @debug({ args: false }) diff --git a/src/views/nodes/fileHistoryNode.ts b/src/views/nodes/fileHistoryNode.ts index 553ee390bf94c..9a69644746fb3 100644 --- a/src/views/nodes/fileHistoryNode.ts +++ b/src/views/nodes/fileHistoryNode.ts @@ -9,6 +9,7 @@ import { configuration } from '../../system/configuration'; import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import { memoize } from '../../system/decorators/memoize'; +import { weakEvent } from '../../system/event'; import { filterMap, flatMap, map, uniqueBy } from '../../system/iterable'; import { Logger } from '../../system/logger'; import { basename } from '../../system/path'; @@ -178,14 +179,17 @@ export class FileHistoryNode if (repo == null) return undefined; const subscription = Disposable.from( - repo.onDidChange(this.onRepositoryChanged, this), - repo.onDidChangeFileSystem(this.onFileSystemChanged, this), - repo.startWatchingFileSystem(), - configuration.onDidChange(e => { - if (configuration.changed(e, 'advanced.fileHistoryFollowsRenames')) { - this.view.resetNodeLastKnownLimit(this); - } - }), + weakEvent(repo.onDidChange, this.onRepositoryChanged, this), + weakEvent(repo.onDidChangeFileSystem, this.onFileSystemChanged, this, [repo.startWatchingFileSystem()]), + weakEvent( + configuration.onDidChange, + e => { + if (configuration.changed(e, 'advanced.fileHistoryFollowsRenames')) { + this.view.resetNodeLastKnownLimit(this); + } + }, + this, + ), ); return subscription; diff --git a/src/views/nodes/fileHistoryTrackerNode.ts b/src/views/nodes/fileHistoryTrackerNode.ts index f7a68ffe1281d..d854e1c26dd7f 100644 --- a/src/views/nodes/fileHistoryTrackerNode.ts +++ b/src/views/nodes/fileHistoryTrackerNode.ts @@ -8,6 +8,7 @@ import { UriComparer } from '../../system/comparers'; import { setContext } from '../../system/context'; import { gate } from '../../system/decorators/gate'; import { debug, log } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import type { Deferrable } from '../../system/function'; import { debounce } from '../../system/function'; import { Logger } from '../../system/logger'; @@ -20,29 +21,31 @@ import { ContextValues, SubscribeableViewNode } from './viewNode'; export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history-tracker', FileHistoryView> { private _base: string | undefined; - private _child: FileHistoryNode | undefined; protected override splatted = true; constructor(view: FileHistoryView) { super('file-history-tracker', unknownGitUri, view); } + @debug() override dispose() { super.dispose(); - - this.resetChild(); + this.child = undefined; } - @debug() - private resetChild() { - if (this._child == null) return; + private _child: FileHistoryNode | undefined; + protected get child(): FileHistoryNode | undefined { + return this._child; + } + protected set child(value: FileHistoryNode | undefined) { + if (this._child === value) return; - this._child.dispose(); - this._child = undefined; + this._child?.dispose(); + this._child = value; } async getChildren(): Promise { - if (this._child == null) { + if (this.child == null) { if (!this.hasUri) { this.view.description = undefined; @@ -79,10 +82,10 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history- filter: b => b.name === commitish.sha, })); } - this._child = new FileHistoryNode(fileUri, this.view, this, folder, branch); + this.child = new FileHistoryNode(fileUri, this.view, this, folder, branch); } - return this._child.getChildren(); + return this.child.getChildren(); } getTreeItem(): TreeItem { @@ -124,7 +127,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history- } else { this._base = pick.ref; } - if (this._child == null) return; + if (this.child == null) return; this.setUri(); await this.triggerChange(); @@ -190,7 +193,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history- this.reset(); } else { this.setUri(gitUri); - this.resetChild(); + this.child = undefined; } setLogScopeExit(scope, `, uri=${Logger.toLoggable(this._uri)}`); @@ -199,7 +202,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history- private reset() { this.setUri(); - this.resetChild(); + this.child = undefined; } @log() @@ -223,7 +226,9 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history- @debug() protected subscribe() { - return Disposable.from(window.onDidChangeActiveTextEditor(debounce(this.onActiveEditorChanged, 250), this)); + return Disposable.from( + weakEvent(window.onDidChangeActiveTextEditor, debounce(this.onActiveEditorChanged, 250), this), + ); } protected override etag(): number { diff --git a/src/views/nodes/lineHistoryNode.ts b/src/views/nodes/lineHistoryNode.ts index e93dfa939e021..195fb19616515 100644 --- a/src/views/nodes/lineHistoryNode.ts +++ b/src/views/nodes/lineHistoryNode.ts @@ -11,6 +11,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode } from '../../git/mode import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import { memoize } from '../../system/decorators/memoize'; +import { weakEvent } from '../../system/event'; import { filterMap } from '../../system/iterable'; import { Logger } from '../../system/logger'; import type { FileHistoryView } from '../fileHistoryView'; @@ -199,9 +200,8 @@ export class LineHistoryNode if (repo == null) return undefined; const subscription = Disposable.from( - repo.onDidChange(this.onRepositoryChanged, this), - repo.onDidChangeFileSystem(this.onFileSystemChanged, this), - repo.startWatchingFileSystem(), + weakEvent(repo.onDidChange, this.onRepositoryChanged, this), + weakEvent(repo.onDidChangeFileSystem, this.onFileSystemChanged, this, [repo.startWatchingFileSystem()]), ); return subscription; diff --git a/src/views/nodes/lineHistoryTrackerNode.ts b/src/views/nodes/lineHistoryTrackerNode.ts index 715168d295345..32bd00aa2d9b9 100644 --- a/src/views/nodes/lineHistoryTrackerNode.ts +++ b/src/views/nodes/lineHistoryTrackerNode.ts @@ -9,6 +9,7 @@ import { UriComparer } from '../../system/comparers'; import { setContext } from '../../system/context'; import { gate } from '../../system/decorators/gate'; import { debug, log } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import { debounce } from '../../system/function'; import { Logger } from '../../system/logger'; import { getLogScope, setLogScopeExit } from '../../system/logger.scope'; @@ -24,7 +25,6 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< FileHistoryView | LineHistoryView > { private _base: string | undefined; - private _child: LineHistoryNode | undefined; private _editorContents: string | undefined; private _selection: Selection | undefined; protected override splatted = true; @@ -33,22 +33,25 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< super('line-history-tracker', unknownGitUri, view); } + @debug() override dispose() { super.dispose(); - - this.resetChild(); + this.child = undefined; } - @debug() - private resetChild() { - if (this._child == null) return; + private _child: LineHistoryNode | undefined; + protected get child(): LineHistoryNode | undefined { + return this._child; + } + protected set child(value: LineHistoryNode | undefined) { + if (this._child === value) return; - this._child.dispose(); - this._child = undefined; + this._child?.dispose(); + this._child = value; } async getChildren(): Promise { - if (this._child == null) { + if (this.child == null) { if (!this.hasUri) { this.view.description = undefined; @@ -87,10 +90,10 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< filter: b => b.name === commitish.sha, })); } - this._child = new LineHistoryNode(fileUri, this.view, this, branch, this._selection, this._editorContents); + this.child = new LineHistoryNode(fileUri, this.view, this, branch, this._selection, this._editorContents); } - return this._child.getChildren(); + return this.child.getChildren(); } getTreeItem(): TreeItem { @@ -134,7 +137,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< } else { this._base = pick.ref; } - if (this._child == null) return; + if (this.child == null) return; this.setUri(); await this.triggerChange(); @@ -198,7 +201,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< this.setUri(gitUri); this._editorContents = editor.document.isDirty ? editor.document.getText() : undefined; this._selection = editor.selection; - this.resetChild(); + this.child = undefined; } setLogScopeExit(scope, `, uri=${Logger.toLoggable(this._uri)}`); @@ -209,7 +212,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< this.setUri(); this._editorContents = undefined; this._selection = undefined; - this.resetChild(); + this.child = undefined; } @log() @@ -225,11 +228,15 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode< return this.view.container.lineTracker.subscribe( this, - this.view.container.lineTracker.onDidChangeActiveLines((e: LinesChangeEvent) => { - if (e.pending) return; - - onActiveLinesChanged(e); - }), + weakEvent( + this.view.container.lineTracker.onDidChangeActiveLines, + (e: LinesChangeEvent) => { + if (e.pending) return; + + onActiveLinesChanged(e); + }, + this, + ), ); } diff --git a/src/views/nodes/reflogNode.ts b/src/views/nodes/reflogNode.ts index 3cfb814ab982c..e8ee9944e019a 100644 --- a/src/views/nodes/reflogNode.ts +++ b/src/views/nodes/reflogNode.ts @@ -2,16 +2,18 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import type { GitUri } from '../../git/gitUri'; import type { GitReflog } from '../../git/models/reflog'; import type { Repository } from '../../git/models/repository'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import type { RepositoriesView } from '../repositoriesView'; import type { WorkspacesView } from '../workspacesView'; import { LoadMoreNode, MessageNode } from './common'; import { ReflogRecordNode } from './reflogRecordNode'; -import type { PageableViewNode } from './viewNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { PageableViewNode, ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; -export class ReflogNode extends ViewNode<'reflog', RepositoriesView | WorkspacesView> implements PageableViewNode { +export class ReflogNode + extends CacheableChildrenViewNode<'reflog', RepositoriesView | WorkspacesView> + implements PageableViewNode +{ limit: number | undefined; constructor( @@ -31,10 +33,8 @@ export class ReflogNode extends ViewNode<'reflog', RepositoriesView | Workspaces return this._uniqueId; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children === undefined) { + if (this.children === undefined) { const children = []; const reflog = await this.getReflog(); @@ -48,9 +48,9 @@ export class ReflogNode extends ViewNode<'reflog', RepositoriesView | Workspaces children.push(new LoadMoreNode(this.view, this, children[children.length - 1])); } - this._children = children; + this.children = children; } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -66,10 +66,10 @@ export class ReflogNode extends ViewNode<'reflog', RepositoriesView | Workspaces return item; } - @gate() @debug() override refresh(reset?: boolean) { - this._children = undefined; + super.refresh(true); + if (reset) { this._reflog = undefined; } diff --git a/src/views/nodes/remotesNode.ts b/src/views/nodes/remotesNode.ts index 2fdd80f5c6c32..c36436ab5de28 100644 --- a/src/views/nodes/remotesNode.ts +++ b/src/views/nodes/remotesNode.ts @@ -1,14 +1,14 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import type { GitUri } from '../../git/gitUri'; import type { Repository } from '../../git/models/repository'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import type { ViewsWithRemotesNode } from '../viewBase'; import { MessageNode } from './common'; import { RemoteNode } from './remoteNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; -export class RemotesNode extends ViewNode<'remotes', ViewsWithRemotesNode> { +export class RemotesNode extends CacheableChildrenViewNode<'remotes', ViewsWithRemotesNode> { constructor( uri: GitUri, view: ViewsWithRemotesNode, @@ -29,19 +29,17 @@ export class RemotesNode extends ViewNode<'remotes', ViewsWithRemotesNode> { return this.repo.path; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const remotes = await this.repo.getRemotes({ sort: true }); if (remotes.length === 0) { return [new MessageNode(this.view, this, 'No remotes could be found')]; } - this._children = remotes.map(r => new RemoteNode(this.uri, this.view, this, this.repo, r)); + this.children = remotes.map(r => new RemoteNode(this.uri, this.view, this, this.repo, r)); } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -53,9 +51,8 @@ export class RemotesNode extends ViewNode<'remotes', ViewsWithRemotesNode> { return item; } - @gate() @debug() override refresh() { - this._children = undefined; + super.refresh(true); } } diff --git a/src/views/nodes/repositoriesNode.ts b/src/views/nodes/repositoriesNode.ts index 19d804a71df87..b1ad2da6f2d97 100644 --- a/src/views/nodes/repositoriesNode.ts +++ b/src/views/nodes/repositoriesNode.ts @@ -4,6 +4,7 @@ import type { RepositoriesChangeEvent } from '../../git/gitProviderService'; import { GitUri, unknownGitUri } from '../../git/gitUri'; import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import { debounce, szudzikPairing } from '../../system/function'; import { Logger } from '../../system/logger'; import type { ViewsWithRepositoriesNode } from '../viewBase'; @@ -12,40 +13,24 @@ import { RepositoryNode } from './repositoryNode'; import type { ViewNode } from './viewNode'; import { ContextValues, SubscribeableViewNode } from './viewNode'; -export class RepositoriesNode extends SubscribeableViewNode<'repositories', ViewsWithRepositoriesNode> { - private _children: (RepositoryNode | MessageNode)[] | undefined; - +export class RepositoriesNode extends SubscribeableViewNode< + 'repositories', + ViewsWithRepositoriesNode, + RepositoryNode | MessageNode +> { constructor(view: ViewsWithRepositoriesNode) { super('repositories', unknownGitUri, view); } - override dispose() { - super.dispose(); - - this.resetChildren(); - } - - @debug() - private resetChildren() { - if (this._children == null) return; - - for (const child of this._children) { - if ('dispose' in child) { - child.dispose(); - } - } - this._children = undefined; - } - getChildren(): ViewNode[] { - if (this._children == null) { + if (this.children == null) { const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) return [new MessageNode(this.view, this, 'No repositories could be found.')]; - this._children = repositories.map(r => new RepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r)); + this.children = repositories.map(r => new RepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r)); } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -82,10 +67,11 @@ export class RepositoriesNode extends SubscribeableViewNode<'repositories', View @gate() @debug() override async refresh(reset: boolean = false) { - if (this._children == null) return; + const hasChildren = this.children != null; + super.refresh(reset); + if (!hasChildren) return; if (reset) { - this.resetChildren(); await this.unsubscribe(); void this.ensureSubscription(); @@ -93,17 +79,17 @@ export class RepositoriesNode extends SubscribeableViewNode<'repositories', View } const repositories = this.view.container.git.openRepositories; - if (repositories.length === 0 && (this._children == null || this._children.length === 0)) return; + if (repositories.length === 0 && (this.children == null || this.children.length === 0)) return; if (repositories.length === 0) { - this._children = [new MessageNode(this.view, this, 'No repositories could be found.')]; + this.children = [new MessageNode(this.view, this, 'No repositories could be found.')]; return; } const children = []; for (const repo of repositories) { const id = repo.id; - const child = (this._children as RepositoryNode[]).find(c => c.repo.id === id); + const child = (this.children as RepositoryNode[]).find(c => c.repo.id === id); if (child != null) { children.push(child); void child.refresh(); @@ -112,23 +98,21 @@ export class RepositoriesNode extends SubscribeableViewNode<'repositories', View } } - for (const child of this._children as RepositoryNode[]) { - if (children.includes(child)) continue; - - child.dispose(); - } - - this._children = children; + this.children = children; void this.ensureSubscription(); } @debug() protected subscribe() { - const subscriptions = [this.view.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this)]; + const subscriptions = [ + weakEvent(this.view.container.git.onDidChangeRepositories, this.onRepositoriesChanged, this), + ]; if (this.view.id === 'gitlens.views.repositories' && this.view.config.autoReveal) { - subscriptions.push(window.onDidChangeActiveTextEditor(debounce(this.onActiveEditorChanged, 500), this)); + subscriptions.push( + weakEvent(window.onDidChangeActiveTextEditor, debounce(this.onActiveEditorChanged, 500), this), + ); } return Disposable.from(...subscriptions); @@ -140,13 +124,13 @@ export class RepositoriesNode extends SubscribeableViewNode<'repositories', View @debug({ args: false }) private onActiveEditorChanged(editor: TextEditor | undefined) { - if (editor == null || this._children == null || this._children.length === 1) { + if (editor == null || this.children == null || this.children.length === 1) { return; } try { const uri = editor.document.uri; - const node = this._children.find(n => n instanceof RepositoryNode && n.repo.containsUri(uri)) as + const node = this.children.find(n => n instanceof RepositoryNode && n.repo.containsUri(uri)) as | RepositoryNode | undefined; if (node == null) return; diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index d774ed22678ad..bb4b4ea0d627b 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -16,6 +16,7 @@ import type { import { findLastIndex } from '../../system/array'; import { gate } from '../../system/decorators/gate'; import { debug, log } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import { disposableInterval } from '../../system/function'; import { pad } from '../../system/string'; import type { ViewsWithRepositories } from '../viewBase'; @@ -37,7 +38,6 @@ import { ContextValues, getViewNodeId, SubscribeableViewNode } from './viewNode' import { WorktreesNode } from './worktreesNode'; export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWithRepositories> { - private _children: ViewNode[] | undefined; private _status: Promise; constructor( @@ -76,7 +76,7 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit } async getChildren(): Promise { - if (this._children === undefined) { + if (this.children === undefined) { const children = []; const status = await this._status; @@ -192,9 +192,9 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit children.push(new ReflogNode(this.uri, this.view, this, this.repo)); } - this._children = children; + this.children = children; } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -349,10 +349,10 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit @gate() @debug() override async refresh(reset: boolean = false) { + super.refresh(reset); + if (reset) { this._status = this.repo.getStatus(); - - this._children = undefined; } await this.ensureSubscription(); @@ -374,7 +374,7 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit protected async subscribe() { const lastFetched = (await this.repo?.getLastFetched()) ?? 0; - const disposables = [this.repo.onDidChange(this.onRepositoryChanged, this)]; + const disposables = [weakEvent(this.repo.onDidChange, this.onRepositoryChanged, this)]; const interval = Repository.getLastFetchedUpdateInterval(lastFetched); if (lastFetched !== 0 && interval > 0) { @@ -396,8 +396,9 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit if (this.view.config.includeWorkingTree) { disposables.push( - this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), - this.repo.startWatchingFileSystem(), + weakEvent(this.repo.onDidChangeFileSystem, this.onFileSystemChanged, this, [ + this.repo.startWatchingFileSystem(), + ]), ); } @@ -420,22 +421,22 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit private async onFileSystemChanged(_e: RepositoryFileSystemChangeEvent) { this._status = this.repo.getStatus(); - if (this._children !== undefined) { + if (this.children !== undefined) { const status = await this._status; - let index = this._children.findIndex(c => c.type === 'status-files'); + let index = this.children.findIndex(c => c.type === 'status-files'); if (status !== undefined && (status.state.ahead || status.files.length !== 0)) { let deleteCount = 1; if (index === -1) { - index = findLastIndex(this._children, c => c.type === 'tracking-status' || c.type === 'branch'); + index = findLastIndex(this.children, c => c.type === 'tracking-status' || c.type === 'branch'); deleteCount = 0; index++; } const range = undefined; //status.upstream ? createRange(status.upstream, status.sha) : undefined; - this._children.splice(index, deleteCount, new StatusFilesNode(this.view, this, status, range)); + this.children.splice(index, deleteCount, new StatusFilesNode(this.view, this, status, range)); } else if (index !== -1) { - this._children.splice(index, 1); + this.children.splice(index, 1); } } @@ -451,7 +452,7 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit } if ( - this._children == null || + this.children == null || e.changed( RepositoryChange.Config, RepositoryChange.Index, @@ -468,21 +469,21 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit } if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) { - const node = this._children.find(c => c.type === 'remotes'); + const node = this.children.find(c => c.type === 'remotes'); if (node != null) { this.view.triggerNodeChange(node); } } if (e.changed(RepositoryChange.Stash, RepositoryChangeComparisonMode.Any)) { - const node = this._children.find(c => c.type === 'stashes'); + const node = this.children.find(c => c.type === 'stashes'); if (node != null) { this.view.triggerNodeChange(node); } } if (e.changed(RepositoryChange.Tags, RepositoryChangeComparisonMode.Any)) { - const node = this._children.find(c => c.type === 'tags'); + const node = this.children.find(c => c.type === 'tags'); if (node != null) { this.view.triggerNodeChange(node); } diff --git a/src/views/nodes/stashesNode.ts b/src/views/nodes/stashesNode.ts index ddc54b891099e..0643b197a459e 100644 --- a/src/views/nodes/stashesNode.ts +++ b/src/views/nodes/stashesNode.ts @@ -1,15 +1,15 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import type { GitUri } from '../../git/gitUri'; import type { Repository } from '../../git/models/repository'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import { map } from '../../system/iterable'; import type { ViewsWithStashesNode } from '../viewBase'; import { MessageNode } from './common'; import { StashNode } from './stashNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; -export class StashesNode extends ViewNode<'stashes', ViewsWithStashesNode> { +export class StashesNode extends CacheableChildrenViewNode<'stashes', ViewsWithStashesNode> { constructor( uri: GitUri, view: ViewsWithStashesNode, @@ -30,17 +30,15 @@ export class StashesNode extends ViewNode<'stashes', ViewsWithStashesNode> { return this.repo.path; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const stash = await this.repo.getStash(); if (stash == null) return [new MessageNode(this.view, this, 'No stashes could be found.')]; - this._children = [...map(stash.commits.values(), c => new StashNode(this.view, this, c))]; + this.children = [...map(stash.commits.values(), c => new StashNode(this.view, this, c))]; } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -51,9 +49,8 @@ export class StashesNode extends ViewNode<'stashes', ViewsWithStashesNode> { return item; } - @gate() @debug() override refresh() { - this._children = undefined; + super.refresh(true); } } diff --git a/src/views/nodes/tagsNode.ts b/src/views/nodes/tagsNode.ts index e9011d489774b..c6e72ac96990a 100644 --- a/src/views/nodes/tagsNode.ts +++ b/src/views/nodes/tagsNode.ts @@ -2,15 +2,15 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GitUri } from '../../git/gitUri'; import type { Repository } from '../../git/models/repository'; import { makeHierarchical } from '../../system/array'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import type { ViewsWithTagsNode } from '../viewBase'; import { BranchOrTagFolderNode } from './branchOrTagFolderNode'; import { MessageNode } from './common'; import { TagNode } from './tagNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; -export class TagsNode extends ViewNode<'tags', ViewsWithTagsNode> { +export class TagsNode extends CacheableChildrenViewNode<'tags', ViewsWithTagsNode> { constructor( uri: GitUri, view: ViewsWithTagsNode, @@ -31,10 +31,8 @@ export class TagsNode extends ViewNode<'tags', ViewsWithTagsNode> { return this.repo.path; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const tags = await this.repo.getTags({ sort: true }); if (tags.values.length === 0) return [new MessageNode(this.view, this, 'No tags could be found.')]; @@ -52,10 +50,10 @@ export class TagsNode extends ViewNode<'tags', ViewsWithTagsNode> { ); const root = new BranchOrTagFolderNode(this.view, this, 'tag', hierarchy, this.repo.path, '', undefined); - this._children = root.getChildren(); + this.children = root.getChildren(); } - return this._children; + return this.children; } getTreeItem(): TreeItem { @@ -66,9 +64,8 @@ export class TagsNode extends ViewNode<'tags', ViewsWithTagsNode> { return item; } - @gate() @debug() override refresh() { - this._children = undefined; + super.refresh(true); } } diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index aad0690345fb8..972bb138cce75 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -32,10 +32,12 @@ import type { } from '../../plus/workspaces/models'; import { gate } from '../../system/decorators/gate'; import { debug, log, logName } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import { is as isA, szudzikPairing } from '../../system/function'; import { getLoggableName } from '../../system/logger'; import { pad } from '../../system/string'; import type { View } from '../viewBase'; +import { disposeChildren } from '../viewBase'; import type { BranchNode } from './branchNode'; import type { BranchTrackingStatus } from './branchTrackingStatusNode'; import type { CommitFileNode } from './commitFileNode'; @@ -214,14 +216,16 @@ export abstract class ViewNode< Type extends TreeViewNodeTypes = TreeViewNodeTypes, TView extends View = View, State extends object = any, -> { +> implements Disposable +{ is(type: T): this is TreeViewNodesByType[T] { return this.type === (type as unknown as Type); } protected _uniqueId!: string; - protected splatted = false; + // NOTE: @eamodio uncomment to track node leaks + // readonly uuid = uuid(); constructor( public readonly type: Type, @@ -230,9 +234,19 @@ export abstract class ViewNode< public readonly view: TView, protected parent?: ViewNode, ) { + // NOTE: @eamodio uncomment to track node leaks + // queueMicrotask(() => this.view.registerNode(this)); this._uri = uri; } + protected _disposed = false; + @debug() + dispose() { + this._disposed = true; + // NOTE: @eamodio uncomment to track node leaks + // this.view.unregisterNode(this); + } + get id(): string | undefined { return this._uniqueId; } @@ -279,11 +293,11 @@ export abstract class ViewNode< refresh?(reset?: boolean): boolean | void | Promise | Promise; - @gate((reset: boolean = false, force: boolean = false, avoidSelf?: ViewNode) => - JSON.stringify([reset, force, avoidSelf?.toString()]), - ) + @gate((reset, force, avoidSelf) => `${reset}|${force}|${avoidSelf?.toString()}`) @debug() triggerChange(reset: boolean = false, force: boolean = false, avoidSelf?: ViewNode): Promise { + if (this._disposed) return Promise.resolve(); + // If this node has been splatted (e.g. not shown itself, but its children are), then delegate the change to its parent if (this.splatted && this.parent != null && this.parent !== avoidSelf) { return this.parent.triggerChange(reset, force); @@ -322,9 +336,41 @@ export abstract class ViewNode< this.view.nodeState.storeState(this.id, key as string, value, sticky); } } + type StateKey = keyof T; type StateValue> = P extends keyof T ? T[P] : never; +export abstract class CacheableChildrenViewNode< + Type extends TreeViewNodeTypes = TreeViewNodeTypes, + TView extends View = View, + TChild extends ViewNode = ViewNode, + State extends object = any, +> extends ViewNode { + private _children: TChild[] | undefined; + protected get children(): TChild[] | undefined { + return this._children; + } + protected set children(value: TChild[] | undefined) { + if (this._children === value) return; + + disposeChildren(this._children, value); + this._children = value; + } + + @debug() + override dispose() { + super.dispose(); + this.children = undefined; + } + + @debug() + override refresh(reset: boolean = false) { + if (reset) { + this.children = undefined; + } + } +} + export abstract class ViewFileNode< Type extends TreeViewFileNodeTypes = TreeViewFileNodeTypes, TView extends View = View, @@ -401,8 +447,9 @@ export function isPageableViewNode(node: ViewNode): node is ViewNode & PageableV export abstract class SubscribeableViewNode< Type extends TreeViewSubscribableNodeTypes = TreeViewSubscribableNodeTypes, TView extends View = View, + TChild extends ViewNode = ViewNode, State extends object = any, -> extends ViewNode { +> extends CacheableChildrenViewNode { protected disposable: Disposable; protected subscription: Promise | undefined; @@ -412,12 +459,12 @@ export abstract class SubscribeableViewNode< super(type, uri, view, parent); const disposables = [ - this.view.onDidChangeVisibility(this.onVisibilityChanged, this), - // this.view.onDidChangeNodeCollapsibleState(this.onNodeCollapsibleStateChanged, this), + weakEvent(this.view.onDidChangeVisibility, this.onVisibilityChanged, this), + // weak(this.view.onDidChangeNodeCollapsibleState, this.onNodeCollapsibleStateChanged, this), ]; if (canAutoRefreshView(this.view)) { - disposables.push(this.view.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this)); + disposables.push(weakEvent(this.view.onDidChangeAutoRefresh, this.onAutoRefreshChanged, this)); } const getTreeItem = this.getTreeItem; @@ -438,16 +485,17 @@ export abstract class SubscribeableViewNode< } @debug() - dispose() { + override dispose() { + super.dispose(); void this.unsubscribe(); this.disposable?.dispose(); } - @gate() + @gate((reset, force) => `${reset}|${force}`) @debug() override async triggerChange(reset: boolean = false, force: boolean = false): Promise { - if (!this.loaded) return; + if (!this.loaded || this._disposed) return; if (reset && !this.view.visible) { this._pendingReset = reset; @@ -457,7 +505,7 @@ export abstract class SubscribeableViewNode< private _canSubscribe: boolean = true; protected get canSubscribe(): boolean { - return this._canSubscribe; + return this._canSubscribe && !this._disposed; } protected set canSubscribe(value: boolean) { if (this._canSubscribe === value) return; @@ -508,7 +556,6 @@ export abstract class SubscribeableViewNode< // protected onParentCollapsibleStateChanged?(state: TreeItemCollapsibleState): void; // protected onCollapsibleStateChanged?(state: TreeItemCollapsibleState): void; - // protected collapsibleState: TreeItemCollapsibleState | undefined; // protected onNodeCollapsibleStateChanged(e: TreeViewNodeCollapsibleStateChangeEvent) { // if (e.element === this) { @@ -562,7 +609,6 @@ export abstract class RepositoryFolderNode< TChild extends ViewNode = ViewNode, > extends SubscribeableViewNode<'repo-folder', TView> { protected override splatted = true; - protected child: TChild | undefined; constructor( uri: GitUri, @@ -580,6 +626,23 @@ export abstract class RepositoryFolderNode< this.splatted = splatted; } + private _child: TChild | undefined; + protected get child(): TChild | undefined { + return this._child; + } + protected set child(value: TChild | undefined) { + if (this._child === value) return; + + this._child?.dispose(); + this._child = value; + } + + @debug() + override dispose() { + super.dispose(); + this.child = undefined; + } + override get id(): string { return this._uniqueId; } @@ -686,6 +749,7 @@ export abstract class RepositoryFolderNode< @gate() @debug() override async refresh(reset: boolean = false) { + super.refresh(reset); await this.child?.triggerChange(reset, false, this); await this.ensureSubscription(); @@ -705,7 +769,7 @@ export abstract class RepositoryFolderNode< @debug() protected subscribe(): Disposable | Promise { - return this.repo.onDidChange(this.onRepositoryChanged, this); + return weakEvent(this.repo.onDidChange, this.onRepositoryChanged, this); } protected override etag(): number { @@ -740,31 +804,14 @@ export abstract class RepositoryFolderNode< export abstract class RepositoriesSubscribeableNode< TView extends View = View, - TChild extends ViewNode & Disposable = ViewNode & Disposable, -> extends SubscribeableViewNode<'repositories', TView> { + TChild extends ViewNode = ViewNode, +> extends SubscribeableViewNode<'repositories', TView, TChild> { protected override splatted = true; - protected children: TChild[] | undefined; constructor(view: TView) { super('repositories', unknownGitUri, view); } - override dispose() { - super.dispose(); - this.resetChildren(); - } - - private resetChildren() { - if (this.children == null) return; - - for (const child of this.children) { - if ('dispose' in child) { - child.dispose(); - } - } - this.children = undefined; - } - override async getSplattedChild() { if (this.children == null) { await this.getChildren(); @@ -773,16 +820,6 @@ export abstract class RepositoriesSubscribeableNode< return this.children?.length === 1 ? this.children[0] : undefined; } - @gate() - @debug() - override refresh(reset: boolean = false) { - if (this.children == null) return; - - if (reset) { - this.resetChildren(); - } - } - protected override etag(): number { return szudzikPairing(this.view.container.git.etag, this.view.container.subscription.etag); } @@ -790,8 +827,8 @@ export abstract class RepositoriesSubscribeableNode< @debug() protected subscribe(): Disposable | Promise { return Disposable.from( - this.view.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this), - this.view.container.subscription.onDidChange(this.onSubscriptionChanged, this), + weakEvent(this.view.container.git.onDidChangeRepositories, this.onRepositoriesChanged, this), + weakEvent(this.view.container.subscription.onDidChange, this.onSubscriptionChanged, this), ); } diff --git a/src/views/nodes/workspaceNode.ts b/src/views/nodes/workspaceNode.ts index 490ac1961a3c8..dbcb30ce7f445 100644 --- a/src/views/nodes/workspaceNode.ts +++ b/src/views/nodes/workspaceNode.ts @@ -3,8 +3,8 @@ import type { RepositoriesChangeEvent } from '../../git/gitProviderService'; import { GitUri } from '../../git/gitUri'; import type { CloudWorkspace, LocalWorkspace } from '../../plus/workspaces/models'; import { createCommand } from '../../system/command'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; +import { weakEvent } from '../../system/event'; import type { WorkspacesView } from '../workspacesView'; import { CommandMessageNode, MessageNode } from './common'; import { RepositoryNode } from './repositoryNode'; @@ -12,7 +12,11 @@ import type { ViewNode } from './viewNode'; import { ContextValues, getViewNodeId, SubscribeableViewNode } from './viewNode'; import { WorkspaceMissingRepositoryNode } from './workspaceMissingRepositoryNode'; -export class WorkspaceNode extends SubscribeableViewNode<'workspace', WorkspacesView> { +export class WorkspaceNode extends SubscribeableViewNode< + 'workspace', + WorkspacesView, + CommandMessageNode | MessageNode | RepositoryNode | WorkspaceMissingRepositoryNode +> { constructor( uri: GitUri, view: WorkspacesView, @@ -25,22 +29,6 @@ export class WorkspaceNode extends SubscribeableViewNode<'workspace', Workspaces this._uniqueId = getViewNodeId(this.type, this.context); } - override dispose() { - super.dispose(); - this.resetChildren(); - } - - private resetChildren() { - if (this._children == null) return; - - for (const child of this._children) { - if ('dispose' in child) { - child.dispose(); - } - } - this._children = undefined; - } - override get id(): string { return this._uniqueId; } @@ -49,19 +37,15 @@ export class WorkspaceNode extends SubscribeableViewNode<'workspace', Workspaces return this.workspace.name; } - private _children: - | (CommandMessageNode | MessageNode | RepositoryNode | WorkspaceMissingRepositoryNode)[] - | undefined; - async getChildren(): Promise { - if (this._children == null) { - this._children = []; + if (this.children == null) { + const children = []; try { const descriptors = await this.workspace.getRepositoryDescriptors(); - if (descriptors == null || descriptors.length === 0) { - this._children.push( + if (!descriptors?.length) { + children.push( new CommandMessageNode( this.view, this, @@ -73,7 +57,9 @@ export class WorkspaceNode extends SubscribeableViewNode<'workspace', Workspaces 'No repositories', ), ); - return this._children; + + this.children = children; + return this.children; } const reposByName = await this.workspace.getRepositoriesByName({ force: true }); @@ -81,13 +67,11 @@ export class WorkspaceNode extends SubscribeableViewNode<'workspace', Workspaces for (const descriptor of descriptors) { const repo = reposByName.get(descriptor.name)?.repository; if (!repo) { - this._children.push( - new WorkspaceMissingRepositoryNode(this.view, this, this.workspace, descriptor), - ); + children.push(new WorkspaceMissingRepositoryNode(this.view, this, this.workspace, descriptor)); continue; } - this._children.push( + children.push( new RepositoryNode( GitUri.fromRepoPath(repo.path), this.view, @@ -98,11 +82,14 @@ export class WorkspaceNode extends SubscribeableViewNode<'workspace', Workspaces ); } } catch (ex) { + this.children = undefined; return [new MessageNode(this.view, this, 'Failed to load repositories')]; } + + this.children = children; } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -146,23 +133,15 @@ export class WorkspaceNode extends SubscribeableViewNode<'workspace', Workspaces return item; } - @gate() - @debug() - override refresh(reset: boolean = false) { - if (this._children == null) return; - - if (reset) { - this.resetChildren(); - } - } - protected override etag(): number { return this.view.container.git.etag; } @debug() protected subscribe(): Disposable | Promise { - return Disposable.from(this.view.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this)); + return Disposable.from( + weakEvent(this.view.container.git.onDidChangeRepositories, this.onRepositoriesChanged, this), + ); } private onRepositoriesChanged(_e: RepositoriesChangeEvent) { diff --git a/src/views/nodes/worktreeNode.ts b/src/views/nodes/worktreeNode.ts index c1aefffece461..fda5f730aee99 100644 --- a/src/views/nodes/worktreeNode.ts +++ b/src/views/nodes/worktreeNode.ts @@ -22,14 +22,15 @@ import { CompareBranchNode } from './compareBranchNode'; import { insertDateMarkers } from './helpers'; import { PullRequestNode } from './pullRequestNode'; import { UncommittedFilesNode } from './UncommittedFilesNode'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; type State = { pullRequest: PullRequest | null | undefined; pendingPullRequest: Promise | undefined; }; -export class WorktreeNode extends ViewNode<'worktree', ViewsWithWorktrees, State> { +export class WorktreeNode extends CacheableChildrenViewNode<'worktree', ViewsWithWorktrees, ViewNode, State> { limit: number | undefined; private _branch: GitBranch | undefined; @@ -59,10 +60,8 @@ export class WorktreeNode extends ViewNode<'worktree', ViewsWithWorktrees, State return this.uri.repoPath!; } - private _children: ViewNode[] | undefined; - async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const branch = this._branch; let onCompleted: Deferred | undefined; @@ -96,9 +95,9 @@ export class WorktreeNode extends ViewNode<'worktree', ViewsWithWorktrees, State clearTimeout(timeout); // If we found a pull request, insert it into the children cache (if loaded) and refresh the node - if (pr != null && this._children != null) { - this._children.splice( - this._children[0].type === 'compare-branch' ? 1 : 0, + if (pr != null && this.children != null) { + this.children.splice( + this.children[0].type === 'compare-branch' ? 1 : 0, 0, new PullRequestNode(this.view, this, pr, branch), ); @@ -181,11 +180,11 @@ export class WorktreeNode extends ViewNode<'worktree', ViewsWithWorktrees, State children.unshift(new UncommittedFilesNode(this.view, this, status, undefined)); } - this._children = children; + this.children = children; onCompleted?.fulfill(); } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -378,10 +377,10 @@ export class WorktreeNode extends ViewNode<'worktree', ViewsWithWorktrees, State return item; } - @gate() @debug() override refresh(reset?: boolean) { - this._children = undefined; + super.refresh(true); + if (reset) { this._log = undefined; this.deleteState(); @@ -442,7 +441,7 @@ export class WorktreeNode extends ViewNode<'worktree', ViewsWithWorktrees, State this._log = log; this.limit = log?.count; - this._children = undefined; + this.children = undefined; void this.triggerChange(false); } } diff --git a/src/views/nodes/worktreesNode.ts b/src/views/nodes/worktreesNode.ts index 644ceaaf4fb73..33dae0140d005 100644 --- a/src/views/nodes/worktreesNode.ts +++ b/src/views/nodes/worktreesNode.ts @@ -3,16 +3,14 @@ import { GlyphChars } from '../../constants'; import { PlusFeatures } from '../../features'; import type { GitUri } from '../../git/gitUri'; import type { Repository } from '../../git/models/repository'; -import { gate } from '../../system/decorators/gate'; import { debug } from '../../system/decorators/log'; import type { ViewsWithWorktreesNode } from '../viewBase'; import { MessageNode } from './common'; -import { ContextValues, getViewNodeId, ViewNode } from './viewNode'; +import type { ViewNode } from './viewNode'; +import { CacheableChildrenViewNode, ContextValues, getViewNodeId } from './viewNode'; import { WorktreeNode } from './worktreeNode'; -export class WorktreesNode extends ViewNode<'worktrees', ViewsWithWorktreesNode> { - private _children: WorktreeNode[] | undefined; - +export class WorktreesNode extends CacheableChildrenViewNode<'worktrees', ViewsWithWorktreesNode, WorktreeNode> { constructor( uri: GitUri, view: ViewsWithWorktreesNode, @@ -34,17 +32,17 @@ export class WorktreesNode extends ViewNode<'worktrees', ViewsWithWorktreesNode> } async getChildren(): Promise { - if (this._children == null) { + if (this.children == null) { const access = await this.repo.access(PlusFeatures.Worktrees); if (!access.allowed) return []; const worktrees = await this.repo.getWorktrees(); if (worktrees.length === 0) return [new MessageNode(this.view, this, 'No worktrees could be found.')]; - this._children = worktrees.map(wt => new WorktreeNode(this.uri, this.view, this, wt)); + this.children = worktrees.map(wt => new WorktreeNode(this.uri, this.view, this, wt)); } - return this._children; + return this.children; } async getTreeItem(): Promise { @@ -64,9 +62,8 @@ export class WorktreesNode extends ViewNode<'worktrees', ViewsWithWorktreesNode> return item; } - @gate() @debug() override refresh() { - this._children = undefined; + super.refresh(true); } } diff --git a/src/views/searchAndCompareView.ts b/src/views/searchAndCompareView.ts index b7fc860b4ff44..e01d6e3df34b4 100644 --- a/src/views/searchAndCompareView.ts +++ b/src/views/searchAndCompareView.ts @@ -24,7 +24,7 @@ import { CompareResultsNode, restoreComparisonCheckedFiles } from './nodes/compa import { FilesQueryFilter, ResultsFilesNode } from './nodes/resultsFilesNode'; import { SearchResultsNode } from './nodes/searchResultsNode'; import { ContextValues, RepositoryFolderNode, ViewNode } from './nodes/viewNode'; -import { ViewBase } from './viewBase'; +import { disposeChildren, ViewBase } from './viewBase'; import { registerViewCommand } from './viewCommands'; export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchAndCompareView> { @@ -35,20 +35,33 @@ export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchA super('search-compare', unknownGitUri, view); } + override dispose() { + disposeChildren(this._children); + } + private _children: (ComparePickerNode | CompareResultsNode | SearchResultsNode)[] | undefined; private get children(): (ComparePickerNode | CompareResultsNode | SearchResultsNode)[] { if (this._children == null) { - this._children = []; + const children = []; // Get stored searches & comparisons const stored = this.view.getStoredNodes(); if (stored.length !== 0) { - this._children.push(...stored); + children.push(...stored); } + + disposeChildren(this._children, children); + this._children = children; } return this._children; } + private set children(value: (ComparePickerNode | CompareResultsNode | SearchResultsNode)[] | undefined) { + if (this.children === value) return; + + disposeChildren(this.children, value); + this._children = value; + } getChildren(): ViewNode[] { const children = this.children; @@ -66,10 +79,11 @@ export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchA } addOrReplace(results: CompareResultsNode | SearchResultsNode) { - const children = this.children; + const children = [...this.children]; if (children.includes(results)) return; children.push(results); + this.children = children; this.view.triggerNodeChange(); } @@ -79,7 +93,7 @@ export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchA if (this.children.length === 0) return; this.removeComparePicker(true); - this._children!.length = 0; + this.children = []; await this.view.clearStorage(); @@ -98,13 +112,14 @@ export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchA node.dismiss(); } - const children = this.children; + const children = [...this.children]; if (children.length === 0) return; const index = children.indexOf(node); if (index === -1) return; children.splice(index, 1); + this.children = children; this.view.triggerNodeChange(); } @@ -210,7 +225,11 @@ export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchA repoPath: repoPath, ref: ref, }); - this.children.unshift(this.comparePicker); + + const children = [...this.children]; + children.unshift(this.comparePicker); + this.children = children; + void setContext('gitlens:views:canCompare', true); await this.triggerChange(); @@ -231,10 +250,12 @@ export class SearchAndCompareViewNode extends ViewNode<'search-compare', SearchA private removeComparePicker(silent: boolean = false) { void setContext('gitlens:views:canCompare', false); if (this.comparePicker != null) { - const children = this.children; + const children = [...this.children]; const index = children.indexOf(this.comparePicker); if (index !== -1) { children.splice(index, 1); + this.children = children; + if (!silent) { void this.triggerChange(); } diff --git a/src/views/viewBase.ts b/src/views/viewBase.ts index 914a6d8e17929..b5b004a0d0503 100644 --- a/src/views/viewBase.ts +++ b/src/views/viewBase.ts @@ -732,6 +732,57 @@ export abstract class ViewBase< return this._config; } + + // NOTE: @eamodio uncomment to track node leaks + // private _nodeTracking = new Map(); + // private registry = new FinalizationRegistry(uuid => { + // const id = this._nodeTracking.get(uuid); + + // Logger.log(`@@@ ${this.type} Finalizing [${uuid}]:${id}`); + + // this._nodeTracking.delete(uuid); + + // if (id != null) { + // const c = count(this._nodeTracking.values(), v => v === id); + // Logger.log(`@@@ ${this.type} [${padLeft(String(c), 3)}] ${id}`); + // } + // }); + + // registerNode(node: ViewNode) { + // const uuid = node.uuid; + + // Logger.log(`@@@ ${this.type}.registerNode [${uuid}]:${node.id}`); + + // this._nodeTracking.set(uuid, node.id); + // this.registry.register(node, uuid); + // } + + // unregisterNode(node: ViewNode) { + // const uuid = node.uuid; + + // Logger.log(`@@@ ${this.type}.unregisterNode [${uuid}]:${node.id}`); + + // this._nodeTracking.delete(uuid); + // this.registry.unregister(node); + // } + + // private _timer = setInterval(() => { + // const counts = new Map(); + // for (const value of this._nodeTracking.values()) { + // const count = counts.get(value) ?? 0; + // counts.set(value, count + 1); + // } + + // let total = 0; + // for (const [id, count] of counts) { + // if (count > 1) { + // Logger.log(`@@@ ${this.type} [${padLeft(String(count), 3)}] ${id}`); + // } + // total += count; + // } + + // Logger.log(`@@@ ${this.type} total=${total}`); + // }, 10000); } export class ViewNodeState implements Disposable { @@ -826,3 +877,13 @@ export class ViewNodeState implements Disposable { } } } + +export function disposeChildren(oldChildren: ViewNode[] | undefined, newChildren?: ViewNode[]) { + if (!oldChildren?.length) return; + + for (const child of oldChildren) { + if (newChildren?.includes(child)) continue; + + child.dispose(); + } +} diff --git a/src/views/workspacesView.ts b/src/views/workspacesView.ts index 9605b8ea26844..fdd52cc1a3e88 100644 --- a/src/views/workspacesView.ts +++ b/src/views/workspacesView.ts @@ -16,7 +16,7 @@ import { RepositoryNode } from './nodes/repositoryNode'; import { ViewNode } from './nodes/viewNode'; import type { WorkspaceMissingRepositoryNode } from './nodes/workspaceMissingRepositoryNode'; import { WorkspaceNode } from './nodes/workspaceNode'; -import { ViewBase } from './viewBase'; +import { disposeChildren, ViewBase } from './viewBase'; import { registerViewCommand } from './viewCommands'; export class WorkspacesViewNode extends ViewNode<'workspaces-view', WorkspacesView> { @@ -73,14 +73,7 @@ export class WorkspacesViewNode extends ViewNode<'workspaces-view', WorkspacesVi override refresh() { if (this._children == null) return; - if (this._children.length) { - for (const child of this._children) { - if ('dispose' in child) { - child.dispose(); - } - } - } - + disposeChildren(this._children); this._children = undefined; } }