Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

58 lines
2.5 KiB
Swift

import Foundation
/// Pure state machine for one folder-title inline-rename session (design §2.3).
///
/// The hard problem it solves is the SINGLE-RESOLUTION latch: a Return both fires
/// `insertNewline` AND (via the refocus that follows) a native end-editing notification;
/// an Esc-cancel is followed by the same end-editing. Without the latch the second event
/// would commit again — after a cancel, committing the very text the user just discarded.
/// `hasResolved` guarantees a session resolves exactly once; the store's no-change guard
/// is the independent second line of defence.
///
/// Internal + value-typed so the resolution table is unit-testable without AppKit
/// (the established "extract the drag logic into internal methods" pattern).
struct LaunchpadRenameEditSession: Equatable {
/// How a session ended. `commit` carries the raw text (the store trims and applies the
/// empty-name fallback); `cancel` carries the original name the field should restore.
enum Resolution: Equatable {
case commit(String)
case cancel(restore: String)
}
private(set) var originalName: String
private(set) var currentText: String
private(set) var hasResolved = false
/// The field became first responder: snapshot the name Esc restores.
static func begin(originalName: String) -> LaunchpadRenameEditSession {
LaunchpadRenameEditSession(originalName: originalName, currentText: originalName)
}
/// Live text mirror while editing. Ignored after resolution — a stale field-editor
/// notification must not mutate a finished session.
mutating func textChanged(_ text: String) {
guard !hasResolved else { return }
currentText = text
}
/// Return key (or an explicit programmatic commit). At most one resolution per session.
mutating func commit() -> Resolution? {
guard !hasResolved else { return nil }
hasResolved = true
return .commit(currentText)
}
/// Esc: discard edits, restore the original name. At most one resolution per session.
mutating func cancel() -> Resolution? {
guard !hasResolved else { return nil }
hasResolved = true
return .cancel(restore: originalName)
}
/// Focus loss / panel unmount: commits — unless Return/Esc already resolved this
/// session, in which case this is the trailing native end-editing and must be inert.
mutating func endEditing() -> Resolution? {
commit()
}
}