Keep a Notes or Snippets folder change from reusing the old folder's contents

This commit is contained in:
abue-ammar
2026-09-27 01:47:50 +06:00
parent 7ddae5000c
commit ed6230780b
3 changed files with 58 additions and 3 deletions
+36
View File
@@ -17,6 +17,7 @@ struct NotesTests {
try await testStoreCollectionAndAutosave()
try await testCollectionMutationsFlushTheDraft()
try await testStoreRecoversFromFailures()
try await testStoreRelocates()
print(failures == 0 ? "Notes tests passed" : "\(failures) tests failed")
exit(failures == 0 ? 0 : 1)
@@ -414,6 +415,41 @@ struct NotesTests {
store.stop()
}
private static func testStoreRelocates() async throws {
let root = temporaryRoot("relocation")
defer { try? FileManager.default.removeItem(at: root) }
let first = try repository(in: root, support: root.appendingPathComponent("first"))
let second = try repository(in: root, support: root.appendingPathComponent("second"))
let fm = FileManager.default
try fm.createDirectory(at: first.notesDirectory, withIntermediateDirectories: true)
try fm.createDirectory(at: second.notesDirectory, withIntermediateDirectories: true)
let firstURL = first.fileURL(for: NoteID(rawValue: "Plan.md"))
try Data("plan".utf8).write(to: firstURL, options: .atomic)
let unreadable = second.fileURL(for: NoteID(rawValue: "Broken.md"))
try Data([0xFF]).write(to: unreadable, options: .atomic)
let store = NotesStore(repository: first)
_ = await store.start()
store.updateSource("unsaved plan")
try fm.setAttributes([.posixPermissions: 0o555], ofItemAtPath: first.notesDirectory.path)
await store.relocate(to: second)
try fm.setAttributes([.posixPermissions: 0o755], ofItemAtPath: first.notesDirectory.path)
check(
"a draft the old folder can't take keeps the store there",
store.notesDirectory == first.notesDirectory && store.source == "unsaved plan")
_ = await store.retrySave()
for _ in 0..<100 where store.notesDirectory != second.notesDirectory {
try await Task.sleep(for: .milliseconds(10))
}
check(
"the draft is saved where it was, then the move goes ahead",
try String(contentsOf: firstURL, encoding: .utf8) == "unsaved plan"
&& store.notesDirectory == second.notesDirectory)
check("a folder that fails to load leaves no old note open", store.activeID == nil)
store.stop()
}
private static func testMarkdownParser() {
let tiled = ["a\nb\n", "a\r\nb", "\n\n", "x", "a\u{2029}b\rc"]
check("an empty source is one empty line", NoteMarkdownParser.parse("").lines.map(\.kind) == [.blank])
@@ -39,6 +39,8 @@ final class NotesStore {
@ObservationIgnored private var searchTask: Task<Void, Never>?
@ObservationIgnored private var searchWorker: Task<[NoteSearchResult], Never>?
private var saveFailed = false
/// A folder change that waits on a draft the old folder could not take yet.
@ObservationIgnored private var pendingRelocation: NotesRepository?
private var searchGeneration = 0
init(
@@ -63,19 +65,25 @@ final class NotesStore {
guard isLoaded else { return await reload(preferredID: loadSelection()) }
let repository = repository
let result = await detached({ try repository.list() }, recover: { repository.notesDirectory })
guard repository.notesDirectory == notesDirectory else { return true }
if case .success(let summaries) = result { self.summaries = summaries }
return true
}
/// Moves to another folder: the open draft is saved where it was, then the new one lists.
/// Moves to another folder once the open draft is saved where it was, then the new one lists.
func relocate(to repository: NotesRepository) async {
pendingRelocation = nil
guard repository.notesDirectory != notesDirectory else { return }
await flush()
guard await flush() else {
pendingRelocation = repository
return
}
cancelSearch()
self.repository = repository
notesDirectory = repository.notesDirectory
saveFailed = false
guard isLoaded else { return }
// Cleared first, so a folder that fails to load leaves no old note to save into it.
apply(nil, summaries: [])
_ = await reload(preferredID: nil)
}
@@ -305,6 +313,8 @@ final class NotesStore {
} recover: {
repository.notesDirectory
}
// A relocation while this ran owns the editor now, and loads it itself.
guard repository.notesDirectory == notesDirectory else { return true }
switch result {
case .success(let payload):
apply(payload.1, summaries: payload.0)
@@ -338,6 +348,9 @@ final class NotesStore {
case .success(let summaries):
self.summaries = summaries
isDirty = savedSource != source
if !isDirty, let pending = pendingRelocation {
Task { [weak self] in await self?.relocate(to: pending) }
}
case .failure(let failure):
saveFailed = true
publish(.save(failure))
@@ -63,6 +63,12 @@ final class SnippetsStore {
stop()
self.repository = repository
snippetsDirectory = repository.snippetsDirectory
// Emptied first, so a folder that fails to load leaves no old snippet expanding.
if !snippets.isEmpty || !issues.isEmpty {
snippets = []
issues = []
onSnapshot?(SnippetRepository.Snapshot(records: [], issues: []))
}
guard wasStarted else { return }
await start()
}