import Foundation import OrgCore public protocol FileSystem: Sendable { /// The file's bytes, or nil if it doesn't exist. func read(_ url: URL) throws -> [UInt8]? /// Replaces the file through a temporary file in the same folder. Returns the bytes that /// were replaced, read from the replaced file itself, or nil if there was none. func replace(_ url: URL, with bytes: [UInt8]) throws -> [UInt8]? } public protocol RecoveryStore: Sendable { func keep(_ bytes: [UInt8], for url: URL, label: String) throws } public enum SaveOutcome: Sendable, Equatable { case saved /// The file had changed since it was read; the change merged cleanly into the buffer and /// the merge was written. case mergedAndSaved([TextEdit]) /// The file had changed and the change conflicts. Nothing was written; the buffer is /// unchanged. case conflict([MergeConflict]) /// Another writer replaced the file between our last check and our write. Ours is on disk; /// theirs is in recovery and was merged into the buffer where possible. case overwroteExternalChange(DocumentState.ExternalChange) /// Another writer changed the file right after our write. Theirs is on disk; ours is in /// recovery. case changedAfterWrite(DocumentState.ExternalChange) } public enum SaveError: Error, Equatable { case fileKeepsChanging } /// The save sequence from the design: read, merge if the file moved, check again, replace, /// read back. Emacs and Syncthing don't coordinate, so the sequence can't lock them out; it /// narrows the window and makes sure every version it displaces lands in recovery. public struct Saver: Sendable { public let fileSystem: FileSystem public let recovery: RecoveryStore public var maxAttempts = 3 public init(fileSystem: FileSystem, recovery: RecoveryStore) { self.fileSystem = fileSystem self.recovery = recovery } public func save(_ state: inout DocumentState, to url: URL) throws -> SaveOutcome { guard state.isEditable else { throw DocumentState.EditError.readOnly } for _ in 0..