From c72771c954059a531d38338786f4c1e10383adb0 Mon Sep 17 00:00:00 2001 From: Joncallim <64296013+Joncallim@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:55:08 +0800 Subject: [PATCH] Keep Open Recent entries valid for files Replace in Folder rewrites Co-Authored-By: Claude Sonnet 5.5 --- .../MacDown2/FolderSearchModel+Replace.swift | 6 ++++++ MacDown2/MacDown2/FolderSearchModel.swift | 4 ++++ .../WindowController+WorkspaceIndex.swift | 3 +++ .../MacDown2Tests/FolderReplaceModelTests.swift | 16 ++++++++++++++++ 4 files changed, 29 insertions(+) diff --git a/MacDown2/MacDown2/FolderSearchModel+Replace.swift b/MacDown2/MacDown2/FolderSearchModel+Replace.swift index 595d3481..6a2ec4b2 100644 --- a/MacDown2/MacDown2/FolderSearchModel+Replace.swift +++ b/MacDown2/MacDown2/FolderSearchModel+Replace.swift @@ -128,6 +128,12 @@ extension FolderSearchModel { replaceCompleted += 1 } ) + // Notified even when the run was superseded: the files are already rewritten on disk. + for result in results { + if case .replaced = result.outcome { + fileWasRewritten(root.appendingPathComponent(result.relativePath)) + } + } guard replaceGeneration == thisGeneration else { return } replaceSummary = Self.summarize(results) isReplacing = false diff --git a/MacDown2/MacDown2/FolderSearchModel.swift b/MacDown2/MacDown2/FolderSearchModel.swift index 2a1ae855..21701ab0 100644 --- a/MacDown2/MacDown2/FolderSearchModel.swift +++ b/MacDown2/MacDown2/FolderSearchModel.swift @@ -72,6 +72,10 @@ final class FolderSearchModel { /// overwritten on disk. Assigned by `WindowController`, which knows the /// coordinator that can see every window's tabs. var hasUnsavedOpenDocument: @MainActor (URL) -> Bool = { _ in false } + /// Called once for every file Replace in Folder rewrote. A rewrite publishes a new inode and creation date, which + /// the Open Recent fingerprint rejects, so the entry for that file must follow it + /// (`RecentFileDocuments.noteWrite`). + var fileWasRewritten: @MainActor (URL) -> Void = { _ in } let performReplace: FolderReplaceRunner let performPreview: FolderReplacePreviewBuilder var replaceTask: Task? diff --git a/MacDown2/MacDown2/WindowController+WorkspaceIndex.swift b/MacDown2/MacDown2/WindowController+WorkspaceIndex.swift index 1a8da2f7..50bd96a6 100644 --- a/MacDown2/MacDown2/WindowController+WorkspaceIndex.swift +++ b/MacDown2/MacDown2/WindowController+WorkspaceIndex.swift @@ -11,6 +11,9 @@ extension WindowController { folderSearchModel.hasUnsavedOpenDocument = { [weak coordinator] url in coordinator?.hasUnsavedOpenDocument(at: url) ?? false } + folderSearchModel.fileWasRewritten = { [weak coordinator] url in + coordinator?.recentFileDocuments.noteWrite(of: url) + } fileTreeModel.onDidMutate = { [weak self] in Task { await self?.refreshWorkspaceIndex() } } diff --git a/MacDown2/MacDown2Tests/FolderReplaceModelTests.swift b/MacDown2/MacDown2Tests/FolderReplaceModelTests.swift index ac5492ea..1d1e8528 100644 --- a/MacDown2/MacDown2Tests/FolderReplaceModelTests.swift +++ b/MacDown2/MacDown2Tests/FolderReplaceModelTests.swift @@ -84,6 +84,22 @@ struct FolderReplaceModelTests { #expect(model.replaceSummary?.replacedFiles == 1) } + /// Review pass 6: a rewritten file gets a new inode and creation date, so its Open Recent entry was rejected by + /// the fingerprint check and silently dropped on the next click. + @Test func everyRewrittenFileIsReportedSoRecentsCanFollowIt() async throws { + let tree = try TempTree(["a.txt": "foo a", "b.txt": "foo b", "c.txt": "none"]) + let model = await searchedModel(tree, query: "foo") + var rewritten: [String] = [] + model.fileWasRewritten = { rewritten.append($0.lastPathComponent) } + model.replacement = "bar" + + model.requestReplace() + model.confirmReplace() + await waitUntil { model.replaceSummary != nil && model.outcome != nil } + + #expect(rewritten.sorted() == ["a.txt", "b.txt"]) + } + @Test func aFileOpenWithUnsavedChangesIsSkippedAndReported() async throws { let tree = try TempTree(["dirty.txt": "foo dirty", "clean.txt": "foo clean"]) let model = await searchedModel(tree, query: "foo")