mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 08:14:38 +08:00
Keep a Notes or Snippets folder change from reusing the old folder's contents
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user