macos: simplify command palette sort keys (#13872)

Store the Comparable ObjectIdentifier directly instead of wrapping the
only sort key type in AnySortKey.

The expected deterministic ordering of equal terminal command titles is
also now verified by a unit test.
This commit is contained in:
Mitchell Hashimoto
2026-08-17 10:44:57 -07:00
committed by GitHub
4 changed files with 43 additions and 43 deletions

View File

@@ -20,7 +20,7 @@ struct CommandOption: Identifiable, Hashable {
/// Whether to visually emphasize this option.
let emphasis: Bool
/// Sort key for stable ordering when titles are equal.
let sortKey: AnySortKey?
let sortKey: ObjectIdentifier?
/// The action to perform when this option is selected.
let action: () -> Void
@@ -33,7 +33,7 @@ struct CommandOption: Identifiable, Hashable {
leadingColor: Color? = nil,
badge: String? = nil,
emphasis: Bool = false,
sortKey: AnySortKey? = nil,
sortKey: ObjectIdentifier? = nil,
action: @escaping () -> Void
) {
self.title = title

View File

@@ -1,6 +1,21 @@
import SwiftUI
import GhosttyKit
func sortedTerminalPaletteOptions(_ options: [CommandOption]) -> [CommandOption] {
options.sorted { lhs, rhs in
let lhsTitle = lhs.title.replacingOccurrences(of: ":", with: "\t")
let rhsTitle = rhs.title.replacingOccurrences(of: ":", with: "\t")
let comparison = lhsTitle.localizedCaseInsensitiveCompare(rhsTitle)
if comparison != .orderedSame {
return comparison == .orderedAscending
}
if let lhsKey = lhs.sortKey, let rhsKey = rhs.sortKey {
return lhsKey < rhsKey
}
return false
}
}
struct TerminalCommandPaletteView: View {
/// The surface that this command palette represents.
let surfaceView: Ghostty.SurfaceView
@@ -64,19 +79,7 @@ struct TerminalCommandPaletteView: View {
// Sort the rest. We replace ":" with a character that sorts before space
// so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker
// for stable ordering when titles are equal.
options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized)
if comparison != .orderedSame {
return comparison == .orderedAscending
}
// Tie-breaker: use sortKey if both have one
if let aSortKey = a.sortKey, let bSortKey = b.sortKey {
return aSortKey < bSortKey
}
return false
})
options.append(contentsOf: sortedTerminalPaletteOptions(jumpOptions + terminalOptions))
return options
}
@@ -168,7 +171,7 @@ struct TerminalCommandPaletteView: View {
subtitle: subtitle,
leadingIcon: "rectangle.on.rectangle",
leadingColor: displayColor?.displayColor.map { Color($0) },
sortKey: AnySortKey(ObjectIdentifier(surface))
sortKey: ObjectIdentifier(surface)
) {
NotificationCenter.default.post(
name: Ghostty.Notification.ghosttyPresentTerminal,

View File

@@ -1,25 +0,0 @@
import Foundation
/// Type-erased wrapper for any Comparable type to use as a sort key.
struct AnySortKey: Comparable {
private let value: Any
private let comparator: (Any, Any) -> ComparisonResult
init<T: Comparable>(_ value: T) {
self.value = value
self.comparator = { lhs, rhs in
guard let l = lhs as? T, let r = rhs as? T else { return .orderedSame }
if l < r { return .orderedAscending }
if l > r { return .orderedDescending }
return .orderedSame
}
}
static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool {
lhs.comparator(lhs.value, rhs.value) == .orderedAscending
}
static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool {
lhs.comparator(lhs.value, rhs.value) == .orderedSame
}
}

View File

@@ -14,13 +14,15 @@ struct CommandPaletteFilterTests {
title: String,
subtitle: String? = nil,
description: String? = nil,
leadingColor: Color? = nil
leadingColor: Color? = nil,
sortKey: ObjectIdentifier? = nil
) -> CommandOption {
CommandOption(
title: title,
subtitle: subtitle,
description: description,
leadingColor: leadingColor
leadingColor: leadingColor,
sortKey: sortKey
) {}
}
@@ -46,4 +48,24 @@ struct CommandPaletteFilterTests {
#expect([first, second].filteredAndSorted(query: "new") == [first, second])
#expect([second, first].filteredAndSorted(query: "new") == [second, first])
}
/// Equal titles use their sort keys independent of input order.
@Test func equalTitlesUseSortKey() {
let firstKey = NSObject()
let secondKey = NSObject()
let first = option(
title: "Focus: Shell",
subtitle: "/tmp",
sortKey: ObjectIdentifier(firstKey)
)
let second = option(
title: "Focus: Shell",
subtitle: "/tmp",
sortKey: ObjectIdentifier(secondKey)
)
let forward = sortedTerminalPaletteOptions([first, second])
let reverse = sortedTerminalPaletteOptions([second, first])
#expect(forward == reverse)
}
}