Repository navigation
Fix stale browser pane content after drag splits #1215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5233874
978f72b
81618bf
1dbd1e5
8aafb68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2005,6 +2005,12 @@ final class BrowserPanel: Panel, ObservableObject { | |
| setupObservers(for: webView) | ||
| } | ||
|
|
||
| private func isCurrentWebView(_ candidate: WKWebView, instanceID: UUID? = nil) -> Bool { | ||
| guard candidate === webView else { return false } | ||
| guard let instanceID else { return true } | ||
| return instanceID == webViewInstanceID | ||
| } | ||
|
|
||
| init(workspaceId: UUID, initialURL: URL? = nil, bypassInsecureHTTPHostOnce: String? = nil) { | ||
| self.id = UUID() | ||
| self.workspaceId = workspaceId | ||
|
|
@@ -2020,15 +2026,16 @@ final class BrowserPanel: Panel, ObservableObject { | |
| navDelegate.didFinish = { webView in | ||
| BrowserHistoryStore.shared.recordVisit(url: webView.url, title: webView.title) | ||
| Task { @MainActor [weak self] in | ||
| self?.refreshFavicon(from: webView) | ||
| self?.applyBrowserThemeModeIfNeeded() | ||
| guard let self, self.isCurrentWebView(webView) else { return } | ||
| self.refreshFavicon(from: webView) | ||
| self.applyBrowserThemeModeIfNeeded() | ||
| // Keep find-in-page open through load completion and refresh matches for the new DOM. | ||
| self?.restoreFindStateAfterNavigation(replaySearch: true) | ||
| self.restoreFindStateAfterNavigation(replaySearch: true) | ||
| } | ||
| } | ||
| navDelegate.didFailNavigation = { [weak self] _, failedURL in | ||
| navDelegate.didFailNavigation = { [weak self] failedWebView, failedURL in | ||
| Task { @MainActor in | ||
| guard let self else { return } | ||
| guard let self, self.isCurrentWebView(failedWebView) else { return } | ||
| // Clear stale title/favicon from the previous page so the tab | ||
| // shows the failed URL instead of the old page's branding. | ||
| self.pageTitle = failedURL.isEmpty ? "" : failedURL | ||
|
|
@@ -2162,39 +2169,44 @@ final class BrowserPanel: Panel, ObservableObject { | |
| } | ||
|
|
||
| private func setupObservers(for webView: WKWebView) { | ||
| let observedWebViewInstanceID = webViewInstanceID | ||
|
|
||
| // URL changes | ||
| let urlObserver = webView.observe(\.url, options: [.new]) { [weak self] webView, _ in | ||
| Task { @MainActor in | ||
| self?.currentURL = webView.url | ||
| guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } | ||
| self.currentURL = webView.url | ||
| } | ||
| } | ||
| webViewObservers.append(urlObserver) | ||
|
|
||
| // Title changes | ||
| let titleObserver = webView.observe(\.title, options: [.new]) { [weak self] webView, _ in | ||
| Task { @MainActor in | ||
| guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } | ||
| // Keep showing the last non-empty title while the new navigation is loading. | ||
| // WebKit often clears title to nil/"" during reload/navigation, which causes | ||
| // a distracting tab-title flash (e.g. to host/URL). Only accept non-empty titles. | ||
| let trimmed = (webView.title ?? "").trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { return } | ||
| self?.pageTitle = trimmed | ||
| self.pageTitle = trimmed | ||
| } | ||
| } | ||
| webViewObservers.append(titleObserver) | ||
|
|
||
| // Loading state | ||
| let loadingObserver = webView.observe(\.isLoading, options: [.new]) { [weak self] webView, _ in | ||
| Task { @MainActor in | ||
| self?.handleWebViewLoadingChanged(webView.isLoading) | ||
| guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } | ||
| self.handleWebViewLoadingChanged(webView.isLoading) | ||
| } | ||
| } | ||
| webViewObservers.append(loadingObserver) | ||
|
|
||
| // Can go back | ||
| let backObserver = webView.observe(\.canGoBack, options: [.new]) { [weak self] webView, _ in | ||
| Task { @MainActor in | ||
| guard let self else { return } | ||
| guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } | ||
| self.nativeCanGoBack = webView.canGoBack | ||
| self.refreshNavigationAvailability() | ||
| } | ||
|
|
@@ -2204,7 +2216,7 @@ final class BrowserPanel: Panel, ObservableObject { | |
| // Can go forward | ||
| let forwardObserver = webView.observe(\.canGoForward, options: [.new]) { [weak self] webView, _ in | ||
| Task { @MainActor in | ||
| guard let self else { return } | ||
| guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } | ||
| self.nativeCanGoForward = webView.canGoForward | ||
| self.refreshNavigationAvailability() | ||
| } | ||
|
|
@@ -2214,7 +2226,8 @@ final class BrowserPanel: Panel, ObservableObject { | |
| // Progress | ||
| let progressObserver = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in | ||
| Task { @MainActor in | ||
| self?.estimatedProgress = webView.estimatedProgress | ||
| guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } | ||
| self.estimatedProgress = webView.estimatedProgress | ||
| } | ||
| } | ||
| webViewObservers.append(progressObserver) | ||
|
|
@@ -2250,6 +2263,9 @@ final class BrowserPanel: Panel, ObservableObject { | |
|
|
||
| webViewObservers.removeAll() | ||
| webViewCancellables.removeAll() | ||
| faviconTask?.cancel() | ||
| faviconTask = nil | ||
| faviconRefreshGeneration &+= 1 | ||
| BrowserWindowPortalRegistry.detach(webView: terminatedWebView) | ||
| terminatedWebView.stopLoading() | ||
| terminatedWebView.navigationDelegate = nil | ||
|
|
@@ -2260,11 +2276,12 @@ final class BrowserPanel: Panel, ObservableObject { | |
|
|
||
| let replacement = Self.makeWebView() | ||
| replacement.pageZoom = desiredZoom | ||
| webView = replacement | ||
| webViewInstanceID = UUID() | ||
| webView = replacement | ||
| shouldRenderWebView = wasRenderable | ||
|
|
||
| bindWebView(replacement) | ||
| applyBrowserThemeModeIfNeeded() | ||
|
|
||
| if !history.backHistoryURLStrings.isEmpty || !history.forwardHistoryURLStrings.isEmpty { | ||
| restoreSessionNavigationHistory( | ||
|
|
@@ -2360,9 +2377,11 @@ final class BrowserPanel: Panel, ObservableObject { | |
| guard let scheme = pageURL.scheme?.lowercased(), scheme == "http" || scheme == "https" else { return } | ||
| faviconRefreshGeneration &+= 1 | ||
| let refreshGeneration = faviconRefreshGeneration | ||
| let refreshWebViewInstanceID = webViewInstanceID | ||
|
|
||
| faviconTask = Task { @MainActor [weak self, weak webView] in | ||
| guard let self, let webView else { return } | ||
| guard self.isCurrentWebView(webView, instanceID: refreshWebViewInstanceID) else { return } | ||
| guard self.isCurrentFaviconRefresh(generation: refreshGeneration) else { return } | ||
|
|
||
| // Try to discover the best icon URL from the document. | ||
|
|
@@ -2397,6 +2416,7 @@ final class BrowserPanel: Panel, ObservableObject { | |
| discoveredURL = u | ||
| } | ||
| } | ||
| guard self.isCurrentWebView(webView, instanceID: refreshWebViewInstanceID) else { return } | ||
| guard self.isCurrentFaviconRefresh(generation: refreshGeneration) else { return } | ||
|
|
||
| let fallbackURL = URL(string: "/favicon.ico", relativeTo: pageURL) | ||
|
|
@@ -2422,6 +2442,7 @@ final class BrowserPanel: Panel, ObservableObject { | |
| } catch { | ||
| return | ||
| } | ||
| guard self.isCurrentWebView(webView, instanceID: refreshWebViewInstanceID) else { return } | ||
| guard self.isCurrentFaviconRefresh(generation: refreshGeneration) else { return } | ||
|
|
||
| guard let http = response as? HTTPURLResponse, | ||
|
|
@@ -2701,12 +2722,121 @@ final class BrowserPanel: Panel, ObservableObject { | |
| if let detachedDeveloperToolsWindowCloseObserver { | ||
| NotificationCenter.default.removeObserver(detachedDeveloperToolsWindowCloseObserver) | ||
| } | ||
| webViewObservers.removeAll() | ||
| webViewCancellables.removeAll() | ||
| let webView = webView | ||
| Task { @MainActor in | ||
| BrowserWindowPortalRegistry.detach(webView: webView) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension BrowserPanel { | ||
| private var needsWorkspaceContextReset: Bool { | ||
| shouldRenderWebView || | ||
| currentURL != nil || | ||
| !pageTitle.isEmpty || | ||
| faviconPNGData != nil || | ||
| searchState != nil || | ||
| nativeCanGoBack || | ||
| nativeCanGoForward || | ||
| restoredHistoryCurrentURL != nil || | ||
| !restoredBackHistoryStack.isEmpty || | ||
| !restoredForwardHistoryStack.isEmpty || | ||
| estimatedProgress > 0 || | ||
| isLoading || | ||
| isDownloading || | ||
| activeDownloadCount != 0 || | ||
| preferredDeveloperToolsVisible || | ||
| webView.superview != nil | ||
| } | ||
|
|
||
| func resetForWorkspaceContextChange(reason: String) { | ||
| guard needsWorkspaceContextReset else { | ||
| #if DEBUG | ||
| dlog( | ||
| "browser.contextReset.skip panel=\(id.uuidString.prefix(5)) " + | ||
| "reason=\(reason) render=\(shouldRenderWebView ? 1 : 0)" | ||
| ) | ||
| #endif | ||
| return | ||
| } | ||
|
|
||
| #if DEBUG | ||
| dlog( | ||
| "browser.contextReset.begin panel=\(id.uuidString.prefix(5)) " + | ||
| "reason=\(reason) render=\(shouldRenderWebView ? 1 : 0) " + | ||
| "url=\(preferredURLStringForOmnibar() ?? "nil")" | ||
| ) | ||
| #endif | ||
|
|
||
| _ = hideDeveloperTools() | ||
| cancelDeveloperToolsRestoreRetry() | ||
| preferredDeveloperToolsVisible = false | ||
| preferredDeveloperToolsPresentation = .unknown | ||
| forceDeveloperToolsRefreshOnNextAttach = false | ||
| developerToolsDetachedOpenGraceDeadline = nil | ||
| developerToolsRestoreRetryAttempt = 0 | ||
| preferredAttachedDeveloperToolsWidth = nil | ||
| preferredAttachedDeveloperToolsWidthFraction = nil | ||
|
|
||
| loadingEndWorkItem?.cancel() | ||
| loadingEndWorkItem = nil | ||
| faviconTask?.cancel() | ||
| faviconTask = nil | ||
| faviconRefreshGeneration &+= 1 | ||
| loadingGeneration &+= 1 | ||
| activeDownloadCount = 0 | ||
| isDownloading = false | ||
| isLoading = false | ||
| estimatedProgress = 0 | ||
| nativeCanGoBack = false | ||
| nativeCanGoForward = false | ||
| navigationDelegate?.lastAttemptedURL = nil | ||
| abandonRestoredSessionHistoryIfNeeded() | ||
|
|
||
| pendingAddressBarFocusRequestId = nil | ||
| preferredFocusIntent = .addressBar | ||
| suppressOmnibarAutofocusUntil = nil | ||
| suppressWebViewFocusUntil = nil | ||
| endSuppressWebViewFocusForAddressBar() | ||
| invalidateAddressBarPageFocusRestoreAttempts() | ||
| invalidateSearchFocusRequests(reason: "contextReset") | ||
| searchState = nil | ||
|
|
||
| pageTitle = "" | ||
| currentURL = nil | ||
| faviconPNGData = nil | ||
| lastFaviconURLString = nil | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| activePortalHostLease = nil | ||
| pendingDistinctPortalHostReplacementPaneId = nil | ||
| lockedPortalHost = nil | ||
|
|
||
| let oldWebView = webView | ||
| webViewObservers.removeAll() | ||
| webViewCancellables.removeAll() | ||
| BrowserWindowPortalRegistry.detach(webView: oldWebView) | ||
| oldWebView.stopLoading() | ||
| oldWebView.navigationDelegate = nil | ||
| oldWebView.uiDelegate = nil | ||
| if let oldCmuxWebView = oldWebView as? CmuxWebView { | ||
| oldCmuxWebView.onContextMenuDownloadStateChanged = nil | ||
| } | ||
|
|
||
| let replacement = Self.makeWebView() | ||
| webViewInstanceID = UUID() | ||
| webView = replacement | ||
| shouldRenderWebView = false | ||
| bindWebView(replacement) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| applyBrowserThemeModeIfNeeded() | ||
| refreshNavigationAvailability() | ||
|
Comment on lines
+2815
to
+2832
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This won't cause an immediate crash (the deinit {
developerToolsRestoreRetryWorkItem?.cancel()
developerToolsRestoreRetryWorkItem = nil
if let obs = detachedDeveloperToolsWindowCloseObserver {
NotificationCenter.default.removeObserver(obs)
}
webViewObservers.removeAll() // clear before any async work
webViewCancellables.removeAll()
let webView = webView
Task { @MainActor in
BrowserWindowPortalRegistry.detach(webView: webView)
}
}
Comment on lines
+2826
to
+2832
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Between Reorder to assign let replacement = Self.makeWebView()
webViewInstanceID = UUID()
webView = replacement
shouldRenderWebView = false
bindWebView(replacement)
refreshNavigationAvailability()
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| #if DEBUG | ||
| dlog( | ||
| "browser.contextReset.end panel=\(id.uuidString.prefix(5)) " + | ||
| "reason=\(reason) instance=\(webViewInstanceID.uuidString.prefix(6))" | ||
| ) | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.