Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions src/providers/foldersCompareProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,10 @@ export class CompareFoldersProvider implements TreeDataProvider<File> {
}

chooseFoldersAndCompare = async (ignoreWorkspace = false) => {
await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Compare folders...`,
},
async () => {
this.handleDiffResult(
await chooseFoldersAndCompare(
ignoreWorkspace ? undefined : await this.getWorkspaceFolder()
)
);
}
this.handleDiffResult(
await chooseFoldersAndCompare(
ignoreWorkspace ? undefined : await this.getWorkspaceFolder()
)
);
};

Expand Down Expand Up @@ -217,10 +209,10 @@ export class CompareFoldersProvider implements TreeDataProvider<File> {
}
try {
if (shouldCompareFolders) {
this._diffs = (await compareFolders());
this._diffs = await compareFolders();

this.filterIgnoredFromDiffs();
if (shouldShowInfoMessage && this._diffs.hasResult()) {
if (shouldShowInfoMessage && this._diffs?.hasResult()) {
showInfoMessageWithTimeout('Source Refreshed');
}
}
Expand Down
212 changes: 150 additions & 62 deletions src/services/comparer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { commands, Uri, window, extensions } from 'vscode';
import { compare, fileCompareHandlers,type Difference } from 'dir-compare';
import { commands, Uri, window, extensions, ProgressLocation } from 'vscode';
import { compare, fileCompareHandlers, type Difference, type Entry, type DifferenceState, type Reason, type PermissionDeniedState, type DiffSet, type InitialStatistics } from 'dir-compare';
import { openFolder } from './openFolder';
import * as path from 'path';
import { DiffViewTitle, getConfiguration } from './configuration';
Expand Down Expand Up @@ -127,67 +127,155 @@ function getOptions() {

export async function compareFolders(): Promise<CompareResult> {
const emptyResponse = () => Promise.resolve(new CompareResult([], [], [], [], [], '', ''));
try {
if (!validate()) {
return emptyResponse();
}
const [folder1Path, folder2Path] = pathContext.getPaths();
validatePermissions(folder1Path, folder2Path);
const showIdentical = getConfiguration('showIdentical');
const options = getOptions();
const concatenatedOptions: CompareOptions = {
compareContent: true,
handlePermissionDenied: true,
...options,
};
// do the comparison
const res = await compare(folder1Path, folder2Path, concatenatedOptions);
printOptions(options);
printResult(res);

// get the diffs
const { diffSet = [] } = res;

// diffSet contains all the files and filter only the not equals files and map them to pairs of Uris
const distinct: DiffPathss = diffSet
.filter((diff) => diff.state === 'distinct')
.map((diff) => [path.join(diff.path1!, diff.name1!), path.join(diff.path2!, diff.name2!)]);

// readable 👍 performance 👎
const left: ViewOnlyPaths = diffSet
.filter((diff) => diff.state === 'left' && diff.type1 === 'file')
.map((diff) => [buildPath(diff, '1')]);

const right: ViewOnlyPaths = diffSet
.filter((diff) => diff.state === 'right' && diff.type2 === 'file')
.map((diff) => [buildPath(diff, '2')]);

const identicals: ViewOnlyPaths = showIdentical
? diffSet
.filter((diff) => diff.state === 'equal' && diff.type1 === 'file')
.map((diff) => [buildPath(diff, '1')])
: [];

const unaccessibles = diffSet
.filter((diff) => diff.permissionDeniedState !== 'access-ok')
.map((diff) =>
buildPath(diff, diff.permissionDeniedState === 'access-error-left' ? '1' : '2')
);

return window.withProgress(
{
location: ProgressLocation.Notification,
title: 'Comparing folders...',
cancellable: false,
},
async (progress) => {
try {
if (!validate()) {
return emptyResponse();
}
const [folder1Path, folder2Path] = pathContext.getPaths();
validatePermissions(folder1Path, folder2Path);
const showIdentical = getConfiguration('showIdentical');
const options = getOptions();

// First, do a quick scan to count total entries for percentage calculation
progress.report({ message: 'Counting all entries...' });
let totalEntries = 0;
const countingBuilder = () => {
totalEntries++;
};

try {
await compare(folder1Path, folder2Path, {
compareContent: false, // Fast scan without content comparison
compareSize: false,
noDiffSet: true, // Don't build diff set for counting
resultBuilder: countingBuilder,
});
progress.report({ message: `Found ${totalEntries} entries (all files/folders), comparing...` });
} catch (error) {
log('Warning: Failed to count entries, proceeding without percentage', error);
totalEntries = 0; // Fallback to showing count without total
}

// Now do the actual comparison with progress tracking
let processedCount = 0;

// Create a custom result builder to track progress
const progressTrackingBuilder = (
entry1: Entry | undefined,
entry2: Entry | undefined,
state: DifferenceState,
level: number,
relativePath: string,
options: CompareOptions,
statistics: InitialStatistics,
diffSet: DiffSet | undefined,
reason: Reason | undefined,
permissionDeniedState: PermissionDeniedState
) => {
processedCount++;

// Calculate percentage
const percentage = totalEntries > 0 ? Math.floor((processedCount / totalEntries) * 100) : 0;

// Report progress to VS Code UI with count and percentage
const currentPath = relativePath || '/';
if (totalEntries > 0) {
progress.report({
message: `${processedCount}/${totalEntries} (${percentage}%) - ${currentPath}`,
});
} else {
// Fallback when total is unknown (counting failed)
progress.report({
message: `${processedCount} entries - ${currentPath}`,
});
}

// Call default result builder behavior - add to diffSet if not disabled
if (!options.noDiffSet && diffSet) {
diffSet.push({
path1: entry1 ? path.dirname(entry1.path) : undefined,
path2: entry2 ? path.dirname(entry2.path) : undefined,
relativePath: relativePath,
name1: entry1 ? entry1.name : undefined,
name2: entry2 ? entry2.name : undefined,
state: state,
type1: entry1 ? (entry1.isBrokenLink ? 'broken-link' : entry1.isDirectory ? 'directory' : 'file') : 'missing',
type2: entry2 ? (entry2.isBrokenLink ? 'broken-link' : entry2.isDirectory ? 'directory' : 'file') : 'missing',
level: level,
size1: entry1 ? entry1.stat.size : undefined,
size2: entry2 ? entry2.stat.size : undefined,
date1: entry1 ? entry1.stat.mtime : undefined,
date2: entry2 ? entry2.stat.mtime : undefined,
reason: reason,
permissionDeniedState: permissionDeniedState
});
}
};

const concatenatedOptions: CompareOptions = {
compareContent: true,
handlePermissionDenied: true,
...options,
resultBuilder: progressTrackingBuilder,
};
// do the comparison
const res = await compare(folder1Path, folder2Path, concatenatedOptions);
printOptions(options);
printResult(res);

return new CompareResult(
distinct,
left,
right,
identicals,
unaccessibles,
folder1Path,
folder2Path
);
} catch (error) {
log('error while comparing', error);
showErrorMessage('Oops, something went wrong while comparing', error);
return emptyResponse();
}
// get the diffs
const { diffSet = [] } = res;

// diffSet contains all the files and filter only the not equals files and map them to pairs of Uris
const distinct: DiffPathss = diffSet
.filter((diff) => diff.state === 'distinct')
.map((diff) => [path.join(diff.path1!, diff.name1!), path.join(diff.path2!, diff.name2!)]);

// readable 👍 performance 👎
const left: ViewOnlyPaths = diffSet
.filter((diff) => diff.state === 'left' && diff.type1 === 'file')
.map((diff) => [buildPath(diff, '1')]);

const right: ViewOnlyPaths = diffSet
.filter((diff) => diff.state === 'right' && diff.type2 === 'file')
.map((diff) => [buildPath(diff, '2')]);

const identicals: ViewOnlyPaths = showIdentical
? diffSet
.filter((diff) => diff.state === 'equal' && diff.type1 === 'file')
.map((diff) => [buildPath(diff, '1')])
: [];

const unaccessibles = diffSet
.filter((diff) => diff.permissionDeniedState !== 'access-ok')
.map((diff) =>
buildPath(diff, diff.permissionDeniedState === 'access-error-left' ? '1' : '2')
);

return new CompareResult(
distinct,
left,
right,
identicals,
unaccessibles,
folder1Path,
folder2Path
);
} catch (error) {
log('error while comparing', error);
showErrorMessage('Oops, something went wrong while comparing', error);
return emptyResponse();
}
}
);
}

function buildPath(diff: Difference, side: '1' | '2') {
Expand Down
48 changes: 21 additions & 27 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,7 @@
resolved "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz"
integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==

"@types/node@*":
version "24.1.0"
resolved "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz"
integrity sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==
dependencies:
undici-types "~7.8.0"

"@types/node@^10.12.21":
"@types/node@*", "@types/node@^10.12.21":
version "10.17.60"
resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
Expand Down Expand Up @@ -1005,12 +998,7 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"

safe-buffer@^5.1.0:
version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==

safe-buffer@~5.1.0, safe-buffer@~5.1.1:
safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
Expand Down Expand Up @@ -1076,7 +1064,16 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"

string-width@^4.1.0, string-width@^4.2.0:
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -1120,7 +1117,14 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"

supports-color@^7.1.0, supports-color@^7.2.0:
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"

supports-color@^7.2.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
Expand Down Expand Up @@ -1177,12 +1181,7 @@ tsutils@^2.29.0:
dependencies:
tslib "^1.8.1"

type-detect@^4.0.8:
version "4.1.0"
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz"
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==

type-detect@4.0.8:
type-detect@^4.0.8, type-detect@4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
Expand All @@ -1192,11 +1191,6 @@ typescript@^4.7.4, "typescript@>=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==

undici-types@~7.8.0:
version "7.8.0"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz"
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==

universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
Expand Down