Register inverse operations at the Writer boundary
The store is the Writer boundary, so it computes and registers inverses: a weak history sink bound at session composition, one HistoryStep per gesture at exactly the brackets that were already one performWrite each — multi-card moves, style batches, width pairs, and multi-row restores each undo as one plurally-titled step, and the Edit session registers once at the flip from the bytes disk held before its first landed write, debounce ticks registering nothing. Crossings run through performWrite, so an undo brackets the watcher, echoes through the reload, and reaches every window; every closure captures values, never snapshots. The inventory follows 13 exactly: moves return to origin lane and order, renames restore or remove the title key, restyles and resizes restore field values or absence, tombstones and restores swap with captured timestamps, and an undone create is a real removal — no trace — with redo re-materializing the same UUID from bytes captured at gesture time. Purge, attachments, repair, bookkeeping, checkbox flips, raw Apply, and the whole arrival family register nothing, each exclusion documented where it lives. Step names speak 06's verb vocabulary through the new HistoryPhrase. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -84,6 +84,27 @@ public final class CardBodyEditSession {
|
||||
@ObservationIgnored
|
||||
public var save: ((String) -> CardBodyWriteOutcome)?
|
||||
|
||||
/// Where this session's **one undo step** goes, called at `endEditSession()` with the body disk
|
||||
/// held when the session's first save landed and the body it holds now (13-native-undo.md
|
||||
/// ▸ Rules: "an Edit session is one step, registered at the Edit→Preview flip").
|
||||
///
|
||||
/// A closure for `save`'s reason exactly — this type is a buffer and a clock, and it stays
|
||||
/// testable by having no idea what a board or an undo stack is. `CardWindowHost` points it at
|
||||
/// `BoardStore.registerBodyEdit(inCard:priorBody:newBody:)`, which builds the step; `nil` is a
|
||||
/// session whose window has not joined its board, and registers nothing.
|
||||
@ObservationIgnored
|
||||
public var registerUndo: ((_ priorBody: String, _ newBody: String) -> Void)?
|
||||
|
||||
/// What disk said before this session's **first** landed save — the step's before-value, held
|
||||
/// from the first write until the session ends.
|
||||
///
|
||||
/// `nil` means "no save has landed in this session", which is also what a session that only ever
|
||||
/// read looks like: nothing was written, so there is nothing to undo and no step to register. It
|
||||
/// is captured at the *write*, not at Edit entry, so that a session opened and abandoned leaves
|
||||
/// the stack exactly as it found it.
|
||||
@ObservationIgnored
|
||||
private var sessionOriginBody: String?
|
||||
|
||||
@ObservationIgnored
|
||||
private var pending: Task<Void, Never>?
|
||||
|
||||
@@ -142,7 +163,17 @@ public final class CardBodyEditSession {
|
||||
/// doc comment).
|
||||
@discardableResult
|
||||
public func endEditSession() -> CardBodyWriteOutcome {
|
||||
flush()
|
||||
let outcome = flush()
|
||||
// The session's one undo step, registered here and nowhere else — see `registerUndo`. It
|
||||
// fires only when something of this session's actually landed: `sessionOriginBody` is set by
|
||||
// the first successful write, and the guard against `disk` covers the session that typed its
|
||||
// way back to where it started across several ticks (each of which wrote, so the origin is
|
||||
// set, but whose net effect on the file is nothing to undo).
|
||||
if let origin = sessionOriginBody, origin != disk {
|
||||
registerUndo?(origin, disk)
|
||||
}
|
||||
sessionOriginBody = nil
|
||||
return outcome
|
||||
}
|
||||
|
||||
/// `DirtyBufferGuard`'s `attemptSave`: the same flush, with a real failure raised instead of
|
||||
@@ -197,9 +228,19 @@ public final class CardBodyEditSession {
|
||||
guard let save else { return .vanished }
|
||||
|
||||
saveAttempts += 1
|
||||
// Read before the write, because the write is what makes it stale: this is the body the
|
||||
// session is about to start overwriting, and only the *first* landed save of a session may
|
||||
// claim it (13-native-undo.md ▸ Rules — one step per session, not per tick).
|
||||
let priorBody = disk
|
||||
let outcome = save(text)
|
||||
switch outcome {
|
||||
case .written, .unchanged:
|
||||
case .written:
|
||||
if sessionOriginBody == nil { sessionOriginBody = priorBody }
|
||||
disk = text
|
||||
case .unchanged:
|
||||
// Nothing was replaced — the file already read like the buffer, so this session has
|
||||
// overwritten nothing yet and has nothing to hand an undo. (It is also the one outcome
|
||||
// where `disk` was demonstrably wrong, so the bytes it held are not a state to restore.)
|
||||
disk = text
|
||||
case .suspended, .vanished, .failed:
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user