Browse Source

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
main
Eric Amodio 1 year ago
parent
commit
7cc24e4694
25 changed files with 486 additions and 364 deletions
  1. +24
    -0
      src/system/event.ts
  2. +6
    -17
      src/views/nodes/autolinkedItemsNode.ts
  3. +26
    -10
      src/views/nodes/branchNode.ts
  4. +7
    -10
      src/views/nodes/branchesNode.ts
  5. +27
    -10
      src/views/nodes/commitNode.ts
  6. +14
    -13
      src/views/nodes/compareBranchNode.ts
  7. +15
    -17
      src/views/nodes/compareResultsNode.ts
  8. +14
    -13
      src/views/nodes/contributorsNode.ts
  9. +12
    -8
      src/views/nodes/fileHistoryNode.ts
  10. +20
    -15
      src/views/nodes/fileHistoryTrackerNode.ts
  11. +3
    -3
      src/views/nodes/lineHistoryNode.ts
  12. +26
    -19
      src/views/nodes/lineHistoryTrackerNode.ts
  13. +11
    -11
      src/views/nodes/reflogNode.ts
  14. +7
    -10
      src/views/nodes/remotesNode.ts
  15. +24
    -40
      src/views/nodes/repositoriesNode.ts
  16. +19
    -18
      src/views/nodes/repositoryNode.ts
  17. +7
    -10
      src/views/nodes/stashesNode.ts
  18. +7
    -10
      src/views/nodes/tagsNode.ts
  19. +84
    -47
      src/views/nodes/viewNode.ts
  20. +22
    -43
      src/views/nodes/workspaceNode.ts
  21. +12
    -13
      src/views/nodes/worktreeNode.ts
  22. +7
    -10
      src/views/nodes/worktreesNode.ts
  23. +29
    -8
      src/views/searchAndCompareView.ts
  24. +61
    -0
      src/views/viewBase.ts
  25. +2
    -9
      src/views/workspacesView.ts

+ 24
- 0
src/system/event.ts View File

@ -103,3 +103,27 @@ export function promisifyDeferred(
cancel: () => cancel?.(),
};
}
export function weakEvent<T, U extends object>(
event: Event<T>,
listener: (e: T) => any,
thisArg: U,
disposables?: Disposable[],
): Disposable {
const ref = new WeakRef<U>(thisArg);
const disposable = event(
(e: T) => {
const obj = ref.deref();
if (obj != null) {
listener.call(obj, e);
} else {
disposable.dispose();
}
},
null,
disposables,
);
return disposable;
}

+ 6
- 17
src/views/nodes/autolinkedItemsNode.ts View File

@ -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<ViewNode[]> {
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;
}
}

+ 26
- 10
src/views/nodes/branchNode.ts View File

@ -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<ViewNode[]> {
if (this._children == null) {
if (this.children == null) {
const branch = this.branch;
let onCompleted: Deferred<void> | 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<TreeItem> {
@ -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);
}
}

+ 7
- 10
src/views/nodes/branchesNode.ts View File

@ -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<ViewNode[]> {
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<TreeItem> {
@ -96,9 +94,8 @@ export class BranchesNode extends ViewNode<'branches', ViewsWithBranchesNode> {
return item;
}
@gate()
@debug()
override refresh() {
this._children = undefined;
super.refresh(true);
}
}

+ 27
- 10
src/views/nodes/commitNode.ts View File

@ -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<ViewNode[]> {
if (this._children == null) {
if (this.children == null) {
const commit = this.commit;
let children: (PullRequestNode | FileNode)[] = [];
let children: ViewNode[] = [];
let onCompleted: Deferred<void> | 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<TreeItem> {
@ -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();
}

+ 14
- 13
src/views/nodes/compareBranchNode.ts View File

@ -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<Disposable | undefined> | undefined {
return this.view.onDidChangeNodesCheckedState(this.onNodesCheckedStateChanged, this);
return weakEvent(this.view.onDidChangeNodesCheckedState, this.onNodesCheckedStateChanged, this);
}
private onNodesCheckedStateChanged(e: TreeCheckboxChangeEvent<ViewNode>) {
@ -105,7 +105,7 @@ export class CompareBranchNode extends SubscribeableViewNode<
async getChildren(): Promise<ViewNode[]> {
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);
}

+ 15
- 17
src/views/nodes/compareResultsNode.ts View File

@ -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<Disposable | undefined> | undefined {
return this.view.onDidChangeNodesCheckedState(this.onNodesCheckedStateChanged, this);
return weakEvent(this.view.onDidChangeNodesCheckedState, this.onNodesCheckedStateChanged, this);
}
private onNodesCheckedStateChanged(e: TreeCheckboxChangeEvent<ViewNode>) {
@ -113,10 +119,8 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results',
void this.remove(true);
}
private _children: ViewNode[] | undefined;
async getChildren(): Promise<ViewNode[]> {
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 }));
}

+ 14
- 13
src/views/nodes/contributorsNode.ts View File

@ -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<ViewNode[]> {
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 })

+ 12
- 8
src/views/nodes/fileHistoryNode.ts View File

@ -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;

+ 20
- 15
src/views/nodes/fileHistoryTrackerNode.ts View File

@ -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<ViewNode[]> {
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 {

+ 3
- 3
src/views/nodes/lineHistoryNode.ts View File

@ -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;

+ 26
- 19
src/views/nodes/lineHistoryTrackerNode.ts View File

@ -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<ViewNode[]> {
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,
),
);
}

+ 11
- 11
src/views/nodes/reflogNode.ts View File

@ -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<ViewNode[]> {
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;
}

+ 7
- 10
src/views/nodes/remotesNode.ts View File

@ -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<ViewNode[]> {
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);
}
}

