Tighten command palette search cache keys
This commit is contained in:
parent
8a05c7d1da
commit
661cb99da6
2 changed files with 478 additions and 109 deletions
|
|
@ -1257,6 +1257,7 @@ struct ContentView: View {
|
|||
@State private var commandPaletteMode: CommandPaletteMode = .commands
|
||||
@State private var commandPaletteRenameDraft: String = ""
|
||||
@State private var commandPaletteSelectedResultIndex: Int = 0
|
||||
@State private var commandPaletteSelectionAnchorCommandID: String?
|
||||
@State private var commandPaletteHoveredResultIndex: Int?
|
||||
@State private var commandPaletteScrollTargetIndex: Int?
|
||||
@State private var commandPaletteScrollTargetAnchor: UnitPoint?
|
||||
|
|
@ -1270,7 +1271,7 @@ struct ContentView: View {
|
|||
@State private var commandPaletteSearchRequestID: UInt64 = 0
|
||||
@State private var commandPaletteResolvedSearchRequestID: UInt64 = 0
|
||||
@State private var isCommandPaletteSearchPending = false
|
||||
@State private var commandPaletteDeferredSubmitRequestID: UInt64?
|
||||
@State private var commandPalettePendingActivation: CommandPalettePendingActivation?
|
||||
@State private var commandPaletteResultsRevision: UInt64 = 0
|
||||
@State private var commandPaletteUsageHistoryByCommandId: [String: CommandPaletteUsageEntry] = [:]
|
||||
@AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
|
||||
|
|
@ -1291,6 +1292,16 @@ struct ContentView: View {
|
|||
case switcher
|
||||
}
|
||||
|
||||
enum CommandPalettePendingActivation: Equatable {
|
||||
case selected(requestID: UInt64, fallbackSelectedIndex: Int, preferredCommandID: String?)
|
||||
case command(requestID: UInt64, commandID: String)
|
||||
}
|
||||
|
||||
enum CommandPaletteResolvedActivation: Equatable {
|
||||
case selected(index: Int)
|
||||
case command(commandID: String)
|
||||
}
|
||||
|
||||
private struct CommandPaletteRenameTarget: Equatable {
|
||||
enum Kind: Equatable {
|
||||
case workspace(workspaceId: UUID)
|
||||
|
|
@ -1412,6 +1423,13 @@ struct ContentView: View {
|
|||
func string(_ key: String) -> String? {
|
||||
stringValues[key]
|
||||
}
|
||||
|
||||
func fingerprint() -> Int {
|
||||
ContentView.commandPaletteContextFingerprint(
|
||||
boolValues: boolValues,
|
||||
stringValues: stringValues
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CommandPaletteContextKeys {
|
||||
|
|
@ -1495,6 +1513,19 @@ struct ContentView: View {
|
|||
let windowLabel: String?
|
||||
}
|
||||
|
||||
struct CommandPaletteSwitcherFingerprintWorkspace: Sendable {
|
||||
let id: UUID
|
||||
let displayName: String
|
||||
let metadata: CommandPaletteSwitcherSearchMetadata
|
||||
}
|
||||
|
||||
struct CommandPaletteSwitcherFingerprintContext: Sendable {
|
||||
let windowId: UUID
|
||||
let windowLabel: String?
|
||||
let selectedWorkspaceId: UUID?
|
||||
let workspaces: [CommandPaletteSwitcherFingerprintWorkspace]
|
||||
}
|
||||
|
||||
private static let fixedSidebarResizeCursor = NSCursor(
|
||||
image: NSCursor.resizeLeftRight.image,
|
||||
hotSpot: NSCursor.resizeLeftRight.hotSpot
|
||||
|
|
@ -2741,7 +2772,6 @@ struct ContentView: View {
|
|||
|
||||
private var commandPaletteCommandListView: some View {
|
||||
let visibleResults = cachedCommandPaletteResults
|
||||
let isSearchPending = isCommandPaletteSearchPending
|
||||
let selectedIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
let commandPaletteListMaxHeight: CGFloat = 450
|
||||
let commandPaletteRowHeight: CGFloat = 24
|
||||
|
|
@ -2780,11 +2810,6 @@ struct ContentView: View {
|
|||
.backport.onKeyPress("k") { modifiers in
|
||||
handleCommandPaletteControlNavigationKey(modifiers: modifiers, delta: -1)
|
||||
}
|
||||
|
||||
if isSearchPending {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 7)
|
||||
|
|
@ -2794,22 +2819,12 @@ struct ContentView: View {
|
|||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
if visibleResults.isEmpty {
|
||||
if isSearchPending {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Spacer()
|
||||
}
|
||||
Text(commandPaletteEmptyStateText)
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 12)
|
||||
} else {
|
||||
Text(commandPaletteEmptyStateText)
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
} else {
|
||||
ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in
|
||||
let isSelected = index == selectedIndex
|
||||
|
|
@ -2854,7 +2869,6 @@ struct ContentView: View {
|
|||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isSearchPending)
|
||||
.id(index)
|
||||
.onHover { hovering in
|
||||
if hovering {
|
||||
|
|
@ -2900,6 +2914,7 @@ struct ContentView: View {
|
|||
}
|
||||
.onChange(of: commandPaletteQuery) { _ in
|
||||
commandPaletteSelectedResultIndex = 0
|
||||
commandPaletteSelectionAnchorCommandID = nil
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
|
|
@ -2911,7 +2926,13 @@ struct ContentView: View {
|
|||
syncCommandPaletteDebugStateForObservedWindow()
|
||||
}
|
||||
.onChange(of: commandPaletteResultsRevision) { _ in
|
||||
commandPaletteSelectedResultIndex = commandPaletteSelectedIndex(resultCount: cachedCommandPaletteResults.count)
|
||||
let resultIDs = cachedCommandPaletteResults.map(\.id)
|
||||
commandPaletteSelectedResultIndex = Self.commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: commandPaletteSelectionAnchorCommandID,
|
||||
fallbackSelectedIndex: commandPaletteSelectedResultIndex,
|
||||
resultIDs: resultIDs
|
||||
)
|
||||
syncCommandPaletteSelectionAnchorFromCurrentResults()
|
||||
updateCommandPaletteScrollTarget(resultCount: cachedCommandPaletteResults.count, animated: false)
|
||||
if let hoveredIndex = commandPaletteHoveredResultIndex, hoveredIndex >= cachedCommandPaletteResults.count {
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
|
|
@ -3098,34 +3119,76 @@ struct ContentView: View {
|
|||
commandPaletteSearchTask = nil
|
||||
}
|
||||
|
||||
nonisolated private static func commandPaletteResolvedSearchResults(
|
||||
searchCorpus: [CommandPaletteSearchCorpusEntry<String>],
|
||||
commandsByID: [String: CommandPaletteCommand],
|
||||
query: String,
|
||||
usageHistory: [String: CommandPaletteUsageEntry],
|
||||
queryIsEmpty: Bool,
|
||||
historyTimestamp: TimeInterval,
|
||||
shouldCancel: @escaping () -> Bool = { false }
|
||||
) -> [CommandPaletteSearchResult] {
|
||||
let results = CommandPaletteSearchEngine.search(
|
||||
entries: searchCorpus,
|
||||
query: query,
|
||||
historyBoost: { commandId, _ in
|
||||
Self.commandPaletteHistoryBoost(
|
||||
for: commandId,
|
||||
queryIsEmpty: queryIsEmpty,
|
||||
history: usageHistory,
|
||||
now: historyTimestamp
|
||||
)
|
||||
},
|
||||
shouldCancel: shouldCancel
|
||||
)
|
||||
|
||||
return results.compactMap { result in
|
||||
guard let command = commandsByID[result.payload] else { return nil }
|
||||
return CommandPaletteSearchResult(
|
||||
command: command,
|
||||
score: result.score,
|
||||
titleMatchIndices: result.titleMatchIndices
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh: Bool = false) {
|
||||
refreshCommandPaletteSearchCorpus(force: forceSearchCorpusRefresh)
|
||||
|
||||
commandPaletteSearchRequestID &+= 1
|
||||
let requestID = commandPaletteSearchRequestID
|
||||
commandPaletteDeferredSubmitRequestID = nil
|
||||
isCommandPaletteSearchPending = true
|
||||
let query = commandPaletteQueryForMatching
|
||||
let scope = commandPaletteListScope
|
||||
let fingerprint = cachedCommandPaletteFingerprint
|
||||
let searchCorpus = commandPaletteSearchCorpus
|
||||
let commandsByID = commandPaletteSearchCommandsByID
|
||||
let usageHistory = commandPaletteUsageHistoryByCommandId
|
||||
let queryIsEmpty = CommandPaletteFuzzyMatcher.preparedQuery(query).isEmpty
|
||||
let historyTimestamp = Date().timeIntervalSince1970
|
||||
commandPalettePendingActivation = nil
|
||||
if cachedCommandPaletteResults.isEmpty {
|
||||
cachedCommandPaletteResults = Self.commandPaletteResolvedSearchResults(
|
||||
searchCorpus: searchCorpus,
|
||||
commandsByID: commandsByID,
|
||||
query: query,
|
||||
usageHistory: usageHistory,
|
||||
queryIsEmpty: queryIsEmpty,
|
||||
historyTimestamp: historyTimestamp
|
||||
)
|
||||
commandPaletteResolvedSearchRequestID = requestID
|
||||
commandPaletteResultsRevision &+= 1
|
||||
}
|
||||
isCommandPaletteSearchPending = true
|
||||
|
||||
cancelCommandPaletteSearch()
|
||||
commandPaletteSearchTask = Task.detached(priority: .userInitiated) {
|
||||
let results = CommandPaletteSearchEngine.search(
|
||||
entries: searchCorpus,
|
||||
let results = Self.commandPaletteResolvedSearchResults(
|
||||
searchCorpus: searchCorpus,
|
||||
commandsByID: commandsByID,
|
||||
query: query,
|
||||
historyBoost: { commandId, _ in
|
||||
Self.commandPaletteHistoryBoost(
|
||||
for: commandId,
|
||||
queryIsEmpty: queryIsEmpty,
|
||||
history: usageHistory,
|
||||
now: historyTimestamp
|
||||
)
|
||||
},
|
||||
usageHistory: usageHistory,
|
||||
queryIsEmpty: queryIsEmpty,
|
||||
historyTimestamp: historyTimestamp,
|
||||
shouldCancel: { Task.isCancelled }
|
||||
)
|
||||
|
||||
|
|
@ -3140,26 +3203,25 @@ struct ContentView: View {
|
|||
return
|
||||
}
|
||||
|
||||
cachedCommandPaletteResults = results.compactMap { result in
|
||||
guard let command = commandPaletteSearchCommandsByID[result.payload] else { return nil }
|
||||
return CommandPaletteSearchResult(
|
||||
command: command,
|
||||
score: result.score,
|
||||
titleMatchIndices: result.titleMatchIndices
|
||||
)
|
||||
}
|
||||
cachedCommandPaletteResults = results
|
||||
let resultIDs = cachedCommandPaletteResults.map(\.id)
|
||||
let pendingActivation = commandPalettePendingActivation
|
||||
let resolvedActivation = Self.commandPaletteResolvedPendingActivation(
|
||||
pendingActivation,
|
||||
requestID: requestID,
|
||||
resultIDs: resultIDs
|
||||
)
|
||||
commandPaletteResolvedSearchRequestID = requestID
|
||||
isCommandPaletteSearchPending = false
|
||||
let shouldRunDeferredSubmit = commandPaletteDeferredSubmitRequestID == requestID
|
||||
if shouldRunDeferredSubmit {
|
||||
commandPaletteDeferredSubmitRequestID = nil
|
||||
if Self.commandPalettePendingActivationRequestID(pendingActivation) == requestID {
|
||||
commandPalettePendingActivation = nil
|
||||
}
|
||||
commandPaletteResultsRevision &+= 1
|
||||
if commandPaletteSearchRequestID == requestID {
|
||||
commandPaletteSearchTask = nil
|
||||
}
|
||||
if shouldRunDeferredSubmit {
|
||||
runSelectedCommandPaletteResult()
|
||||
if let resolvedActivation {
|
||||
runCommandPaletteResolvedActivation(resolvedActivation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3175,51 +3237,29 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
private func commandPaletteCommandsFingerprint() -> Int {
|
||||
let panelContext = focusedPanelContext
|
||||
let focusedDirectory: String? = {
|
||||
guard let panelContext else { return nil }
|
||||
if let directory = panelContext.workspace.panelDirectories[panelContext.panelId] {
|
||||
return directory
|
||||
}
|
||||
return panelContext.workspace.currentDirectory
|
||||
}()
|
||||
var hasher = Hasher()
|
||||
hasher.combine(tabManager.sessionAutosaveFingerprint())
|
||||
hasher.combine(commandPaletteContextSnapshot().fingerprint())
|
||||
hasher.combine(AppDelegate.shared?.isCmuxCLIInstalledInPATH() ?? false)
|
||||
hasher.combine(panelContext?.panelId)
|
||||
hasher.combine(panelContext?.panel.panelType.rawValue)
|
||||
hasher.combine(panelContext?.workspace.id)
|
||||
hasher.combine(panelContext?.workspace.manualUnreadPanelIds.count ?? 0)
|
||||
hasher.combine(panelContext?.workspace.sidebarPullRequestsInDisplayOrder().count ?? 0)
|
||||
hasher.combine(focusedDirectory)
|
||||
hasher.combine(caseIsUpdateAvailable(updateViewModel.effectiveState))
|
||||
let availableTargets = TerminalDirectoryOpenTarget.cachedLiveAvailableTargets
|
||||
.map(\.rawValue)
|
||||
.sorted()
|
||||
for target in availableTargets {
|
||||
hasher.combine(target)
|
||||
}
|
||||
return hasher.finalize()
|
||||
}
|
||||
|
||||
private func commandPaletteSwitcherEntriesFingerprint() -> Int {
|
||||
let windowContexts = commandPaletteSwitcherWindowContexts()
|
||||
var hasher = Hasher()
|
||||
hasher.combine(windowContexts.count)
|
||||
for context in windowContexts {
|
||||
hasher.combine(context.windowId)
|
||||
hasher.combine(context.windowLabel)
|
||||
hasher.combine(context.selectedWorkspaceId)
|
||||
hasher.combine(context.tabManager.sessionAutosaveFingerprint())
|
||||
let fingerprintContexts = windowContexts.map { context in
|
||||
CommandPaletteSwitcherFingerprintContext(
|
||||
windowId: context.windowId,
|
||||
windowLabel: context.windowLabel,
|
||||
selectedWorkspaceId: context.selectedWorkspaceId,
|
||||
workspaces: commandPaletteOrderedSwitcherWorkspaces(for: context).map { workspace in
|
||||
CommandPaletteSwitcherFingerprintWorkspace(
|
||||
id: workspace.id,
|
||||
displayName: workspaceDisplayName(workspace),
|
||||
metadata: commandPaletteWorkspaceSearchMetadata(for: workspace)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
return hasher.finalize()
|
||||
}
|
||||
|
||||
private func caseIsUpdateAvailable(_ state: UpdateState) -> Bool {
|
||||
if case .updateAvailable = state {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return Self.commandPaletteSwitcherFingerprint(windowContexts: fingerprintContexts)
|
||||
}
|
||||
|
||||
private func commandPaletteHighlightedTitleText(_ title: String, matchedIndices: Set<Int>) -> Text {
|
||||
|
|
@ -3274,16 +3314,9 @@ struct ContentView: View {
|
|||
var nextRank = 0
|
||||
|
||||
for context in windowContexts {
|
||||
var workspaces = context.tabManager.tabs
|
||||
let workspaces = commandPaletteOrderedSwitcherWorkspaces(for: context)
|
||||
guard !workspaces.isEmpty else { continue }
|
||||
|
||||
let selectedWorkspaceId = context.selectedWorkspaceId ?? context.tabManager.selectedTabId
|
||||
if let selectedWorkspaceId,
|
||||
let selectedIndex = workspaces.firstIndex(where: { $0.id == selectedWorkspaceId }) {
|
||||
let selectedWorkspace = workspaces.remove(at: selectedIndex)
|
||||
workspaces.insert(selectedWorkspace, at: 0)
|
||||
}
|
||||
|
||||
let windowId = context.windowId
|
||||
let windowTabManager = context.tabManager
|
||||
let windowKeywords = commandPaletteWindowKeywords(windowLabel: context.windowLabel)
|
||||
|
|
@ -3386,6 +3419,22 @@ struct ContentView: View {
|
|||
return ["window", windowLabel.lowercased()]
|
||||
}
|
||||
|
||||
private func commandPaletteOrderedSwitcherWorkspaces(
|
||||
for context: CommandPaletteSwitcherWindowContext
|
||||
) -> [Workspace] {
|
||||
var workspaces = context.tabManager.tabs
|
||||
guard !workspaces.isEmpty else { return [] }
|
||||
|
||||
let selectedWorkspaceId = context.selectedWorkspaceId ?? context.tabManager.selectedTabId
|
||||
if let selectedWorkspaceId,
|
||||
let selectedIndex = workspaces.firstIndex(where: { $0.id == selectedWorkspaceId }) {
|
||||
let selectedWorkspace = workspaces.remove(at: selectedIndex)
|
||||
workspaces.insert(selectedWorkspace, at: 0)
|
||||
}
|
||||
|
||||
return workspaces
|
||||
}
|
||||
|
||||
private func focusCommandPaletteSwitcherTarget(
|
||||
windowId: UUID,
|
||||
tabManager: TabManager,
|
||||
|
|
@ -4537,6 +4586,107 @@ struct ContentView: View {
|
|||
return min(max(commandPaletteSelectedResultIndex, 0), resultCount - 1)
|
||||
}
|
||||
|
||||
static func commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: String?,
|
||||
fallbackSelectedIndex: Int,
|
||||
resultIDs: [String]
|
||||
) -> Int {
|
||||
guard !resultIDs.isEmpty else { return 0 }
|
||||
if let preferredCommandID,
|
||||
let anchoredIndex = resultIDs.firstIndex(of: preferredCommandID) {
|
||||
return anchoredIndex
|
||||
}
|
||||
return min(max(fallbackSelectedIndex, 0), resultIDs.count - 1)
|
||||
}
|
||||
|
||||
static func commandPalettePendingActivationRequestID(
|
||||
_ pendingActivation: CommandPalettePendingActivation?
|
||||
) -> UInt64? {
|
||||
switch pendingActivation {
|
||||
case .selected(let requestID, _, _):
|
||||
return requestID
|
||||
case .command(let requestID, _):
|
||||
return requestID
|
||||
case nil:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func commandPaletteResolvedPendingActivation(
|
||||
_ pendingActivation: CommandPalettePendingActivation?,
|
||||
requestID: UInt64,
|
||||
resultIDs: [String]
|
||||
) -> CommandPaletteResolvedActivation? {
|
||||
switch pendingActivation {
|
||||
case .selected(let activationRequestID, let fallbackSelectedIndex, let preferredCommandID):
|
||||
guard activationRequestID == requestID else { return nil }
|
||||
let resolvedIndex = commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: preferredCommandID,
|
||||
fallbackSelectedIndex: fallbackSelectedIndex,
|
||||
resultIDs: resultIDs
|
||||
)
|
||||
return .selected(index: resolvedIndex)
|
||||
case .command(let activationRequestID, let commandID):
|
||||
guard activationRequestID == requestID, resultIDs.contains(commandID) else { return nil }
|
||||
return .command(commandID: commandID)
|
||||
case nil:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func commandPaletteContextFingerprint(
|
||||
boolValues: [String: Bool],
|
||||
stringValues: [String: String]
|
||||
) -> Int {
|
||||
var hasher = Hasher()
|
||||
for key in boolValues.keys.sorted() {
|
||||
hasher.combine(key)
|
||||
hasher.combine(boolValues[key] ?? false)
|
||||
}
|
||||
for key in stringValues.keys.sorted() {
|
||||
hasher.combine(key)
|
||||
hasher.combine(stringValues[key] ?? "")
|
||||
}
|
||||
return hasher.finalize()
|
||||
}
|
||||
|
||||
static func commandPaletteSwitcherFingerprint(
|
||||
windowContexts: [CommandPaletteSwitcherFingerprintContext]
|
||||
) -> Int {
|
||||
var hasher = Hasher()
|
||||
hasher.combine(windowContexts.count)
|
||||
for context in windowContexts {
|
||||
hasher.combine(context.windowId)
|
||||
hasher.combine(context.windowLabel)
|
||||
hasher.combine(context.selectedWorkspaceId)
|
||||
hasher.combine(context.workspaces.count)
|
||||
for workspace in context.workspaces {
|
||||
hasher.combine(workspace.id)
|
||||
hasher.combine(workspace.displayName)
|
||||
combineCommandPaletteSwitcherSearchMetadata(workspace.metadata, into: &hasher)
|
||||
}
|
||||
}
|
||||
return hasher.finalize()
|
||||
}
|
||||
|
||||
static func combineCommandPaletteSwitcherSearchMetadata(
|
||||
_ metadata: CommandPaletteSwitcherSearchMetadata,
|
||||
into hasher: inout Hasher
|
||||
) {
|
||||
hasher.combine(metadata.directories.count)
|
||||
for directory in metadata.directories {
|
||||
hasher.combine(directory)
|
||||
}
|
||||
hasher.combine(metadata.branches.count)
|
||||
for branch in metadata.branches {
|
||||
hasher.combine(branch)
|
||||
}
|
||||
hasher.combine(metadata.ports.count)
|
||||
for port in metadata.ports {
|
||||
hasher.combine(port)
|
||||
}
|
||||
}
|
||||
|
||||
static func commandPaletteScrollPositionAnchor(
|
||||
selectedIndex: Int,
|
||||
resultCount: Int
|
||||
|
|
@ -4576,6 +4726,15 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func syncCommandPaletteSelectionAnchorFromCurrentResults() {
|
||||
guard !cachedCommandPaletteResults.isEmpty else {
|
||||
commandPaletteSelectionAnchorCommandID = nil
|
||||
return
|
||||
}
|
||||
let selectedIndex = commandPaletteSelectedIndex(resultCount: cachedCommandPaletteResults.count)
|
||||
commandPaletteSelectionAnchorCommandID = cachedCommandPaletteResults[selectedIndex].id
|
||||
}
|
||||
|
||||
private func moveCommandPaletteSelection(by delta: Int) {
|
||||
let count = cachedCommandPaletteResults.count
|
||||
guard count > 0 else {
|
||||
|
|
@ -4584,6 +4743,9 @@ struct ContentView: View {
|
|||
}
|
||||
let current = commandPaletteSelectedIndex(resultCount: count)
|
||||
commandPaletteSelectedResultIndex = min(max(current + delta, 0), count - 1)
|
||||
if commandPaletteHasCurrentResolvedResults {
|
||||
syncCommandPaletteSelectionAnchorFromCurrentResults()
|
||||
}
|
||||
syncCommandPaletteDebugStateForObservedWindow()
|
||||
}
|
||||
|
||||
|
|
@ -4644,29 +4806,55 @@ struct ContentView: View {
|
|||
!isCommandPaletteSearchPending && commandPaletteResolvedSearchRequestID == commandPaletteSearchRequestID
|
||||
}
|
||||
|
||||
private func runCommandPaletteResolvedActivation(_ activation: CommandPaletteResolvedActivation) {
|
||||
switch activation {
|
||||
case .command(let commandID):
|
||||
guard let command = cachedCommandPaletteResults.first(where: { $0.id == commandID })?.command else {
|
||||
return
|
||||
}
|
||||
runCommandPaletteCommand(command)
|
||||
case .selected(let fallbackIndex):
|
||||
guard !cachedCommandPaletteResults.isEmpty else {
|
||||
NSSound.beep()
|
||||
return
|
||||
}
|
||||
let resolvedIndex = Self.commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: commandPaletteSelectionAnchorCommandID,
|
||||
fallbackSelectedIndex: fallbackIndex,
|
||||
resultIDs: cachedCommandPaletteResults.map(\.id)
|
||||
)
|
||||
commandPaletteSelectedResultIndex = resolvedIndex
|
||||
syncCommandPaletteSelectionAnchorFromCurrentResults()
|
||||
runCommandPaletteCommand(cachedCommandPaletteResults[resolvedIndex].command)
|
||||
}
|
||||
}
|
||||
|
||||
private func runCommandPaletteResult(commandID: String) {
|
||||
guard commandPaletteHasCurrentResolvedResults,
|
||||
let command = cachedCommandPaletteResults.first(where: { $0.id == commandID })?.command else {
|
||||
guard commandPaletteHasCurrentResolvedResults else {
|
||||
if isCommandPalettePresented {
|
||||
commandPalettePendingActivation = .command(
|
||||
requestID: commandPaletteSearchRequestID,
|
||||
commandID: commandID
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
runCommandPaletteCommand(command)
|
||||
runCommandPaletteResolvedActivation(.command(commandID: commandID))
|
||||
}
|
||||
|
||||
private func runSelectedCommandPaletteResult() {
|
||||
guard commandPaletteHasCurrentResolvedResults else {
|
||||
if isCommandPalettePresented {
|
||||
commandPaletteDeferredSubmitRequestID = commandPaletteSearchRequestID
|
||||
commandPalettePendingActivation = .selected(
|
||||
requestID: commandPaletteSearchRequestID,
|
||||
fallbackSelectedIndex: commandPaletteSelectedResultIndex,
|
||||
preferredCommandID: commandPaletteSelectionAnchorCommandID
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let visibleResults = cachedCommandPaletteResults
|
||||
guard !visibleResults.isEmpty else {
|
||||
NSSound.beep()
|
||||
return
|
||||
}
|
||||
let index = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
runCommandPaletteCommand(visibleResults[index].command)
|
||||
runCommandPaletteResolvedActivation(.selected(index: commandPaletteSelectedResultIndex))
|
||||
}
|
||||
|
||||
private func handleCommandPaletteSubmitRequest() {
|
||||
|
|
@ -4820,6 +5008,7 @@ struct ContentView: View {
|
|||
commandPaletteQuery = initialQuery
|
||||
commandPaletteRenameDraft = ""
|
||||
commandPaletteSelectedResultIndex = 0
|
||||
commandPaletteSelectionAnchorCommandID = nil
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
|
|
@ -4837,6 +5026,7 @@ struct ContentView: View {
|
|||
commandPaletteQuery = ""
|
||||
commandPaletteRenameDraft = ""
|
||||
commandPaletteSelectedResultIndex = 0
|
||||
commandPaletteSelectionAnchorCommandID = nil
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
|
|
@ -4850,7 +5040,7 @@ struct ContentView: View {
|
|||
cachedCommandPaletteFingerprint = nil
|
||||
commandPaletteResolvedSearchRequestID = commandPaletteSearchRequestID
|
||||
isCommandPaletteSearchPending = false
|
||||
commandPaletteDeferredSubmitRequestID = nil
|
||||
commandPalettePendingActivation = nil
|
||||
commandPaletteResultsRevision &+= 1
|
||||
if let window = observedWindow {
|
||||
_ = window.makeFirstResponder(nil)
|
||||
|
|
@ -5242,7 +5432,7 @@ struct ContentView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
struct CommandPaletteSwitcherSearchMetadata {
|
||||
struct CommandPaletteSwitcherSearchMetadata: Equatable, Sendable {
|
||||
let directories: [String]
|
||||
let branches: [String]
|
||||
let ports: [Int]
|
||||
|
|
|
|||
|
|
@ -219,6 +219,177 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
|
|||
XCTAssertGreaterThanOrEqual(cancellationChecks, 4)
|
||||
}
|
||||
|
||||
func testResolvedSelectionIndexPrefersAnchoredCommand() {
|
||||
let resultIDs = ["command.0", "command.1", "command.2"]
|
||||
|
||||
XCTAssertEqual(
|
||||
ContentView.commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: "command.2",
|
||||
fallbackSelectedIndex: 0,
|
||||
resultIDs: resultIDs
|
||||
),
|
||||
2
|
||||
)
|
||||
XCTAssertEqual(
|
||||
ContentView.commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: "missing",
|
||||
fallbackSelectedIndex: 9,
|
||||
resultIDs: resultIDs
|
||||
),
|
||||
2
|
||||
)
|
||||
XCTAssertEqual(
|
||||
ContentView.commandPaletteResolvedSelectionIndex(
|
||||
preferredCommandID: nil,
|
||||
fallbackSelectedIndex: 1,
|
||||
resultIDs: []
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testResolvedPendingActivationPreservesSubmitAndClickSemantics() {
|
||||
let resultIDs = ["command.0", "command.1", "command.2"]
|
||||
|
||||
XCTAssertEqual(
|
||||
ContentView.commandPaletteResolvedPendingActivation(
|
||||
.selected(requestID: 41, fallbackSelectedIndex: 0, preferredCommandID: "command.2"),
|
||||
requestID: 41,
|
||||
resultIDs: resultIDs
|
||||
),
|
||||
.selected(index: 2)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
ContentView.commandPaletteResolvedPendingActivation(
|
||||
.command(requestID: 41, commandID: "command.1"),
|
||||
requestID: 41,
|
||||
resultIDs: resultIDs
|
||||
),
|
||||
.command(commandID: "command.1")
|
||||
)
|
||||
XCTAssertNil(
|
||||
ContentView.commandPaletteResolvedPendingActivation(
|
||||
.command(requestID: 41, commandID: "missing"),
|
||||
requestID: 41,
|
||||
resultIDs: resultIDs
|
||||
)
|
||||
)
|
||||
XCTAssertNil(
|
||||
ContentView.commandPaletteResolvedPendingActivation(
|
||||
.selected(requestID: 40, fallbackSelectedIndex: 0, preferredCommandID: nil),
|
||||
requestID: 41,
|
||||
resultIDs: resultIDs
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testCommandContextFingerprintTracksExactContextValues() {
|
||||
let base = ContentView.commandPaletteContextFingerprint(
|
||||
boolValues: [
|
||||
"workspace.hasPullRequests": true,
|
||||
"panel.hasUnread": false,
|
||||
"panel.isTerminal": true,
|
||||
],
|
||||
stringValues: [
|
||||
"workspace.name": "Alpha",
|
||||
"panel.name": "Main",
|
||||
]
|
||||
)
|
||||
let unreadChanged = ContentView.commandPaletteContextFingerprint(
|
||||
boolValues: [
|
||||
"workspace.hasPullRequests": true,
|
||||
"panel.hasUnread": true,
|
||||
"panel.isTerminal": true,
|
||||
],
|
||||
stringValues: [
|
||||
"workspace.name": "Alpha",
|
||||
"panel.name": "Main",
|
||||
]
|
||||
)
|
||||
let renamed = ContentView.commandPaletteContextFingerprint(
|
||||
boolValues: [
|
||||
"workspace.hasPullRequests": true,
|
||||
"panel.hasUnread": false,
|
||||
"panel.isTerminal": true,
|
||||
],
|
||||
stringValues: [
|
||||
"workspace.name": "Alpha",
|
||||
"panel.name": "Logs",
|
||||
]
|
||||
)
|
||||
|
||||
XCTAssertNotEqual(base, unreadChanged)
|
||||
XCTAssertNotEqual(base, renamed)
|
||||
}
|
||||
|
||||
func testSwitcherFingerprintTracksMetadataValuesAtSameCardinality() {
|
||||
let windowID = UUID()
|
||||
let workspaceID = UUID()
|
||||
let base = ContentView.commandPaletteSwitcherFingerprint(
|
||||
windowContexts: [
|
||||
ContentView.CommandPaletteSwitcherFingerprintContext(
|
||||
windowId: windowID,
|
||||
windowLabel: "Window 2",
|
||||
selectedWorkspaceId: workspaceID,
|
||||
workspaces: [
|
||||
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
|
||||
id: workspaceID,
|
||||
displayName: "Workspace Alpha",
|
||||
metadata: CommandPaletteSwitcherSearchMetadata(
|
||||
directories: ["/Users/example/dev/cmuxterm"],
|
||||
branches: ["feature/search-speed"],
|
||||
ports: [3000]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
let changedMetadata = ContentView.commandPaletteSwitcherFingerprint(
|
||||
windowContexts: [
|
||||
ContentView.CommandPaletteSwitcherFingerprintContext(
|
||||
windowId: windowID,
|
||||
windowLabel: "Window 2",
|
||||
selectedWorkspaceId: workspaceID,
|
||||
workspaces: [
|
||||
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
|
||||
id: workspaceID,
|
||||
displayName: "Workspace Alpha",
|
||||
metadata: CommandPaletteSwitcherSearchMetadata(
|
||||
directories: ["/Users/example/dev/other"],
|
||||
branches: ["feature/search-speed"],
|
||||
ports: [4000]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
let changedDisplayName = ContentView.commandPaletteSwitcherFingerprint(
|
||||
windowContexts: [
|
||||
ContentView.CommandPaletteSwitcherFingerprintContext(
|
||||
windowId: windowID,
|
||||
windowLabel: "Window 2",
|
||||
selectedWorkspaceId: workspaceID,
|
||||
workspaces: [
|
||||
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
|
||||
id: workspaceID,
|
||||
displayName: "Workspace Beta",
|
||||
metadata: CommandPaletteSwitcherSearchMetadata(
|
||||
directories: ["/Users/example/dev/cmuxterm"],
|
||||
branches: ["feature/search-speed"],
|
||||
ports: [3000]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
XCTAssertNotEqual(base, changedMetadata)
|
||||
XCTAssertNotEqual(base, changedDisplayName)
|
||||
}
|
||||
|
||||
func testCommandSearchBenchmarkBeatsLegacyPipeline() {
|
||||
let entries = makeCommandEntries(count: 900)
|
||||
let corpus = entries.map { entry in
|
||||
|
|
@ -251,7 +422,11 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
|
|||
}
|
||||
|
||||
print(String(format: "BENCH cmd+shift+p legacy=%.2fms optimized=%.2fms", legacyMs, optimizedMs))
|
||||
XCTAssertLessThan(optimizedMs, legacyMs)
|
||||
XCTAssertLessThan(
|
||||
optimizedMs,
|
||||
legacyMs * 1.25,
|
||||
"Optimized command search regressed significantly: legacy=\(legacyMs) optimized=\(optimizedMs)"
|
||||
)
|
||||
}
|
||||
|
||||
func testSwitcherSearchBenchmarkBeatsLegacyPipeline() {
|
||||
|
|
@ -286,6 +461,10 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
|
|||
}
|
||||
|
||||
print(String(format: "BENCH cmd+p legacy=%.2fms optimized=%.2fms", legacyMs, optimizedMs))
|
||||
XCTAssertLessThan(optimizedMs, legacyMs)
|
||||
XCTAssertLessThan(
|
||||
optimizedMs,
|
||||
legacyMs * 1.25,
|
||||
"Optimized switcher search regressed significantly: legacy=\(legacyMs) optimized=\(optimizedMs)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue