Skip to content

Commit e810f7f

Browse files
authored
Manually manage the checkboxes (#8527)
to prevent flickering
1 parent 868b6cb commit e810f7f

File tree

4 files changed

+56
-31
lines changed

4 files changed

+56
-31
lines changed

src/view/prChangesTreeDataProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export class PullRequestChangesTreeDataProvider extends Disposable implements vs
3737
this._view = this._register(vscode.window.createTreeView('prStatus:github', {
3838
treeDataProvider: this,
3939
showCollapseAll: true,
40-
canSelectMany: true
40+
canSelectMany: true,
41+
manageCheckboxStateManually: true
4142
}));
4243

4344
this._register(

src/view/prsTreeDataProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T
7575
this._view = this._register(vscode.window.createTreeView('pr:github', {
7676
treeDataProvider: this,
7777
showCollapseAll: true,
78+
manageCheckboxStateManually: true
7879
}));
7980

8081
this._register(this._view.onDidChangeVisibility(e => {

src/view/treeNodes/fileChangeNode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ export class FileChangeNode extends TreeNode implements vscode.TreeItem {
195195
this.updateViewed(viewed);
196196
}
197197

198+
public refreshFileViewedContext() {
199+
this.pullRequestManager.setFileViewedContext();
200+
}
201+
198202
updateShowOptions() {
199203
const reviewThreads = this.pullRequest.reviewThreadsCache;
200204
const reviewThreadsByFile = groupBy(reviewThreads, thread => thread.path);

src/view/treeNodes/treeUtils.ts

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,18 @@ export namespace TreeUtils {
1515
const checkedNodes: FileChangeNode[] = [];
1616
const uncheckedNodes: FileChangeNode[] = [];
1717

18-
// The first item is the one the user actually clicked.
19-
// Only collect missing descendants if a directory was clicked directly.
20-
const firstNode = checkboxUpdates.items[0]?.[0];
21-
22-
const eventNodes = new Set<TreeNode>(checkboxUpdates.items.map(([node]) => node));
23-
24-
checkboxUpdates.items.forEach(checkboxUpdate => {
25-
const node = checkboxUpdate[0];
26-
const newState = checkboxUpdate[1];
27-
18+
for (const [node, newState] of checkboxUpdates.items) {
2819
if (node instanceof FileChangeNode) {
2920
if (newState === vscode.TreeItemCheckboxState.Checked) {
3021
checkedNodes.push(node);
3122
} else {
3223
uncheckedNodes.push(node);
3324
}
34-
} else if (firstNode instanceof DirectoryTreeNode && node === firstNode) {
35-
// VS Code auto-propagates to rendered children, but unrendered children
36-
// (due to virtual scrolling) won't be in the event. Collect those missing ones.
37-
collectMissingDescendants(firstNode, newState, checkedNodes, uncheckedNodes, eventNodes);
25+
node.updateFromCheckboxChanged(newState);
26+
} else if (node instanceof DirectoryTreeNode) {
27+
collectAllDescendants(node, newState, checkedNodes, uncheckedNodes);
3828
}
39-
40-
node.updateFromCheckboxChanged(newState);
41-
});
29+
}
4230

4331
if (selectionContainsUpdates) {
4432
for (const selected of selection) {
@@ -48,41 +36,72 @@ export namespace TreeUtils {
4836
if (!checkedNodes.includes(selected) && !uncheckedNodes.includes(selected)) {
4937
// Only process files that have checkboxes (files without checkboxState, like those under commits, are skipped)
5038
if (selected.checkboxState?.state === vscode.TreeItemCheckboxState.Unchecked) {
39+
selected.updateFromCheckboxChanged(vscode.TreeItemCheckboxState.Checked);
5140
checkedNodes.push(selected);
5241
} else if (selected.checkboxState?.state === vscode.TreeItemCheckboxState.Checked) {
42+
selected.updateFromCheckboxChanged(vscode.TreeItemCheckboxState.Unchecked);
5343
uncheckedNodes.push(selected);
5444
}
5545
}
5646
}
5747
}
5848

49+
// Refresh the tree so checkbox visual state updates.
50+
// Refreshing the topmost affected directory will cascade to all descendants.
51+
const allAffected = [...checkedNodes, ...uncheckedNodes];
52+
const refreshedDirs = new Set<DirectoryTreeNode>();
53+
for (const node of allAffected) {
54+
let topDir: DirectoryTreeNode | undefined;
55+
let parent = node.getParent();
56+
while (parent instanceof DirectoryTreeNode) {
57+
topDir = parent;
58+
parent = parent.getParent();
59+
}
60+
if (topDir && !refreshedDirs.has(topDir)) {
61+
refreshedDirs.add(topDir);
62+
topDir.refresh(topDir);
63+
}
64+
}
65+
// If a directory was clicked directly, also refresh it
66+
for (const [node] of checkboxUpdates.items) {
67+
if (node instanceof DirectoryTreeNode && !refreshedDirs.has(node)) {
68+
refreshedDirs.add(node);
69+
node.refresh(node);
70+
}
71+
}
72+
// For flat layout (files have no directory parent), refresh file nodes directly
73+
for (const node of allAffected) {
74+
const parent = node.getParent();
75+
if (!(parent instanceof DirectoryTreeNode)) {
76+
node.refresh(node);
77+
}
78+
}
79+
80+
// Send API requests without firing state change events (UI is already updated optimistically).
81+
// This prevents race conditions where overlapping markFiles calls cause checkboxes to flicker.
5982
if (checkedNodes.length > 0) {
6083
const prModel = checkedNodes[0].pullRequest;
6184
const filenames = checkedNodes.map(n => n.fileName);
62-
prModel.markFiles(filenames, true, 'viewed');
85+
prModel.markFiles(filenames, false, 'viewed').then(() => {
86+
checkedNodes[0].refreshFileViewedContext();
87+
});
6388
}
6489
if (uncheckedNodes.length > 0) {
6590
const prModel = uncheckedNodes[0].pullRequest;
6691
const filenames = uncheckedNodes.map(n => n.fileName);
67-
prModel.markFiles(filenames, true, 'unviewed');
92+
prModel.markFiles(filenames, false, 'unviewed').then(() => {
93+
uncheckedNodes[0].refreshFileViewedContext();
94+
});
6895
}
6996
}
7097

71-
/**
72-
* Collect descendant FileChangeNodes that are NOT already in the event.
73-
* These are children VS Code missed because they weren't rendered (virtual scrolling).
74-
*/
75-
function collectMissingDescendants(
98+
function collectAllDescendants(
7699
dirNode: DirectoryTreeNode,
77100
newState: vscode.TreeItemCheckboxState,
78101
checkedNodes: FileChangeNode[],
79-
uncheckedNodes: FileChangeNode[],
80-
eventNodes: Set<TreeNode>
102+
uncheckedNodes: FileChangeNode[]
81103
): void {
82104
for (const child of dirNode._children) {
83-
if (eventNodes.has(child)) {
84-
continue;
85-
}
86105
if (child instanceof FileChangeNode) {
87106
if (newState === vscode.TreeItemCheckboxState.Checked) {
88107
checkedNodes.push(child);
@@ -91,7 +110,7 @@ export namespace TreeUtils {
91110
}
92111
child.updateFromCheckboxChanged(newState);
93112
} else if (child instanceof DirectoryTreeNode) {
94-
collectMissingDescendants(child, newState, checkedNodes, uncheckedNodes, eventNodes);
113+
collectAllDescendants(child, newState, checkedNodes, uncheckedNodes);
95114
}
96115
}
97116
}

0 commit comments

Comments
 (0)