+ 24
- 40
src/views/nodes/repositoriesNode.ts View File

@ -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;

+ 19
- 18
src/views/nodes/repositoryNode.ts View File

@ -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<GitStatus | undefined>;
constructor(
@ -76,7 +76,7 @@ export class RepositoryNode extends SubscribeableViewNode<'repository', ViewsWit
}
async getChildren(): Promise<ViewNode[]> {
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<TreeItem> {
@ -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);
}

+ 7
- 10
src/views/nodes/stashesNode.ts View File

@ -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<ViewNode[]> {
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);
}
}

+ 7
- 10
src/views/nodes/tagsNode.ts View File

@ -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<ViewNode[]> {
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);
}
}

+ 84
- 47
src/views/nodes/viewNode.ts View File

@ -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<T extends keyof TreeViewNodesByType>(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<void> | Promise<boolean>;
@gate<ViewNode['triggerChange']>((reset: boolean = false, force: boolean = false, avoidSelf?: ViewNode) =>
JSON.stringify([reset, force, avoidSelf?.toString()]),
)
@gate<ViewNode['triggerChange']>((reset, force, avoidSelf) => `${reset}|${force}|${avoidSelf?.toString()}`)
@debug()
triggerChange(reset: boolean = false, force: boolean = false, avoidSelf?: ViewNode): Promise<void> {
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<T> = keyof T;
type StateValue<T, P extends StateKey<T>> = 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<Type, TView, State> {
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<Type, TView, State> {
> extends CacheableChildrenViewNode<Type, TView, TChild, State> {
protected disposable: Disposable;
protected subscription: Promise<Disposable | undefined> | 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<ViewNode['triggerChange']>((reset, force) => `${reset}|${force}`)
@debug()
override async triggerChange(reset: boolean = false, force: boolean = false): Promise<void> {
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<ViewNode>) {
// 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<Disposable> {
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<Disposable> {
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),
);
}

+ 22
- 43
src/views/nodes/workspaceNode.ts View File

@ -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<ViewNode[]> {
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<TreeItem> {
@ -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<Disposable> {
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) {

+ 12
- 13
src/views/nodes/worktreeNode.ts View File

@ -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<PullRequest | undefined> | 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<ViewNode[]> {
if (this._children == null) {
if (this.children == null) {
const branch = this._branch;
let onCompleted: Deferred<void> | 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<TreeItem> {
@ -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);
}
}

+ 7
- 10
src/views/nodes/worktreesNode.ts View File

@ -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<ViewNode[]> {
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<TreeItem> {
@ -64,9 +62,8 @@ export class WorktreesNode extends ViewNode<'worktrees', ViewsWithWorktreesNode>
return item;
}
@gate()
@debug()
override refresh() {
this._children = undefined;
super.refresh(true);
}
}

+ 29
- 8
src/views/searchAndCompareView.ts View File

@ -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();
}

+ 61
- 0
src/views/viewBase.ts View File

@ -732,6 +732,57 @@ export abstract class ViewBase<
return this._config;
}
// NOTE: @eamodio uncomment to track node leaks
// private _nodeTracking = new Map<string, string | undefined>();
// private registry = new FinalizationRegistry<string>(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<string | undefined, number>();
// 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();
}
}

+ 2
- 9
src/views/workspacesView.ts View File

@ -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;
}
}

Loading…
Cancel
Save