Phase B of the two-level undo card: every card-window gesture — comment
post/delete/edit, body Edit sessions, style and details changes —
registers fine-grained on the window's own stack (window.undoManager
answers with it; board ⌘Z never sees mid-session card steps; an empty
window stack beeps, never falls through). Window close folds the stack
into one coarse values-based board step ("Edit card 'X'") — per-target
per-field later-wins merge, so foreign mid-session writes stay out by
construction, a no-net-change session registers nothing, and any stale
component skips the whole step. The comments/.trash purge defers with
the coarse step via a step-retirement seam on the providers: it runs
when the step leaves the board stack or the board session ends; the git
provider retires dropped steps on register, which keeps Pro's
purge-at-close-flush structural with no tier check. Interim on git
boards: gestures still auto-commit per debounce until phase C's
close-flush commit.
2432 tests in 418 suites green.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
130 lines
6.4 KiB
Swift
130 lines
6.4 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
|
|
/// tiers 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 delete — ⌫, drop-on-trash, the card window's Actions ▸ Delete — which is a *move* into
|
|
/// `.trash/` for a card and a physical removal for a lane (03-board-ui.md § Trash).
|
|
///
|
|
/// **There is no `restore` verb**: restoring is an ordinary move out (drag or ⌘X/⌘V), so it
|
|
/// registers as `move` like any other, and Put Back is retired with the tombstone model
|
|
/// (resettled 2026-07-28).
|
|
case delete = "Delete"
|
|
/// 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
|
|
/// One comment. `.delete` and `.edit` reach it — the inline edit session joined the
|
|
/// vocabulary with the window stack (re-ruled 2026-07-31: "every gesture issued in that
|
|
/// window — comment post/delete/**edit**, body Edit sessions ... registers there at fine
|
|
/// grain"), registered at its own commit point exactly as the body's session is. Posting has
|
|
/// its own phrase (`comment`, below); the draft save and the trash purge still register no
|
|
/// step at all (13-native-undo.md — the permanent-delete posture).
|
|
case comment
|
|
|
|
var singular: String {
|
|
switch self {
|
|
case .card: "Card"
|
|
case .lane: "Lane"
|
|
case .board: "Board"
|
|
case .comment: "Comment"
|
|
}
|
|
}
|
|
|
|
var plural: String {
|
|
switch self {
|
|
case .card: "Cards"
|
|
case .lane: "Lanes"
|
|
case .board: "Board"
|
|
case .comment: "Comments"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: The comment family
|
|
|
|
/// **The post's phrase** — 06-history-undo.md's path-shaped "Comment on '⟨card⟩'" as a menu
|
|
/// title, which is the verb on its own.
|
|
///
|
|
/// Not `name(_:kind:count:)` with a `Verb.comment`, because the composition would read "Comment
|
|
/// Comment": here the verb already names its object, which is `Kind.board`'s count-less rule
|
|
/// arriving from the other direction. The destination clause a commit subject carries ("on 'Fix
|
|
/// login'") is dropped exactly as every other phrase drops it — a menu row has to stay short.
|
|
public static let comment = "Comment"
|
|
|
|
// MARK: The card-window session
|
|
|
|
/// **The coarse close step's phrase** — one card window's whole session, as the board's stack sees
|
|
/// it: 13-native-undo.md ▸ Rules names it "Edit card 'Fix login'", so the menu title is the same
|
|
/// verb and noun every other card edit uses, dropping the item clause a menu row has no space for.
|
|
///
|
|
/// Deliberately *not* a new verb. The session is an edit to a card — the fine-grained things
|
|
/// inside it (a comment posted, a colour chosen, a paragraph rewritten) are the window stack's
|
|
/// story, and a board-level row that tried to enumerate them would be the "Mixed update" problem
|
|
/// in a menu (06-history-undo.md ▸ Commit messages). It reads identically to a body-edit step
|
|
/// because on the board's stack it *is* the card's edit.
|
|
public static let cardSession = name(.edit, kind: .card)
|
|
|
|
// 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)"
|
|
}
|
|
}
|