From 8e4a3e5f62a71efc7310f1f62244bf102fbae2b1 Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Fri, 31 Jul 2026 14:46:59 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN --- App/ComparisonModel.swift | 25 +++++++++++++++++++++---- App/DiffTreeView.swift | 10 +++++----- App/TabsModel.swift | 3 +-- Resources/en.lproj/Localizable.strings | 2 ++ Resources/es.lproj/Localizable.strings | 2 ++ 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/App/ComparisonModel.swift b/App/ComparisonModel.swift index d3aa8e8..684d781 100644 --- a/App/ComparisonModel.swift +++ b/App/ComparisonModel.swift @@ -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 = [] + /// 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 diff --git a/App/DiffTreeView.swift b/App/DiffTreeView.swift index 33419b4..40313cc 100644 --- a/App/DiffTreeView.swift +++ b/App/DiffTreeView.swift @@ -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) } } diff --git a/App/TabsModel.swift b/App/TabsModel.swift index 90d0b5d..cb2d16a 100644 --- a/App/TabsModel.swift +++ b/App/TabsModel.swift @@ -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 } diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index 48949c9..5a0eb89 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -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"; diff --git a/Resources/es.lproj/Localizable.strings b/Resources/es.lproj/Localizable.strings index d909eb9..0e1ab6d 100644 --- a/Resources/es.lproj/Localizable.strings +++ b/Resources/es.lproj/Localizable.strings @@ -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";