From be550dafa65405878a48bd37ab86021df528c10a Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Fri, 1 May 2026 15:19:18 -0700 Subject: [PATCH 1/4] Fix browser omnibar typing lag with many workspaces --- GhosttyTabs.xcodeproj/project.pbxproj | 4 + Sources/AppDelegate.swift | 4 +- .../BrowserOmnibarPerformanceSupport.swift | 253 ++++++++++++++++++ Sources/Panels/BrowserPanel.swift | 2 +- Sources/Panels/BrowserPanelView.swift | 215 +++++---------- Sources/TabManager.swift | 2 - Sources/Workspace.swift | 24 +- .../BrowserOmnibarSuggestionsUITests.swift | 36 +-- 8 files changed, 351 insertions(+), 189 deletions(-) create mode 100644 Sources/Panels/BrowserOmnibarPerformanceSupport.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 030f655bfc1d..54d250822fe8 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ D7AB00000000000000000007 /* BrowserPanel+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000008 /* BrowserPanel+MoveTabToNewWorkspace.swift */; }; A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; }; A5001404 /* BrowserPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001414 /* BrowserPanelView.swift */; }; + B0A500000000000000000001 /* BrowserOmnibarPerformanceSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A500000000000000000002 /* BrowserOmnibarPerformanceSupport.swift */; }; A5001405 /* PanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001415 /* PanelContentView.swift */; }; A5001406 /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001416 /* Workspace.swift */; }; C0DE32470000000000000003 /* WorkspaceSurfaceIdentifierClipboardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE32470000000000000004 /* WorkspaceSurfaceIdentifierClipboardText.swift */; }; @@ -465,6 +466,7 @@ D7AB00000000000000000008 /* BrowserPanel+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Panels/BrowserPanel+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = ""; }; A5001414 /* BrowserPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanelView.swift; sourceTree = ""; }; + B0A500000000000000000002 /* BrowserOmnibarPerformanceSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserOmnibarPerformanceSupport.swift; sourceTree = ""; }; A5001415 /* PanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/PanelContentView.swift; sourceTree = ""; }; A5001416 /* Workspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workspace.swift; sourceTree = ""; }; D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+DetachedWorkspace.swift"; sourceTree = ""; }; @@ -836,6 +838,7 @@ A500RG00 /* ReactGrab.swift */, A5001413 /* TerminalPanelView.swift */, A5001414 /* BrowserPanelView.swift */, + B0A500000000000000000002 /* BrowserOmnibarPerformanceSupport.swift */, A5007421 /* BrowserPopupWindowController.swift */, A5007423 /* BrowserWebAuthnSupport.swift */, A5001418 /* MarkdownPanel.swift */, @@ -1306,6 +1309,7 @@ A500RG01 /* ReactGrab.swift in Sources */, A5001403 /* TerminalPanelView.swift in Sources */, A5001404 /* BrowserPanelView.swift in Sources */, + B0A500000000000000000001 /* BrowserOmnibarPerformanceSupport.swift in Sources */, A5007420 /* BrowserPopupWindowController.swift in Sources */, A5007422 /* BrowserWebAuthnSupport.swift in Sources */, A5001420 /* MarkdownPanel.swift in Sources */, diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 701687149264..37f9f4fcd513 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4452,8 +4452,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent func setCommandPaletteVisible(_ visible: Bool, for window: NSWindow) { guard let windowId = mainWindowId(for: window) else { return } - let wasVisible = commandPaletteVisibilityByWindowId[windowId] ?? false - commandPaletteVisibilityByWindowId[windowId] = visible + let wasVisible = commandPaletteVisibilityByWindowId.updateValue(visible, forKey: windowId) ?? false + postCommandPaletteVisibilityDidChangeIfNeeded(wasVisible: wasVisible, visible: visible, window: window, windowId: windowId) // Opening (false -> true) always resolves pending-open. // Closing (true -> false) also clears stale pending state. // Ignore repeated false updates so a stale sync cannot erase an in-flight open request. diff --git a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift new file mode 100644 index 000000000000..93cd5d45168f --- /dev/null +++ b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift @@ -0,0 +1,253 @@ +import AppKit +import Foundation + +struct BrowserOpenTabSuggestionSnapshot: Equatable { + let workspaceId: UUID + let panelId: UUID + let url: String + let title: String? + let lowercasedURL: String + let lowercasedTitle: String + + init?(workspaceId: UUID, panelId: UUID, url: String?, title: String?) { + guard let normalizedURL = url?.trimmingCharacters(in: .whitespacesAndNewlines), + !normalizedURL.isEmpty else { return nil } + let normalizedTitle = title?.trimmingCharacters(in: .whitespacesAndNewlines) + self.workspaceId = workspaceId + self.panelId = panelId + self.url = normalizedURL + self.title = normalizedTitle?.isEmpty == false ? normalizedTitle : nil + self.lowercasedURL = normalizedURL.lowercased() + self.lowercasedTitle = self.title?.lowercased() ?? "" + } +} + +struct OmnibarOpenTabMatch: Equatable { + let tabId: UUID + let panelId: UUID + let url: String + let title: String? + let isKnownOpenTab: Bool + + init(tabId: UUID, panelId: UUID, url: String, title: String?, isKnownOpenTab: Bool = true) { + self.tabId = tabId + self.panelId = panelId + self.url = url + self.title = title + self.isKnownOpenTab = isKnownOpenTab + } +} + +extension BrowserHistoryStore { + static func uiTestSeedEntriesIfConfigured() -> [Entry]? { + let env = ProcessInfo.processInfo.environment + guard env["CMUX_UI_TEST_MODE"] == "1", + let rawSeed = env["CMUX_UI_TEST_BROWSER_HISTORY_JSON"], + let data = rawSeed.data(using: .utf8) else { + return nil + } + return try? JSONDecoder().decode([Entry].self, from: data) + } +} + +private final class BrowserOpenTabSuggestionIndex { + private var suggestionsByPanelId: [UUID: BrowserOpenTabSuggestionSnapshot] = [:] + private var suggestionOrder: [UUID] = [] + private var isSeeded = false + + func upsert(_ snapshot: BrowserOpenTabSuggestionSnapshot) { + let existing = suggestionsByPanelId[snapshot.panelId] + guard existing != snapshot else { return } + suggestionsByPanelId[snapshot.panelId] = snapshot + if existing == nil { + suggestionOrder.append(snapshot.panelId) + } + } + + func remove(panelId: UUID) { + guard suggestionsByPanelId.removeValue(forKey: panelId) != nil else { return } + suggestionOrder.removeAll { $0 == panelId } + } + + func matching( + for query: String, + currentWorkspaceId: UUID, + currentPanelId: UUID, + currentPanelSnapshot: BrowserOpenTabSuggestionSnapshot?, + includeCurrentPanelForSingleCharacterQuery: Bool, + limit: Int, + seedSnapshots: () -> [BrowserOpenTabSuggestionSnapshot] + ) -> [OmnibarOpenTabMatch] { + let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedQuery.isEmpty, limit > 0 else { return [] } + + seedIfNeeded(seedSnapshots()) + + let loweredQuery = trimmedQuery.lowercased() + let singleCharacterQuery = omnibarSingleCharacterQuery(for: trimmedQuery) + var matches: [OmnibarOpenTabMatch] = [] + matches.reserveCapacity(min(limit, suggestionOrder.count + 1)) + var seenKeys = Set() + + func snapshotMatches(_ snapshot: BrowserOpenTabSuggestionSnapshot) -> Bool { + if let singleCharacterQuery { + return omnibarHasSingleCharacterPrefixMatch( + query: singleCharacterQuery, + url: snapshot.url, + title: snapshot.title + ) + } + return snapshot.lowercasedURL.contains(loweredQuery) || + snapshot.lowercasedTitle.contains(loweredQuery) + } + + func append(_ snapshot: BrowserOpenTabSuggestionSnapshot, isKnownOpenTab: Bool) { + guard matches.count < limit else { return } + let key = [ + snapshot.workspaceId.uuidString.lowercased(), + snapshot.panelId.uuidString.lowercased(), + snapshot.lowercasedURL, + ].joined(separator: "|") + guard seenKeys.insert(key).inserted else { return } + guard snapshotMatches(snapshot) else { return } + matches.append( + OmnibarOpenTabMatch( + tabId: snapshot.workspaceId, + panelId: snapshot.panelId, + url: snapshot.url, + title: snapshot.title, + isKnownOpenTab: isKnownOpenTab + ) + ) + } + + if includeCurrentPanelForSingleCharacterQuery, let currentPanelSnapshot { + append(currentPanelSnapshot, isKnownOpenTab: true) + } + + for panelId in suggestionOrder { + guard matches.count < limit, + let snapshot = suggestionsByPanelId[panelId] else { + continue + } + let isCurrentPanel = snapshot.workspaceId == currentWorkspaceId && snapshot.panelId == currentPanelId + if isCurrentPanel && !includeCurrentPanelForSingleCharacterQuery { + continue + } + append(snapshot, isKnownOpenTab: true) + } + + return matches + } + + private func seedIfNeeded(_ snapshots: [BrowserOpenTabSuggestionSnapshot]) { + guard !isSeeded else { return } + isSeeded = true + suggestionsByPanelId.removeAll(keepingCapacity: true) + suggestionOrder.removeAll(keepingCapacity: true) + for snapshot in snapshots { + suggestionsByPanelId[snapshot.panelId] = snapshot + suggestionOrder.append(snapshot.panelId) + } + } +} + +private var browserOpenTabSuggestionIndexesByManagerId: [ObjectIdentifier: BrowserOpenTabSuggestionIndex] = [:] + +extension TabManager { + private var browserOpenTabSuggestionIndex: BrowserOpenTabSuggestionIndex { + let managerId = ObjectIdentifier(self) + if let index = browserOpenTabSuggestionIndexesByManagerId[managerId] { + return index + } + let index = BrowserOpenTabSuggestionIndex() + browserOpenTabSuggestionIndexesByManagerId[managerId] = index + return index + } + + func upsertBrowserOpenTabSuggestion(_ snapshot: BrowserOpenTabSuggestionSnapshot) { + browserOpenTabSuggestionIndex.upsert(snapshot) + } + + func removeBrowserOpenTabSuggestion(panelId: UUID) { + browserOpenTabSuggestionIndex.remove(panelId: panelId) + } + + func matchingOpenBrowserTabSuggestions( + for query: String, + currentWorkspaceId: UUID, + currentPanelId: UUID, + currentPanelSnapshot: BrowserOpenTabSuggestionSnapshot?, + includeCurrentPanelForSingleCharacterQuery: Bool, + limit: Int + ) -> [OmnibarOpenTabMatch] { + browserOpenTabSuggestionIndex.matching( + for: query, + currentWorkspaceId: currentWorkspaceId, + currentPanelId: currentPanelId, + currentPanelSnapshot: currentPanelSnapshot, + includeCurrentPanelForSingleCharacterQuery: includeCurrentPanelForSingleCharacterQuery, + limit: limit, + seedSnapshots: browserOpenTabSuggestionSeedSnapshots + ) + } + + private func browserOpenTabSuggestionSeedSnapshots() -> [BrowserOpenTabSuggestionSnapshot] { + tabs.flatMap { workspace in + workspace.panels.compactMap { _, panel in + guard let browserPanel = panel as? BrowserPanel else { return nil } + return BrowserOpenTabSuggestionSnapshot( + workspaceId: workspace.id, + panelId: browserPanel.id, + url: browserPanel.preferredURLStringForOmnibar(), + title: browserPanel.pageTitle + ) + } + } + } +} + +extension Workspace { + func publishBrowserOpenTabSuggestion(for browserPanel: BrowserPanel) { + guard let snapshot = BrowserOpenTabSuggestionSnapshot( + workspaceId: id, + panelId: browserPanel.id, + url: browserPanel.preferredURLStringForOmnibar(), + title: browserPanel.pageTitle + ) else { + owningTabManager?.removeBrowserOpenTabSuggestion(panelId: browserPanel.id) + return + } + owningTabManager?.upsertBrowserOpenTabSuggestion(snapshot) + } + + func removeBrowserOpenTabSuggestion(panelId: UUID) { + owningTabManager?.removeBrowserOpenTabSuggestion(panelId: panelId) + } + + func removeBrowserOpenTabSuggestionIfNeeded(panel: (any Panel)?, panelId: UUID) { + guard panel is BrowserPanel else { return } + removeBrowserOpenTabSuggestion(panelId: panelId) + } +} + +extension Notification.Name { + static let commandPaletteVisibilityDidChange = Notification.Name("cmux.commandPaletteVisibilityDidChange") +} + +func postCommandPaletteVisibilityDidChangeIfNeeded( + wasVisible: Bool, + visible: Bool, + window: NSWindow, + windowId: UUID +) { + guard wasVisible != visible else { return } + NotificationCenter.default.post( + name: .commandPaletteVisibilityDidChange, + object: window, + userInfo: [ + "windowId": windowId, + "visible": visible, + ] + ) +} diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index d4df2c3f2a77..4dd2272e2cb6 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -1051,7 +1051,7 @@ final class BrowserHistoryStore: ObservableObject { func loadIfNeeded() { guard !didLoad else { return } - didLoad = true + didLoad = true; if let seededEntries = BrowserHistoryStore.uiTestSeedEntriesIfConfigured() { entries = seededEntries.sorted { $0.lastVisited > $1.lastVisited }; return } guard let fileURL else { return } migrateLegacyTaggedHistoryFileIfNeeded(to: fileURL) diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 551c5531c232..0e515713b3a7 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -424,8 +424,7 @@ struct BrowserPanelView: View { @State private var addressBarHeight: CGFloat = 0 @State private var isBrowserImportHintPopoverPresented = false @State private var lastHandledAddressBarFocusRequestId: UUID? - @State private var pendingAddressBarFocusRetryRequestId: UUID? - @State private var pendingAddressBarFocusRetryGeneration: UInt64 = 0 + @State private var omnibarSelectAllRequestId: UInt64 = 0 @State private var isBrowserProfileMenuPresented = false @State private var isBrowserThemeMenuPresented = false @State private var browserChromeStyle = BrowserChromeStyle.resolve( @@ -714,6 +713,9 @@ struct BrowserPanelView: View { .onChange(of: panel.pendingAddressBarFocusRequestId) { _ in applyPendingAddressBarFocusRequestIfNeeded() } + .onReceive(NotificationCenter.default.publisher(for: .commandPaletteVisibilityDidChange)) { _ in + applyPendingAddressBarFocusRequestIfNeeded() + } .onChange(of: panel.profileID) { _ in panel.historyStore.loadIfNeeded() if addressBarFocused { @@ -1177,6 +1179,7 @@ struct BrowserPanelView: View { } ), isFocused: $addressBarFocused, + selectAllRequestId: omnibarSelectAllRequestId, inlineCompletion: inlineCompletion, placeholder: String(localized: "browser.addressBar.placeholder", defaultValue: "Search or enter URL"), onTap: { @@ -1491,27 +1494,8 @@ struct BrowserPanelView: View { return false } - private func clearPendingAddressBarFocusRetry() { - pendingAddressBarFocusRetryRequestId = nil - pendingAddressBarFocusRetryGeneration &+= 1 - } - - private func schedulePendingAddressBarFocusRetryIfNeeded(requestId: UUID) { - guard pendingAddressBarFocusRetryRequestId != requestId else { return } - pendingAddressBarFocusRetryRequestId = requestId - pendingAddressBarFocusRetryGeneration &+= 1 - let generation = pendingAddressBarFocusRetryGeneration - DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { - guard pendingAddressBarFocusRetryGeneration == generation else { return } - pendingAddressBarFocusRetryRequestId = nil - guard panel.pendingAddressBarFocusRequestId == requestId else { return } - applyPendingAddressBarFocusRequestIfNeeded() - } - } - private func applyPendingAddressBarFocusRequestIfNeeded() { guard let requestId = panel.pendingAddressBarFocusRequestId else { - clearPendingAddressBarFocusRetry() return } guard !isCommandPaletteVisibleForPanelWindow() else { @@ -1521,10 +1505,8 @@ struct BrowserPanelView: View { detail: "reason=command_palette_visible request=\(requestId.uuidString.prefix(8))" ) #endif - schedulePendingAddressBarFocusRetryIfNeeded(requestId: requestId) return } - clearPendingAddressBarFocusRetry() guard lastHandledAddressBarFocusRequestId != requestId else { #if DEBUG logBrowserFocusState( @@ -2096,107 +2078,23 @@ struct BrowserPanelView: View { private func matchingOpenTabSuggestions(for query: String, limit: Int) -> [OmnibarOpenTabMatch] { guard !query.isEmpty, limit > 0 else { return [] } - - let loweredQuery = query.lowercased() let singleCharacterQuery = omnibarSingleCharacterQuery(for: query) let includeCurrentPanelForSingleCharacterQuery = singleCharacterQuery != nil - let tabManager = AppDelegate.shared?.tabManager - let currentPanelWorkspaceId = tabManager?.tabs.first(where: { tab in - tab.panels[panel.id] is BrowserPanel - })?.id - var matches: [OmnibarOpenTabMatch] = [] - var seenKeys = Set() - - func preferredPanelURL(_ browserPanel: BrowserPanel) -> String? { - browserPanel.preferredURLStringForOmnibar() - } - - func addMatch( - tabId: UUID, - panelId: UUID, - url: String, - title: String?, - isKnownOpenTab: Bool, - matches: inout [OmnibarOpenTabMatch], - seenKeys: inout Set - ) { - let key = "\(tabId.uuidString.lowercased())|\(panelId.uuidString.lowercased())|\(url.lowercased())" - guard !seenKeys.contains(key) else { return } - seenKeys.insert(key) - matches.append( - OmnibarOpenTabMatch( - tabId: tabId, - panelId: panelId, - url: url, - title: title, - isKnownOpenTab: isKnownOpenTab - ) - ) - } - - if includeCurrentPanelForSingleCharacterQuery, - let query = singleCharacterQuery, - let currentURL = preferredPanelURL(panel), - !currentURL.isEmpty { - let rawTitle = panel.pageTitle.trimmingCharacters(in: .whitespacesAndNewlines) - let title = rawTitle.isEmpty ? nil : rawTitle - if omnibarHasSingleCharacterPrefixMatch(query: query, url: currentURL, title: title) { - addMatch( - tabId: currentPanelWorkspaceId ?? panel.workspaceId, - panelId: panel.id, - url: currentURL, - title: title, - isKnownOpenTab: currentPanelWorkspaceId != nil, - matches: &matches, - seenKeys: &seenKeys - ) - } - } - - guard let tabManager else { return matches } - - for tab in tabManager.tabs { - for (panelId, anyPanel) in tab.panels { - guard let browserPanel = anyPanel as? BrowserPanel else { continue } - guard let currentURL = preferredPanelURL(browserPanel), - !currentURL.isEmpty else { continue } - let isCurrentPanel = tab.id == panel.workspaceId && panelId == panel.id - if isCurrentPanel && !includeCurrentPanelForSingleCharacterQuery { - continue - } - - let rawTitle = browserPanel.pageTitle.trimmingCharacters(in: .whitespacesAndNewlines) - let title = rawTitle.isEmpty ? nil : rawTitle - let isMatch: Bool = { - if let singleCharacterQuery { - return omnibarHasSingleCharacterPrefixMatch( - query: singleCharacterQuery, - url: currentURL, - title: title - ) - } - let haystacks = [ - currentURL.lowercased(), - (title ?? "").lowercased(), - ] - return haystacks.contains { $0.contains(loweredQuery) } - }() - guard isMatch else { continue } - - addMatch( - tabId: tab.id, - panelId: panelId, - url: currentURL, - title: title, - isKnownOpenTab: true, - matches: &matches, - seenKeys: &seenKeys - ) - } - } - - if matches.count <= limit { return matches } - return Array(matches.prefix(limit)) + let currentPanelSnapshot = BrowserOpenTabSuggestionSnapshot( + workspaceId: panel.workspaceId, + panelId: panel.id, + url: panel.preferredURLStringForOmnibar(), + title: panel.pageTitle + ) + let tabManager = AppDelegate.shared?.tabManagerFor(tabId: panel.workspaceId) ?? AppDelegate.shared?.tabManager + return tabManager?.matchingOpenBrowserTabSuggestions( + for: query, + currentWorkspaceId: panel.workspaceId, + currentPanelId: panel.id, + currentPanelSnapshot: currentPanelSnapshot, + includeCurrentPanelForSingleCharacterQuery: includeCurrentPanelForSingleCharacterQuery, + limit: limit + ) ?? [] } private func forcedRemoteSuggestionsForUITest() -> [String]? { @@ -2221,14 +2119,7 @@ struct BrowserPanelView: View { refreshSuggestions() } if effects.shouldSelectAll { - // Apply immediately for fast Cmd+L typing, then retry once in case - // first responder wasn't fully settled on the same runloop. - DispatchQueue.main.async { - NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: nil) - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: nil) - } + omnibarSelectAllRequestId &+= 1 } if effects.shouldBlurToWebView { hideSuggestions() @@ -2302,22 +2193,6 @@ enum OmnibarInputIntent: Equatable { case ambiguous } - struct OmnibarOpenTabMatch: Equatable { - let tabId: UUID - let panelId: UUID - let url: String - let title: String? - let isKnownOpenTab: Bool - - init(tabId: UUID, panelId: UUID, url: String, title: String?, isKnownOpenTab: Bool = true) { - self.tabId = tabId - self.panelId = panelId - self.url = url - self.title = title - self.isKnownOpenTab = isKnownOpenTab - } - } - func omnibarInputIntent(for query: String) -> OmnibarInputIntent { let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return .ambiguous } @@ -3474,6 +3349,7 @@ private final class OmnibarNativeTextField: NSTextField { private struct OmnibarTextFieldRepresentable: NSViewRepresentable { @Binding var text: String @Binding var isFocused: Bool + let selectAllRequestId: UInt64 let inlineCompletion: OmnibarInlineCompletion? let placeholder: String let onTap: () -> Void @@ -3497,6 +3373,8 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { var lastPublishedHasMarkedText: Bool = false /// Guards against infinite focus loops: `true` = focus requested, `false` = blur requested, `nil` = idle. var pendingFocusRequest: Bool? + var pendingSelectAllRequestId: UInt64? + var appliedSelectAllRequestId: UInt64 = 0 init(parent: OmnibarTextFieldRepresentable) { self.parent = parent @@ -3628,6 +3506,9 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { } } attachSelectionObserverIfNeeded() + if let field = obj.object as? OmnibarNativeTextField { + applyPendingSelectAllIfPossible(field: field) + } publishSelectionState() } @@ -3809,6 +3690,40 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { weak var parentField: OmnibarNativeTextField? + func queueSelectAllRequest(_ requestId: UInt64) { + guard requestId != 0, appliedSelectAllRequestId != requestId else { return } + pendingSelectAllRequestId = requestId + } + + @discardableResult + func applyPendingSelectAllIfPossible( + field: OmnibarNativeTextField, + startEditingIfNeeded: Bool = false + ) -> Bool { + guard let requestId = pendingSelectAllRequestId, + requestId != 0, + appliedSelectAllRequestId != requestId else { + return false + } + + if field.currentEditor() == nil, startEditingIfNeeded { + field.selectText(nil) + } + + guard let editor = field.currentEditor() as? NSTextView, + !editor.hasMarkedText() else { + return false + } + let length = editor.string.utf16.count + isProgrammaticMutation = true + editor.setSelectedRange(NSRange(location: 0, length: length)) + isProgrammaticMutation = false + appliedSelectAllRequestId = requestId + pendingSelectAllRequestId = nil + publishSelectionState() + return true + } + func publishSelectionState() { guard let field = parentField else { return } if let editor = field.currentEditor() as? NSTextView { @@ -3978,6 +3893,7 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { context.coordinator.parent = self context.coordinator.parentField = nsView nsView.placeholderString = placeholder + context.coordinator.queueSelectAllRequest(selectAllRequestId) let activeInlineCompletion = omnibarInlineCompletionIfBufferMatchesTypedPrefix( bufferText: text, @@ -4028,11 +3944,15 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { let alreadyFocused = fr === nsView || nsView.currentEditor() != nil || ((fr as? NSTextView)?.delegate as? NSTextField) === nsView - guard !alreadyFocused else { return } + if alreadyFocused { + coordinator?.applyPendingSelectAllIfPossible(field: nsView, startEditingIfNeeded: true) + return + } #if DEBUG coordinator?.logFocusEvent("updateNSView.requestFocus.apply") #endif window.makeFirstResponder(nsView) + coordinator?.applyPendingSelectAllIfPossible(field: nsView, startEditingIfNeeded: true) } } else if !isFocused, isFirstResponder, context.coordinator.pendingFocusRequest != false { #if DEBUG @@ -4066,6 +3986,7 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { } } } + context.coordinator.applyPendingSelectAllIfPossible(field: nsView, startEditingIfNeeded: isFocused) if let editor = nsView.currentEditor() as? NSTextView, !editor.hasMarkedText() { if let activeInlineCompletion { diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index aaf3def30817..37953a77c5a2 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -5646,8 +5646,6 @@ class TabManager: ObservableObject { return true } - // MARK: - Browser Panel Operations - /// Create a new browser panel in a split func newBrowserSplit( tabId: UUID, diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 8377f7e6275f..7294fcea7793 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -7268,7 +7268,6 @@ final class Workspace: Identifiable, ObservableObject { var onClosedBrowserPanel: ((ClosedBrowserPanelRestoreSnapshot) -> Void)? weak var owningTabManager: TabManager? - // Closing tabs mutates split layout immediately; terminal views handle their own AppKit // layout/size synchronization. @@ -8122,20 +8121,18 @@ final class Workspace: Identifiable, ObservableObject { tmuxWorkspaceFlashToken &+= 1 } - private func installBrowserPanelSubscription(_ browserPanel: BrowserPanel) { - let subscription = Publishers.CombineLatest3( - browserPanel.$pageTitle.removeDuplicates(), - browserPanel.$isLoading.removeDuplicates(), - browserPanel.$faviconPNGData.removeDuplicates(by: { $0 == $1 }) + let subscription = Publishers.CombineLatest4( + browserPanel.$pageTitle.removeDuplicates(), browserPanel.$currentURL.removeDuplicates(), + browserPanel.$isLoading.removeDuplicates(), browserPanel.$faviconPNGData.removeDuplicates(by: { $0 == $1 }) ) .receive(on: DispatchQueue.main) - .sink { [weak self, weak browserPanel] _, isLoading, favicon in + .sink { [weak self, weak browserPanel] _, _, isLoading, favicon in guard let self = self, let browserPanel = browserPanel, let tabId = self.surfaceIdFromPanelId(browserPanel.id) else { return } + self.publishBrowserOpenTabSuggestion(for: browserPanel) guard let existing = self.bonsplitController.tab(tabId) else { return } - let nextTitle = browserPanel.displayTitle if self.panelTitles[browserPanel.id] != nextTitle { self.panelTitles[browserPanel.id] = nextTitle @@ -8144,7 +8141,6 @@ final class Workspace: Identifiable, ObservableObject { let titleUpdate: String? = existing.title == resolvedTitle ? nil : resolvedTitle let faviconUpdate: Data?? = existing.iconImageData == favicon ? nil : .some(favicon) let loadingUpdate: Bool? = existing.isLoading == isLoading ? nil : isLoading - guard titleUpdate != nil || faviconUpdate != nil || loadingUpdate != nil else { return } self.bonsplitController.updateTab( tabId, @@ -8155,6 +8151,7 @@ final class Workspace: Identifiable, ObservableObject { ) } panelSubscriptions[browserPanel.id] = subscription + publishBrowserOpenTabSuggestion(for: browserPanel) setPreferredBrowserProfileID(browserPanel.profileID) } @@ -10623,15 +10620,14 @@ final class Workspace: Identifiable, ObservableObject { // panel close work is still unwinding. hideAllTerminalPortalViews() hideAllBrowserPortalViews() - let panelEntries = Array(panels) for (panelId, panel) in panelEntries { removePendingTerminalInputObservers(forPanelId: panelId) + removeBrowserOpenTabSuggestionIfNeeded(panel: panel, panelId: panelId) panelSubscriptions.removeValue(forKey: panelId) PortScanner.shared.unregisterPanel(workspaceId: id, panelId: panelId) panel.close() } - panels.removeAll(keepingCapacity: false) surfaceIdToPanelId.removeAll(keepingCapacity: false) panelSubscriptions.removeAll(keepingCapacity: false) @@ -11164,6 +11160,7 @@ final class Workspace: Identifiable, ObservableObject { isPinned: detached.isPinned, inPane: paneId ) else { + removeBrowserOpenTabSuggestionIfNeeded(panel: detached.panel, panelId: detached.panelId) panels.removeValue(forKey: detached.panelId) panelDirectories.removeValue(forKey: detached.panelId) surfaceTTYNames.removeValue(forKey: detached.panelId) @@ -13337,6 +13334,7 @@ extension Workspace: BonsplitDelegate { panel?.close() } + removeBrowserOpenTabSuggestionIfNeeded(panel: panel, panelId: panelId) panels.removeValue(forKey: panelId) untrackRemoteTerminalSurface(panelId) pendingRemoteTerminalChildExitSurfaceIds.remove(panelId) @@ -13507,7 +13505,9 @@ extension Workspace: BonsplitDelegate { if !closedPanelIds.isEmpty { for panelId in closedPanelIds { removePendingTerminalInputObservers(forPanelId: panelId) - panels[panelId]?.close() + let panel = panels[panelId] + removeBrowserOpenTabSuggestionIfNeeded(panel: panel, panelId: panelId) + panel?.close() panels.removeValue(forKey: panelId) untrackRemoteTerminalSurface(panelId) pendingRemoteTerminalChildExitSurfaceIds.remove(panelId) diff --git a/cmuxUITests/BrowserOmnibarSuggestionsUITests.swift b/cmuxUITests/BrowserOmnibarSuggestionsUITests.swift index 378150ce1607..e6407a255877 100644 --- a/cmuxUITests/BrowserOmnibarSuggestionsUITests.swift +++ b/cmuxUITests/BrowserOmnibarSuggestionsUITests.swift @@ -3,11 +3,13 @@ import Foundation final class BrowserOmnibarSuggestionsUITests: XCTestCase { private var dataPath = "" + private var browserHistorySeedJSON: String? override func setUp() { super.setUp() continueAfterFailure = false dataPath = "/tmp/cmux-ui-test-omnibar-suggestions-\(UUID().uuidString).json" + browserHistorySeedJSON = nil try? FileManager.default.removeItem(atPath: dataPath) // Terminate any lingering app from a prior test so its debounced @@ -351,7 +353,7 @@ final class BrowserOmnibarSuggestionsUITests: XCTestCase { seedBrowserHistoryForTest( seedEntries: [ SeedEntry(url: "https://news.ycombinator.com/", title: "News Y Combinator", visitCount: 12, typedCount: 1), - SeedEntry(url: "https://gmail.com/", title: "Gmail", visitCount: 10, typedCount: 2), + SeedEntry(url: "https://example.com/gmail", title: "Gmail", visitCount: 10, typedCount: 2), ] ) @@ -393,7 +395,8 @@ final class BrowserOmnibarSuggestionsUITests: XCTestCase { guard row.exists else { return nil } return "row\(index)=\((row.value as? String) ?? "")" }.joined(separator: ", ") - XCTFail("Expected a Gmail suggestion row. rows=\(rowValues)") + let seedBytes = browserHistorySeedJSON?.utf8.count ?? 0 + XCTFail("Expected a Gmail suggestion row. rows=\(rowValues) seedBytes=\(seedBytes)") return } @@ -412,9 +415,9 @@ final class BrowserOmnibarSuggestionsUITests: XCTestCase { let committedToGmail = waitForCondition(timeout: 8.0) { let value = (omnibar.value as? String) ?? "" - return value.localizedCaseInsensitiveContains("gmail.com") + return value.localizedCaseInsensitiveContains("example.com/gmail") } - XCTAssertTrue(committedToGmail, "Expected Enter to commit Gmail autocomplete target. value=\(String(describing: omnibar.value))") + XCTAssertTrue(committedToGmail, "Expected Enter to commit Gmail history target. value=\(String(describing: omnibar.value))") } func testOmnibarSingleRowPopupUsesMinimumHeight() { @@ -536,6 +539,9 @@ final class BrowserOmnibarSuggestionsUITests: XCTestCase { } private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) { + if let browserHistorySeedJSON { + app.launchEnvironment["CMUX_UI_TEST_BROWSER_HISTORY_JSON"] = browserHistorySeedJSON + } app.launch() XCTAssertTrue( ensureForegroundAfterLaunch(app, timeout: timeout), @@ -564,22 +570,6 @@ final class BrowserOmnibarSuggestionsUITests: XCTestCase { private func seedBrowserHistoryForTest(entries: [(String, String)]? = nil, seedEntries: [SeedEntry]? = nil) { // Keep the test hermetic: write a deterministic history file in the app's support dir // so the omnibar always has at least one local suggestion row. - let fileManager = FileManager.default - guard let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { - XCTFail("Missing Application Support directory") - return - } - - let bundleId = "com.cmuxterm.app.debug" - let dir = appSupport.appendingPathComponent(bundleId, isDirectory: true) - let url = dir.appendingPathComponent("browser_history.json", isDirectory: false) - do { - try fileManager.createDirectory(at: dir, withIntermediateDirectories: true) - } catch { - XCTFail("Failed to create app support dir: \(error)") - return - } - let now = Date().timeIntervalSinceReferenceDate let resolved: [SeedEntry] if let seedEntries { @@ -619,11 +609,7 @@ final class BrowserOmnibarSuggestionsUITests: XCTestCase { \(entriesJSON) ] """ - do { - try json.write(to: url, atomically: true, encoding: .utf8) - } catch { - XCTFail("Failed to write browser history seed file: \(error)") - } + browserHistorySeedJSON = json } private func attachElementDebug(name: String, element: XCUIElement) { From d97fad3f95647cfc029eb24390370adaba213731 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Fri, 1 May 2026 21:23:01 -0700 Subject: [PATCH 2/4] Add omnibar index seed regression test --- GhosttyTabs.xcodeproj/project.pbxproj | 12 ++++-- .../BrowserOmnibarPerformanceSupport.swift | 2 +- ...rowserOmnibarPerformanceSupportTests.swift | 43 +++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 cmuxTests/BrowserOmnibarPerformanceSupportTests.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 54d250822fe8..1642f6fa003d 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 1A1B2C3D4E5F607180000003 /* WorkspacePromptSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1B2C3D4E5F607180000004 /* WorkspacePromptSubmitTests.swift */; }; 1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */; }; 2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; }; + C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B6A97D1F2E4C71A8B9D002 /* BrowserOmnibarPerformanceSupportTests.swift */; }; 3023A1003023A1003023A100 /* ConfigSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3023B1003023B1003023B100 /* ConfigSource.swift */; }; 3023A1013023A1013023A101 /* ConfigSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3023B1013023B1013023B101 /* ConfigSettingsView.swift */; }; 350DAC5EBD38642A3E81471A /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312DE7503B4658DD173121B8 /* AuthManager.swift */; }; @@ -517,6 +518,7 @@ A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = ""; }; A500RG00 /* ReactGrab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ReactGrab.swift; sourceTree = ""; }; A5F10010A1B2C3D4E5F60719 /* KeyboardShortcutSettingsFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSettingsFileStore.swift; sourceTree = ""; }; + C2B6A97D1F2E4C71A8B9D002 /* BrowserOmnibarPerformanceSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarPerformanceSupportTests.swift; sourceTree = ""; }; C0DEF0B10000000000000002 /* JSONCParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCParser.swift; sourceTree = ""; }; AA1B2C3D4E5F60719 /* BonsplitTabDragUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonsplitTabDragUITests.swift; sourceTree = ""; }; AA1B2C3D4E5F60721 /* RightSidebarChromeHeightUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarChromeHeightUITests.swift; sourceTree = ""; }; @@ -943,8 +945,9 @@ F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */, F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */, FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */, - A5008380 /* BrowserFindJavaScriptTests.swift */, - A5008382 /* CommandPaletteSearchEngineTests.swift */, + A5008380 /* BrowserFindJavaScriptTests.swift */, + C2B6A97D1F2E4C71A8B9D002 /* BrowserOmnibarPerformanceSupportTests.swift */, + A5008382 /* CommandPaletteSearchEngineTests.swift */, A50019B3 /* SettingsSearchIndexTests.swift */, 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */, 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */, @@ -1470,8 +1473,9 @@ 1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */, C3408A000000000000000003 /* RightSidebarCommandPaletteTests.swift in Sources */, CB23911D7E131E8FBC9B82B6 /* SidebarOrderingTests.swift in Sources */, - 4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */, - 734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */, + 4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */, + C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */, + 734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */, B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */, DCC935C5F55C1DCB33E25521 /* WorkspacePullRequestSidebarTests.swift in Sources */, FEEDC0DEC0DEC0DEC0DE0001 /* FeedCoordinatorTests.swift in Sources */, diff --git a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift index 93cd5d45168f..02aa40e3c715 100644 --- a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift +++ b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift @@ -50,7 +50,7 @@ extension BrowserHistoryStore { } } -private final class BrowserOpenTabSuggestionIndex { +final class BrowserOpenTabSuggestionIndex { private var suggestionsByPanelId: [UUID: BrowserOpenTabSuggestionSnapshot] = [:] private var suggestionOrder: [UUID] = [] private var isSeeded = false diff --git a/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift b/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift new file mode 100644 index 000000000000..737f9eb1b52c --- /dev/null +++ b/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift @@ -0,0 +1,43 @@ +import XCTest + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +final class BrowserOmnibarPerformanceSupportTests: XCTestCase { + func testOpenTabSuggestionSeedSnapshotsAreEvaluatedOnlyOnce() { + let workspaceId = UUID() + let panelId = UUID() + let snapshot = BrowserOpenTabSuggestionSnapshot( + workspaceId: workspaceId, + panelId: panelId, + url: "https://example.com/docs", + title: "Example Docs" + ) + XCTAssertNotNil(snapshot) + + let index = BrowserOpenTabSuggestionIndex() + var seedCallCount = 0 + + func matches(for query: String) -> [OmnibarOpenTabMatch] { + index.matching( + for: query, + currentWorkspaceId: UUID(), + currentPanelId: UUID(), + currentPanelSnapshot: nil, + includeCurrentPanelForSingleCharacterQuery: false, + limit: 5, + seedSnapshots: { + seedCallCount += 1 + return [snapshot!] + } + ) + } + + XCTAssertEqual(matches(for: "example").map(\.url), ["https://example.com/docs"]) + XCTAssertEqual(matches(for: "docs").map(\.url), ["https://example.com/docs"]) + XCTAssertEqual(seedCallCount, 1) + } +} From dfb8aa52eb6d6427de0a0c471a1559b77b509ed1 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Fri, 1 May 2026 21:23:08 -0700 Subject: [PATCH 3/4] Avoid repeated omnibar index seeding --- Sources/Panels/BrowserOmnibarPerformanceSupport.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift index 02aa40e3c715..159c7ff46d63 100644 --- a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift +++ b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift @@ -81,7 +81,7 @@ final class BrowserOpenTabSuggestionIndex { let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedQuery.isEmpty, limit > 0 else { return [] } - seedIfNeeded(seedSnapshots()) + seedIfNeeded(seedSnapshots) let loweredQuery = trimmedQuery.lowercased() let singleCharacterQuery = omnibarSingleCharacterQuery(for: trimmedQuery) @@ -140,12 +140,12 @@ final class BrowserOpenTabSuggestionIndex { return matches } - private func seedIfNeeded(_ snapshots: [BrowserOpenTabSuggestionSnapshot]) { + private func seedIfNeeded(_ snapshots: () -> [BrowserOpenTabSuggestionSnapshot]) { guard !isSeeded else { return } isSeeded = true suggestionsByPanelId.removeAll(keepingCapacity: true) suggestionOrder.removeAll(keepingCapacity: true) - for snapshot in snapshots { + for snapshot in snapshots() { suggestionsByPanelId[snapshot.panelId] = snapshot suggestionOrder.append(snapshot.panelId) } From 7a09c17ec4ef4eee5c2b34b2149ee9d89032147f Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Fri, 1 May 2026 21:55:42 -0700 Subject: [PATCH 4/4] Address omnibar suggestion review feedback --- .../BrowserOmnibarPerformanceSupport.swift | 8 ++-- Sources/Panels/BrowserPanelView.swift | 39 ++++++++++++++----- ...rowserOmnibarPerformanceSupportTests.swift | 34 ++++++++++++++++ 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift index 159c7ff46d63..8fd68e27b82f 100644 --- a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift +++ b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift @@ -108,8 +108,8 @@ final class BrowserOpenTabSuggestionIndex { snapshot.panelId.uuidString.lowercased(), snapshot.lowercasedURL, ].joined(separator: "|") - guard seenKeys.insert(key).inserted else { return } guard snapshotMatches(snapshot) else { return } + guard seenKeys.insert(key).inserted else { return } matches.append( OmnibarOpenTabMatch( tabId: snapshot.workspaceId, @@ -126,10 +126,8 @@ final class BrowserOpenTabSuggestionIndex { } for panelId in suggestionOrder { - guard matches.count < limit, - let snapshot = suggestionsByPanelId[panelId] else { - continue - } + guard matches.count < limit else { break } + guard let snapshot = suggestionsByPanelId[panelId] else { continue } let isCurrentPanel = snapshot.workspaceId == currentWorkspaceId && snapshot.panelId == currentPanelId if isCurrentPanel && !includeCurrentPanelForSingleCharacterQuery { continue diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 0e515713b3a7..064a928c5fbd 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -713,7 +713,8 @@ struct BrowserPanelView: View { .onChange(of: panel.pendingAddressBarFocusRequestId) { _ in applyPendingAddressBarFocusRequestIfNeeded() } - .onReceive(NotificationCenter.default.publisher(for: .commandPaletteVisibilityDidChange)) { _ in + .onReceive(NotificationCenter.default.publisher(for: .commandPaletteVisibilityDidChange)) { notification in + guard commandPaletteVisibilityNotificationMatchesPanelWindow(notification) else { return } applyPendingAddressBarFocusRequestIfNeeded() } .onChange(of: panel.profileID) { _ in @@ -1494,6 +1495,29 @@ struct BrowserPanelView: View { return false } + private func commandPaletteVisibilityNotificationMatchesPanelWindow(_ notification: Notification) -> Bool { + if let notificationWindow = notification.object as? NSWindow, + panel.webView.window === notificationWindow { + return true + } + + guard let app = AppDelegate.shared, + let manager = app.tabManagerFor(tabId: panel.workspaceId), + let panelWindowId = app.windowId(for: manager) else { + return false + } + + if let notificationWindowId = notification.userInfo?["windowId"] as? UUID { + return notificationWindowId == panelWindowId + } + + if let notificationWindow = notification.object as? NSWindow, + let panelWindow = app.mainWindow(for: panelWindowId) { + return notificationWindow === panelWindow + } + return false + } + private func applyPendingAddressBarFocusRequestIfNeeded() { guard let requestId = panel.pendingAddressBarFocusRequestId else { return @@ -3697,8 +3721,7 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { @discardableResult func applyPendingSelectAllIfPossible( - field: OmnibarNativeTextField, - startEditingIfNeeded: Bool = false + field: OmnibarNativeTextField ) -> Bool { guard let requestId = pendingSelectAllRequestId, requestId != 0, @@ -3706,10 +3729,6 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { return false } - if field.currentEditor() == nil, startEditingIfNeeded { - field.selectText(nil) - } - guard let editor = field.currentEditor() as? NSTextView, !editor.hasMarkedText() else { return false @@ -3945,14 +3964,14 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { nsView.currentEditor() != nil || ((fr as? NSTextView)?.delegate as? NSTextField) === nsView if alreadyFocused { - coordinator?.applyPendingSelectAllIfPossible(field: nsView, startEditingIfNeeded: true) + coordinator?.applyPendingSelectAllIfPossible(field: nsView) return } #if DEBUG coordinator?.logFocusEvent("updateNSView.requestFocus.apply") #endif window.makeFirstResponder(nsView) - coordinator?.applyPendingSelectAllIfPossible(field: nsView, startEditingIfNeeded: true) + coordinator?.applyPendingSelectAllIfPossible(field: nsView) } } else if !isFocused, isFirstResponder, context.coordinator.pendingFocusRequest != false { #if DEBUG @@ -3986,7 +4005,7 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { } } } - context.coordinator.applyPendingSelectAllIfPossible(field: nsView, startEditingIfNeeded: isFocused) + context.coordinator.applyPendingSelectAllIfPossible(field: nsView) if let editor = nsView.currentEditor() as? NSTextView, !editor.hasMarkedText() { if let activeInlineCompletion { diff --git a/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift b/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift index 737f9eb1b52c..cb41f92d8e25 100644 --- a/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift +++ b/cmuxTests/BrowserOmnibarPerformanceSupportTests.swift @@ -40,4 +40,38 @@ final class BrowserOmnibarPerformanceSupportTests: XCTestCase { XCTAssertEqual(matches(for: "docs").map(\.url), ["https://example.com/docs"]) XCTAssertEqual(seedCallCount, 1) } + + func testNonMatchingCurrentSnapshotDoesNotDedupeIndexedMatch() { + let workspaceId = UUID() + let panelId = UUID() + let url = "https://example.com/" + let currentSnapshot = BrowserOpenTabSuggestionSnapshot( + workspaceId: workspaceId, + panelId: panelId, + url: url, + title: nil + ) + let indexedSnapshot = BrowserOpenTabSuggestionSnapshot( + workspaceId: workspaceId, + panelId: panelId, + url: url, + title: "Docs" + ) + XCTAssertNotNil(currentSnapshot) + XCTAssertNotNil(indexedSnapshot) + + let index = BrowserOpenTabSuggestionIndex() + let matches = index.matching( + for: "d", + currentWorkspaceId: workspaceId, + currentPanelId: panelId, + currentPanelSnapshot: currentSnapshot, + includeCurrentPanelForSingleCharacterQuery: true, + limit: 5, + seedSnapshots: { [indexedSnapshot!] } + ) + + XCTAssertEqual(matches.map(\.title), ["Docs"]) + XCTAssertEqual(matches.map(\.url), [url]) + } }