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
94 lines
4.0 KiB
Swift
94 lines
4.0 KiB
Swift
import Foundation
|
|
|
|
// MARK: - HistoryPhrase
|
|
|
|
/// The menu phrase an undo step carries — "Move 3 Cards", "Rename Lane", "Restyle Board".
|
|
///
|
|
/// ### Why the vocabulary is 06's and not a new one
|
|
///
|
|
/// 13-native-undo.md ▸ Rules hands the naming straight over: "The 06 vocabulary supplies menu titles
|
|
/// ('Undo Move 3 Cards'), via NSUndoManager's dynamic retitling — the same naming machinery both
|
|
/// editions use." So the verbs here are exactly 06-history-undo.md ▸ Commit messages' list — *Add /
|
|
/// Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, and the board*, plus
|
|
/// the trash pair's **Restore** — and the plural rule is that section's own plural folding ("Delete
|
|
/// 12 cards"), which is also 13's coalescing sentence read out loud: "a multi-card move is one step
|
|
/// with a plural title".
|
|
///
|
|
/// The **"Undo "/"Redo " prefix is never here**: the platform composes and localizes it
|
|
/// (`BoardUndoManager.undoMenuItemTitle`), and a phrase that spelled it would read "Undo Undo Move
|
|
/// Card" in the Edit menu — see `HistoryStep.name`.
|
|
///
|
|
/// ### Title case, unlike a commit subject
|
|
///
|
|
/// A commit subject is a sentence ("Move 3 cards to Done"); a menu item is a title, and macOS titles
|
|
/// its Edit-menu rows. The words are 06's; the casing is the menu's. Nothing else differs — and the
|
|
/// destination clause a commit subject carries has no place in a title that has to stay short enough
|
|
/// for a menu row.
|
|
///
|
|
/// Pure, and its own type rather than a `String` built at each call site, because a phrase composed
|
|
/// in eleven places is a vocabulary that drifts in eleven places.
|
|
public enum HistoryPhrase {
|
|
|
|
// MARK: Verbs
|
|
|
|
/// 06-history-undo.md ▸ Commit messages' verb list, restricted to the operations 13 makes
|
|
/// undoable. `Permanently delete`, `Attach`, `Remove` and `Repair` are deliberately absent —
|
|
/// those are exactly the operations that register no step at all (13 ▸ Rules ▸ what is not
|
|
/// undoable, ▸ Out of scope).
|
|
public enum Verb: String, Sendable, CaseIterable {
|
|
/// A create — File ▸ New Lane, the new-card placeholder's commit, a Finder file drop's cards.
|
|
case add = "Add"
|
|
/// A tombstone (⌫, drop-on-trash, the card window's Actions ▸ Delete).
|
|
case delete = "Delete"
|
|
/// Put Back and drag-to-restore.
|
|
case restore = "Restore"
|
|
/// A drop that changes an item's parent.
|
|
case move = "Move"
|
|
/// A drop, a lane drag or ⌥⌘↑/⌥⌘↓ that changes rank among unchanged siblings.
|
|
case reorder = "Reorder"
|
|
case rename = "Rename"
|
|
case restyle = "Restyle"
|
|
case resize = "Resize"
|
|
/// An Edit session's body save — one step at the Edit→Preview flip (13 ▸ Rules).
|
|
case edit = "Edit"
|
|
}
|
|
|
|
// MARK: Nouns
|
|
|
|
/// What the gesture acted on. `board` is deliberately count-less: there is one board, and
|
|
/// "Rename 1 Board" is not a phrase anyone writes.
|
|
public enum Kind: Sendable, Equatable {
|
|
case card
|
|
case lane
|
|
case board
|
|
|
|
var singular: String {
|
|
switch self {
|
|
case .card: "Card"
|
|
case .lane: "Lane"
|
|
case .board: "Board"
|
|
}
|
|
}
|
|
|
|
var plural: String {
|
|
switch self {
|
|
case .card: "Cards"
|
|
case .lane: "Lanes"
|
|
case .board: "Board"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Composition
|
|
|
|
/// The phrase for one gesture: `"Move Card"`, `"Move 3 Cards"`, `"Restyle Board"`.
|
|
///
|
|
/// A `count` of one or less folds to the singular — a batch that turned out to name a single item
|
|
/// is one item, and a zero never reaches here because a gesture that wrote nothing registers
|
|
/// nothing.
|
|
public static func name(_ verb: Verb, kind: Kind, count: Int = 1) -> String {
|
|
guard count > 1, kind != .board else { return "\(verb.rawValue) \(kind.singular)" }
|
|
return "\(verb.rawValue) \(count) \(kind.plural)"
|
|
}
|
|
}
|