Build the HistoryProviding seam and the per-board native undo stack
The provider seam 12 promised: HistoryProviding speaks 13's vocabulary — register a HistoryStep (bare 06 phrase plus undo/redo closures returning applied or skipped), canUndo/canRedo, action names, clear — and no UndoManager type appears anywhere in it, proven by a fake that satisfies the seam with counters. The base provider wraps a private UndoManager with groupsByEvent off so coalescing stays the Writer call site's decision; undo re-registers the reversed step from inside the undo, which makes a stale-skipped step vanish for free and the crossing loop fall through to the next. BoardUndoManager adapts the protocol to the responder chain — a stackless UndoManager subclass answering from the provider — so Pro's git provider inherits menu enablement, dynamic titles, and the nil-target toolbar pair by binding the protocol. One stack per board session, born in beginSession, cleared in the close flush; every window over the board answers it through windowWillReturnUndoManager. Headless probes shaped the routing: a real NSTextView's own manager wins natively, but a field editor's does not — BoardUndoRouting answers the per-window text manager while any NSText is first responder, so a search-field typo never crosses a board step. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -56,6 +56,26 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
||||
/// window that has nothing to flush.
|
||||
var onCloseRequested: (() -> Void)?
|
||||
|
||||
/// This window's board undo stack, asked for afresh every time AppKit wants it — the board
|
||||
/// window's and its card windows' shared answer (13-native-undo.md ▸ Rules: "one stack per
|
||||
/// board, owned by the board session ... `window.undoManager` for board surfaces returns the
|
||||
/// session's manager").
|
||||
///
|
||||
/// A closure rather than a stored manager for two reasons: the session does not exist yet when
|
||||
/// the window attaches, and it stops existing at teardown while the window is still closing —
|
||||
/// answering `nil` then is what keeps a torn-down board's stack from being reachable through a
|
||||
/// window that outlived it by a run-loop turn.
|
||||
///
|
||||
/// `nil` on every window that is not showing a board (welcome, the bootstrap, the template
|
||||
/// chooser), which `BoardUndoRouting` reads as "the platform default".
|
||||
var boardUndoManager: (() -> UndoManager?)?
|
||||
|
||||
/// The text manager this window hands back while a field editor holds the keyboard, and the one
|
||||
/// it hands back when there is no board — 06-history-undo.md ▸ Undo routing, via
|
||||
/// `BoardUndoRouting`. Created on demand, per window, which is what AppKit itself would have
|
||||
/// done for a window whose delegate answered nothing.
|
||||
private lazy var textUndoManager = UndoManager()
|
||||
|
||||
/// Set by `closeAfterFlush()` so the re-entrant `windowShouldClose` lets the close through
|
||||
/// instead of starting a second flush.
|
||||
private var isFlushed = false
|
||||
@@ -189,6 +209,29 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
/// The window-level half of 06-history-undo.md ▸ Undo routing (see `BoardUndoRouting`, which
|
||||
/// owns the rule and the reasoning): the board's stack when the keyboard is on the board, a
|
||||
/// text manager of this window's own while a field editor has it.
|
||||
///
|
||||
/// **Answered here rather than forwarded**, unlike the proxy's other selectors, on the one
|
||||
/// condition that this window has a board: `responds(to:)` reports this method whatever the
|
||||
/// previous delegate does, so a `nil` return would leave a window with *no* undo manager at all
|
||||
/// rather than the one AppKit creates for a delegate that stays silent. A window with no board
|
||||
/// still defers to SwiftUI's delegate if it has an opinion.
|
||||
func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? {
|
||||
let board = boardUndoManager?()
|
||||
if board == nil, let previousDelegate,
|
||||
previousDelegate.responds(to: #selector(NSWindowDelegate.windowWillReturnUndoManager(_:))),
|
||||
let inherited = previousDelegate.windowWillReturnUndoManager?(window) {
|
||||
return inherited
|
||||
}
|
||||
return BoardUndoRouting.undoManager(
|
||||
isTextEditing: BoardUndoRouting.isTextEditing(window.firstResponder),
|
||||
board: board,
|
||||
textFallback: textUndoManager
|
||||
)
|
||||
}
|
||||
|
||||
func windowDidMove(_ notification: Notification) {
|
||||
reportFrame()
|
||||
previousDelegate?.windowDidMove?(notification)
|
||||
|
||||
Reference in New Issue
Block a user