diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 3f9ad98c..34809815 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1404,6 +1404,7 @@ struct ContentView: View { @State private var commandPaletteResolvedSearchRequestID: UInt64 = 0 @State private var commandPaletteResolvedSearchScope: CommandPaletteListScope? @State private var commandPaletteResolvedSearchFingerprint: Int? + @State private var commandPaletteResolvedMatchingQuery = "" @State private var isCommandPaletteSearchPending = false @State private var commandPalettePendingActivation: CommandPalettePendingActivation? @State private var commandPaletteResultsRevision: UInt64 = 0 @@ -3257,7 +3258,7 @@ struct ContentView: View { // stale switcher rows cannot linger above command-mode results. VStack(spacing: 0) { if visibleResults.isEmpty { - if commandPaletteHasCurrentResolvedResults { + if commandPaletteShouldShowEmptyState { Text(commandPaletteEmptyStateText) .font(.system(size: 13, weight: .regular)) .foregroundStyle(.secondary) @@ -4106,6 +4107,27 @@ struct ContentView: View { !hasVisibleResultsForScope } + static func commandPaletteShouldPreserveEmptyStateWhileSearchPending( + isSearchPending: Bool, + visibleResultsScopeMatches: Bool, + resolvedSearchScopeMatches: Bool, + resolvedSearchFingerprintMatches: Bool, + resolvedResultsAreEmpty: Bool, + currentMatchingQuery: String, + resolvedMatchingQuery: String + ) -> Bool { + guard isSearchPending, + visibleResultsScopeMatches, + resolvedSearchScopeMatches, + resolvedSearchFingerprintMatches, + resolvedResultsAreEmpty else { + return false + } + + return currentMatchingQuery == resolvedMatchingQuery + || currentMatchingQuery.hasPrefix(resolvedMatchingQuery) + } + private func scheduleCommandPaletteResultsRefresh( query: String? = nil, forceSearchCorpusRefresh: Bool = false @@ -4152,6 +4174,7 @@ struct ContentView: View { commandPaletteResolvedSearchRequestID = requestID commandPaletteResolvedSearchScope = scope commandPaletteResolvedSearchFingerprint = fingerprint + commandPaletteResolvedMatchingQuery = matchingQuery isCommandPaletteSearchPending = false setCommandPaletteVisibleResults( cachedCommandPaletteResults, @@ -4210,6 +4233,7 @@ struct ContentView: View { commandPaletteResolvedSearchRequestID = requestID commandPaletteResolvedSearchScope = scope commandPaletteResolvedSearchFingerprint = fingerprint + commandPaletteResolvedMatchingQuery = matchingQuery isCommandPaletteSearchPending = false setCommandPaletteVisibleResults( cachedCommandPaletteResults, @@ -6096,6 +6120,23 @@ struct ContentView: View { !isCommandPaletteSearchPending && commandPaletteResolvedSearchRequestID == commandPaletteSearchRequestID } + private var commandPaletteShouldShowEmptyState: Bool { + guard commandPaletteVisibleResults.isEmpty else { return false } + if commandPaletteHasCurrentResolvedResults { + return true + } + + return Self.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + isSearchPending: isCommandPaletteSearchPending, + visibleResultsScopeMatches: commandPaletteVisibleResultsScope == commandPaletteListScope, + resolvedSearchScopeMatches: commandPaletteResolvedSearchScope == commandPaletteListScope, + resolvedSearchFingerprintMatches: commandPaletteResolvedSearchFingerprint == commandPaletteVisibleResultsFingerprint, + resolvedResultsAreEmpty: cachedCommandPaletteResults.isEmpty, + currentMatchingQuery: commandPaletteQueryForMatching, + resolvedMatchingQuery: commandPaletteResolvedMatchingQuery + ) + } + private func runCommandPaletteResolvedActivation(_ activation: CommandPaletteResolvedActivation) { switch activation { case .command(let commandID): diff --git a/cmuxTests/CommandPaletteSearchEngineTests.swift b/cmuxTests/CommandPaletteSearchEngineTests.swift index 6976ad1d..51cd4182 100644 --- a/cmuxTests/CommandPaletteSearchEngineTests.swift +++ b/cmuxTests/CommandPaletteSearchEngineTests.swift @@ -434,6 +434,59 @@ final class CommandPaletteSearchEngineTests: XCTestCase { ) } + func testPendingEmptyStateIsPreservedWhenRefiningAResolvedNoMatchQuery() { + XCTAssertTrue( + ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + isSearchPending: true, + visibleResultsScopeMatches: true, + resolvedSearchScopeMatches: true, + resolvedSearchFingerprintMatches: true, + resolvedResultsAreEmpty: true, + currentMatchingQuery: "zzzzzzzzz", + resolvedMatchingQuery: "zzzzzzzz" + ) + ) + } + + func testPendingEmptyStateIsNotPreservedWhenQueryDoesNotRefineResolvedNoMatch() { + XCTAssertFalse( + ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + isSearchPending: true, + visibleResultsScopeMatches: true, + resolvedSearchScopeMatches: true, + resolvedSearchFingerprintMatches: true, + resolvedResultsAreEmpty: true, + currentMatchingQuery: "zzzza", + resolvedMatchingQuery: "zzzzb" + ) + ) + } + + func testPendingEmptyStateIsNotPreservedWhenResolvedResultsMayBeStale() { + XCTAssertFalse( + ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + isSearchPending: true, + visibleResultsScopeMatches: true, + resolvedSearchScopeMatches: true, + resolvedSearchFingerprintMatches: false, + resolvedResultsAreEmpty: true, + currentMatchingQuery: "zzzzzzzzz", + resolvedMatchingQuery: "zzzzzzzz" + ) + ) + XCTAssertFalse( + ContentView.commandPaletteShouldPreserveEmptyStateWhileSearchPending( + isSearchPending: true, + visibleResultsScopeMatches: true, + resolvedSearchScopeMatches: true, + resolvedSearchFingerprintMatches: true, + resolvedResultsAreEmpty: false, + currentMatchingQuery: "zzzzzzzzz", + resolvedMatchingQuery: "zzzzzzzz" + ) + ) + } + func testVisibleResultsResetWhenQueryChangesCommandPaletteScope() { XCTAssertTrue( ContentView.commandPaletteShouldResetVisibleResultsForQueryTransition(