Skip to content

Commit e22bd42

Browse files
Copilotletmaik
andcommitted
Address PR review comments: improve checkbox reset logic
- Only set timestamp when checkbox is checked (not unchecked) - Update comment to clarify timestamp behavior - Use >= for file mtime comparison to handle race conditions - Fix cleanup logic to not delete folder checkbox states - Consolidate tree refresh to avoid redundant refreshes - Add caching for computeFolderCheckboxState performance Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com>
1 parent f8b9049 commit e22bd42

1 file changed

Lines changed: 61 additions & 19 deletions

File tree

src/treeProvider.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { API as GitAPI, Repository as GitAPIRepository } from './typings/git';
1919

2020
interface CheckboxStateInfo {
2121
state: TreeItemCheckboxState;
22-
timestamp: number; // When the checkbox was checked
22+
timestamp: number; // Timestamp when the checkbox was checked (not set for unchecked states)
2323
}
2424

2525
class FileElement implements IDiffStatus {
@@ -140,6 +140,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
140140
private treeView: TreeView<Element>;
141141
private isPaused: boolean;
142142
private checkboxStates: Map<string, CheckboxStateInfo> = new Map<string, CheckboxStateInfo>();
143+
private folderCheckboxStateCache: Map<string, TreeItemCheckboxState> = new Map<string, TreeItemCheckboxState>();
143144

144145
// Other
145146
private readonly disposables: Disposable[] = [];
@@ -311,12 +312,33 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
311312
}
312313

313314
private async handleChangeCheckboxState(e: TreeCheckboxChangeEvent<Element>) {
315+
// Clear the folder checkbox state cache since states are changing
316+
this.folderCheckboxStateCache.clear();
317+
314318
for (let [element, state] of e.items) {
315319
if (element instanceof FileElement || element instanceof FolderElement) {
316-
this.checkboxStates.set(element.dstAbsPath, {
317-
state: state,
318-
timestamp: Date.now()
319-
});
320+
const key = element.dstAbsPath;
321+
const existing = this.checkboxStates.get(key);
322+
323+
if (state === TreeItemCheckboxState.Checked) {
324+
// Only set timestamp when checking the item
325+
this.checkboxStates.set(key, {
326+
state: state,
327+
timestamp: Date.now()
328+
});
329+
} else if (existing) {
330+
// When unchecking, preserve the timestamp of when it was last checked
331+
this.checkboxStates.set(key, {
332+
state: state,
333+
timestamp: existing.timestamp
334+
});
335+
} else {
336+
// No existing state, just set unchecked with current timestamp
337+
this.checkboxStates.set(key, {
338+
state: state,
339+
timestamp: Date.now()
340+
});
341+
}
320342
}
321343
}
322344
}
@@ -403,10 +425,18 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
403425
}
404426

405427
private computeFolderCheckboxState(folder: FolderElement): TreeItemCheckboxState {
428+
// Check cache first
429+
const cached = this.folderCheckboxStateCache.get(folder.dstAbsPath);
430+
if (cached !== undefined) {
431+
return cached;
432+
}
433+
406434
// Check if user explicitly set state on this folder
407435
const explicitState = this.checkboxStates.get(folder.dstAbsPath);
408436
if (explicitState) {
409-
return explicitState.state;
437+
const result = explicitState.state;
438+
this.folderCheckboxStateCache.set(folder.dstAbsPath, result);
439+
return result;
410440
}
411441

412442
// Otherwise derive from files: folder is checked only if ALL files under it are checked
@@ -429,7 +459,9 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
429459
}
430460
}
431461

432-
return (hasFiles && allChecked) ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked;
462+
const result = (hasFiles && allChecked) ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked;
463+
this.folderCheckboxStateCache.set(folder.dstAbsPath, result);
464+
return result;
433465
}
434466

435467
async getChildren(element?: Element): Promise<Element[]> {
@@ -555,8 +587,10 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
555587
this.log(`${diff.length} diff entries (${untrackedCount} untracked)`);
556588

557589
const newFilePaths = new Set<string>();
590+
const newFolderPaths = new Set<string>();
558591
// Collect files that need mtime checking for async batch processing
559592
const filesToCheckMtime: Array<{filePath: string, stateInfo: CheckboxStateInfo}> = [];
593+
let checkboxesWereReset = false;
560594

561595
for (const entry of diff) {
562596
const folder = path.dirname(entry.dstAbsPath);
@@ -575,6 +609,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
575609
if (!files.has(currentFolder)) {
576610
files.set(currentFolder, new Array());
577611
}
612+
newFolderPaths.add(currentFolder);
578613
currentFolder = path.dirname(currentFolder)
579614
}
580615

@@ -600,8 +635,8 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
600635
const stats = await fs.promises.stat(filePath);
601636
const fileMtime = stats.mtimeMs;
602637

603-
// If file was modified after checkbox was checked, reset it
604-
if (fileMtime > stateInfo.timestamp) {
638+
// If file was modified at or after the checkbox was checked, reset it
639+
if (fileMtime >= stateInfo.timestamp) {
605640
return filePath;
606641
}
607642
} catch (error: unknown) {
@@ -614,23 +649,30 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
614649

615650
const pathsToReset = await Promise.all(statPromises);
616651
const actualPathsToReset = pathsToReset.filter((filePath): filePath is string => filePath !== null);
617-
actualPathsToReset.forEach(filePath => this.checkboxStates.delete(filePath));
618652

619-
// Fire tree refresh to update checkbox UI
620653
if (actualPathsToReset.length > 0) {
621-
this._onDidChangeTreeData.fire();
654+
// Clear folder checkbox state cache before deleting checkbox states
655+
this.folderCheckboxStateCache.clear();
656+
actualPathsToReset.forEach(filePath => this.checkboxStates.delete(filePath));
622657
}
658+
659+
// Track if checkboxes were reset to consolidate tree refresh later
660+
checkboxesWereReset = actualPathsToReset.length > 0;
623661
}
624662

625-
// Clear checkbox state for files that no longer exist in the diff
663+
// Clear checkbox state for files and folders that no longer exist in the diff
626664
const pathsToDelete: string[] = [];
627-
for (const [filePath] of this.checkboxStates) {
628-
if (!newFilePaths.has(filePath)) {
629-
pathsToDelete.push(filePath);
665+
for (const [itemPath] of this.checkboxStates) {
666+
// Only delete if it's not in files AND not in folders
667+
if (!newFilePaths.has(itemPath) && !newFolderPaths.has(itemPath)) {
668+
pathsToDelete.push(itemPath);
630669
}
631670
}
632-
for (const filePath of pathsToDelete) {
633-
this.checkboxStates.delete(filePath);
671+
if (pathsToDelete.length > 0) {
672+
this.folderCheckboxStateCache.clear();
673+
for (const itemPath of pathsToDelete) {
674+
this.checkboxStates.delete(itemPath);
675+
}
634676
}
635677

636678
let treeHasChanged = false;
@@ -679,7 +721,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
679721
this.filesInsideTreeRoot = filesInsideTreeRoot;
680722
this.filesOutsideTreeRoot = filesOutsideTreeRoot;
681723

682-
if (fireChangeEvents && treeHasChanged) {
724+
if (fireChangeEvents && (treeHasChanged || checkboxesWereReset)) {
683725
this.log('Refreshing tree')
684726
this._onDidChangeTreeData.fire();
685727
}

0 commit comments

Comments
 (0)