Fix Set as base leaving one side on its old contents

Setting a new base called setSide twice, so the first call kicked off a
comparison against the *old* other side. Scans run concurrently, so that stale
result could land last and win: the left had zoomed into the archive while the
right still showed the parent, which is exactly what it looked like on screen.

Both sides are now set in one go before comparing, and every scan is stamped so a
slower, older run can no longer overwrite a newer result.

Also renamed the pairing entries to mention 'set as base', since marking one side
and picking the other achieves the same thing for items whose names differ and
that wasn't obvious from the wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
This commit is contained in:
alexandrev-tibco
2026-07-31 14:46:59 +02:00
parent 5331bb5283
commit 8e4a3e5f62
5 changed files with 31 additions and 11 deletions
+21 -4
View File
@@ -32,6 +32,8 @@ final class ComparisonModel: Identifiable {
/// Which folders are open. Held here rather than by List's outline so both
/// sides of a row can carry a disclosure control.
private var expanded: Set<String> = []
/// Stamps each scan so a stale one can't overwrite a newer result.
private var scanGeneration = 0
/// One side of a manual pairing, held while the other is chosen. Lets two
/// files with different names be compared, which the tree alone can't express.
@@ -164,6 +166,17 @@ final class ComparisonModel: Identifiable {
if isReady { compare() }
}
/// Sets both sides at once. Doing it with two `setSide` calls starts a
/// comparison against the *old* other side, and since scans run concurrently
/// that stale result can land last and win which showed up as one side
/// zooming in while the other kept its previous contents.
func setBoth(left: URL, right: URL) {
leftURL = left
rightURL = right
selection = nil
compare()
}
func setSide(_ side: Side, to url: URL) {
switch side {
case .left: leftURL = url
@@ -185,10 +198,7 @@ final class ComparisonModel: Identifiable {
guard !urls.isEmpty else { return }
if urls.count >= 2 {
leftURL = urls[0]
rightURL = urls[1]
selection = nil
compare()
setBoth(left: urls[0], right: urls[1])
return
}
@@ -235,18 +245,25 @@ final class ComparisonModel: Identifiable {
errorMessage = nil
selection = nil
// Scans run off the main actor and a slow one can finish after a newer
// one; stamping each run means only the latest is allowed to land.
scanGeneration &+= 1
let generation = scanGeneration
let options = DirectoryComparer.Options(mode: mode, textOptions: textOptions)
Task.detached(priority: .userInitiated) {
do {
let left = try TreeScanner.scan(leftURL)
let right = try TreeScanner.scan(rightURL)
let diff = DirectoryComparer.compare(left: left, right: right, options: options)
await MainActor.run {
guard generation == self.scanGeneration else { return }
self.root = diff
self.isScanning = false
}
} catch {
await MainActor.run {
guard generation == self.scanGeneration else { return }
self.errorMessage = error.localizedDescription
self.isScanning = false
self.root = nil
+5 -5
View File
@@ -170,19 +170,19 @@ struct DiffTreeView: View {
// other. Works for folders too, so a folder inside a JAR can be set
// against one on disk.
if let left = node.left {
Button("Mark left for comparison") { mark(left, path: node.path) }
Button("Set as base: mark left") { mark(left, path: node.path) }
}
if let right = node.right {
Button("Mark right for comparison") { mark(right, path: node.path) }
Button("Set as base: mark right") { mark(right, path: node.path) }
}
if let pending = model.pendingPick {
Divider()
if let left = node.left {
Button("Compare left with “\(pending.name)") { compare(pending, with: left, newTab: false) }
Button("Set as base with “\(pending.name) (left)") { compare(pending, with: left, newTab: false) }
}
if let right = node.right {
Button("Compare right with “\(pending.name)") { compare(pending, with: right, newTab: false) }
Button("Compare right with “\(pending.name)” in a new tab") {
Button("Set as base with “\(pending.name) (right)") { compare(pending, with: right, newTab: false) }
Button("Set as base with “\(pending.name)” in a new tab") {
compare(pending, with: right, newTab: true)
}
}
+1 -2
View File
@@ -48,8 +48,7 @@ final class TabsModel {
/// new tab, which is what makes a hand-picked pair the new base.
func openPair(left: URL, right: URL, inNewTab: Bool) {
let target = inNewTab ? newTab() : selected
target.setSide(.left, to: left)
target.setSide(.right, to: right)
target.setBoth(left: left, right: right)
selectedID = target.id
}
+2
View File
@@ -110,3 +110,5 @@
"Paste something on each side to compare." = "Paste something on each side to compare.";
"Clear" = "Clear";
"Binary" = "Binary";
"Set as base: mark left" = "Set as base: mark left";
"Set as base: mark right" = "Set as base: mark right";
+2
View File
@@ -110,3 +110,5 @@
"Paste something on each side to compare." = "Pega algo en cada lado para comparar.";
"Clear" = "Limpiar";
"Binary" = "Binario";
"Set as base: mark left" = "Usar como base: marcar izquierda";
"Set as base: mark right" = "Usar como base: marcar derecha";