Add crashed tab recovery
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
use ely_domain::TabId;
|
||||
use gpui::{Context, Window};
|
||||
|
||||
use super::{ElyShell, ShellState};
|
||||
|
||||
impl ElyShell {
|
||||
pub(super) fn recover_crashed_tab(
|
||||
&mut self,
|
||||
tab_id: &TabId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let ShellState::Ready(core) = &mut self.state
|
||||
&& core.recover_crashed_tab(tab_id).is_ok()
|
||||
{
|
||||
self.sync_address_input(window, cx);
|
||||
self.focus_address_bar(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod about;
|
||||
mod bookmarks;
|
||||
mod crash;
|
||||
mod download_actions;
|
||||
mod download_labels;
|
||||
mod download_settings;
|
||||
@@ -26,7 +27,7 @@ mod task_manager;
|
||||
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::{colors, spacing};
|
||||
use ely_domain::{ArchivedTab, BrowserTab};
|
||||
use ely_domain::{ArchivedTab, BrowserTab, TabState};
|
||||
use gpui::{
|
||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||
StatefulInteractiveElement, Styled, div, px, rgb,
|
||||
@@ -43,6 +44,10 @@ impl ElyShell {
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
if tab.state() == &TabState::Crashed {
|
||||
return self.render_crash_page(tab, snapshot, cx);
|
||||
}
|
||||
|
||||
match tab.url().as_str() {
|
||||
"ely://bookmarks" => self.render_bookmarks_page(snapshot, cx),
|
||||
"ely://notes" => self.render_notes_page(snapshot, cx),
|
||||
@@ -52,6 +57,7 @@ impl ElyShell {
|
||||
"ely://archive" => self.render_archive_page(snapshot, cx),
|
||||
"ely://task-manager" => self.render_task_manager_page(snapshot),
|
||||
"ely://plugins" => self.render_plugin_catalog_page(snapshot, cx),
|
||||
url if url.starts_with("ely://crash/") => self.render_crash_route(snapshot, url, cx),
|
||||
url if url.starts_with("ely://plugin/") => {
|
||||
self.render_plugin_detail_page(snapshot, url, cx)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::BrowserTab;
|
||||
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||
use gpui_component::{
|
||||
IconName, Sizable, StyledExt,
|
||||
button::{Button, ButtonVariants},
|
||||
};
|
||||
|
||||
use super::{ElyShell, render_canvas_surface};
|
||||
|
||||
impl ElyShell {
|
||||
pub(super) fn render_crash_page(
|
||||
&mut self,
|
||||
tab: &BrowserTab,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
render_canvas_surface(render_crash_content(tab, snapshot, cx))
|
||||
}
|
||||
|
||||
pub(super) fn render_crash_route(
|
||||
&mut self,
|
||||
snapshot: &BrowserSnapshot,
|
||||
url: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let Some(tab_id) = crash_route_tab_id(url) else {
|
||||
return render_missing_crash_route("Crash route is invalid.");
|
||||
};
|
||||
|
||||
let Some(tab) = snapshot.tabs.iter().find(|tab| tab.id().as_str() == tab_id) else {
|
||||
return render_missing_crash_route("Tab could not be found.");
|
||||
};
|
||||
|
||||
render_canvas_surface(render_crash_content(tab, snapshot, cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_crash_content(
|
||||
tab: &BrowserTab,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let tab_id = tab.id().clone();
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.p_8()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_crash_header(tab, cx))
|
||||
.child(
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(rgb(colors::HAIRLINE))
|
||||
.py_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(crash_detail("URL", tab.url().as_str().to_string()))
|
||||
.child(crash_detail("Title", tab.title().to_string()))
|
||||
.child(crash_detail("Favicon", favicon_status(tab)))
|
||||
.child(crash_detail("Space", space_name(snapshot, tab)))
|
||||
.child(crash_detail("Profile", profile_name(snapshot, tab)))
|
||||
.child(crash_detail(
|
||||
"Form restore prompt",
|
||||
"Session data remains attached to this tab.".to_string(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child(IconName::TriangleAlert)
|
||||
.child("The renderer stopped for this tab. Restore reloads the saved tab state."),
|
||||
)
|
||||
.child(
|
||||
div().flex().child(
|
||||
Button::new("restore-crashed-tab")
|
||||
.primary()
|
||||
.small()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Restore")
|
||||
.tooltip("Restore tab")
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.recover_crashed_tab(&tab_id, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_crash_header(tab: &BrowserTab, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
let tab_id = tab.id().clone();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.items_end()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.text_size(px(26.0))
|
||||
.text_color(rgb(colors::INK))
|
||||
.child(IconName::TriangleAlert)
|
||||
.child("Tab Recovery"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child(format!("Recovering {}", tab.display_url())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("restore-crashed-tab-header")
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Restore")
|
||||
.tooltip("Restore tab")
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.recover_crashed_tab(&tab_id, window, cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_missing_crash_route(message: &'static str) -> AnyElement {
|
||||
render_canvas_surface(
|
||||
div()
|
||||
.size_full()
|
||||
.p_8()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Tab Recovery"))
|
||||
.child(div().text_sm().text_color(rgb(colors::MUTED)).child(message)),
|
||||
)
|
||||
}
|
||||
|
||||
fn crash_detail(label: &'static str, value: String) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(div().min_w(px(128.0)).text_xs().text_color(rgb(colors::MUTED)).child(label))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::INK))
|
||||
.child(value),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn favicon_status(tab: &BrowserTab) -> String {
|
||||
tab.favicon_key()
|
||||
.map_or_else(|| "No favicon saved".to_string(), |favicon| format!("Saved: {favicon}"))
|
||||
}
|
||||
|
||||
fn space_name(snapshot: &BrowserSnapshot, tab: &BrowserTab) -> String {
|
||||
snapshot
|
||||
.spaces
|
||||
.iter()
|
||||
.find(|space| space.id() == tab.space_id())
|
||||
.map_or_else(|| tab.space_id().as_str().to_string(), |space| space.name().to_string())
|
||||
}
|
||||
|
||||
fn profile_name(snapshot: &BrowserSnapshot, tab: &BrowserTab) -> String {
|
||||
snapshot
|
||||
.profiles
|
||||
.iter()
|
||||
.find(|profile| profile.id() == tab.profile_id())
|
||||
.map_or_else(|| tab.profile_id().as_str().to_string(), |profile| profile.name().to_string())
|
||||
}
|
||||
|
||||
fn crash_route_tab_id(url: &str) -> Option<&str> {
|
||||
let tab_id = url.strip_prefix("ely://crash/")?;
|
||||
(!tab_id.is_empty() && !tab_id.contains('/')).then_some(tab_id)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod archive_labels;
|
||||
mod bookmarks;
|
||||
mod crashes;
|
||||
mod downloads;
|
||||
mod history;
|
||||
mod internal_pages;
|
||||
|
||||
@@ -24,6 +24,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> {
|
||||
"ely://archive" => Some("Archived Tabs"),
|
||||
"ely://task-manager" => Some("Task Manager"),
|
||||
"ely://plugins" => Some("Plugin Marketplace"),
|
||||
url if crash_route_tab_id(url).is_some() => Some("Tab Recovery"),
|
||||
url if plugin_detail_route_id(url).is_some() => Some("Plugin Details"),
|
||||
url if SiteOrigin::from_site_route(url).ok().flatten().is_some() => Some("Site Settings"),
|
||||
"ely://about" => Some("About ELY Browser"),
|
||||
@@ -248,3 +249,8 @@ fn plugin_detail_route_id(url: &str) -> Option<&str> {
|
||||
let plugin_id = url.strip_prefix("ely://plugin/")?;
|
||||
(!plugin_id.is_empty() && !plugin_id.contains('/')).then_some(plugin_id)
|
||||
}
|
||||
|
||||
pub(crate) fn crash_route_tab_id(url: &str) -> Option<&str> {
|
||||
let tab_id = url.strip_prefix("ely://crash/")?;
|
||||
(!tab_id.is_empty() && !tab_id.contains('/')).then_some(tab_id)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use sync::SyncObjectPolicies;
|
||||
|
||||
mod bookmarks;
|
||||
mod commands;
|
||||
mod crashes;
|
||||
mod downloads;
|
||||
mod history;
|
||||
mod notes;
|
||||
|
||||
@@ -273,6 +273,15 @@ impl BrowserCore {
|
||||
self.close_active_tab()?;
|
||||
Ok(true)
|
||||
}
|
||||
"crash-tab" | "crash tab" => {
|
||||
self.crash_active_tab()?;
|
||||
Ok(true)
|
||||
}
|
||||
"recover-tab" | "recover tab" | "recover-crashed-tab" | "recover crashed tab" => {
|
||||
let tab_id = self.active_tab()?.id().clone();
|
||||
self.recover_crashed_tab(&tab_id)?;
|
||||
Ok(true)
|
||||
}
|
||||
"close-split-view" | "close split view" => {
|
||||
Ok(self.close_active_saved_split_view()?.is_some())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use ely_domain::TabId;
|
||||
|
||||
use super::BrowserCore;
|
||||
use crate::CoreError;
|
||||
|
||||
impl BrowserCore {
|
||||
pub fn crash_active_tab(&mut self) -> Result<TabId, CoreError> {
|
||||
let tab_id = self.active_tab_id.clone();
|
||||
self.crash_tab(&tab_id)
|
||||
}
|
||||
|
||||
pub fn crash_tab(&mut self, tab_id: &TabId) -> Result<TabId, CoreError> {
|
||||
let tab = self
|
||||
.tabs
|
||||
.iter_mut()
|
||||
.find(|tab| tab.id() == tab_id)
|
||||
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
|
||||
tab.mark_crashed();
|
||||
Ok(tab_id.clone())
|
||||
}
|
||||
|
||||
pub fn recover_crashed_tab(&mut self, tab_id: &TabId) -> Result<TabId, CoreError> {
|
||||
{
|
||||
let tab = self
|
||||
.tabs
|
||||
.iter_mut()
|
||||
.find(|tab| tab.id() == tab_id)
|
||||
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
|
||||
tab.mark_ready();
|
||||
}
|
||||
|
||||
self.select_tab(tab_id)?;
|
||||
Ok(tab_id.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::error::Error;
|
||||
|
||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||
use ely_domain::{CommandIntent, TabState, UrlText};
|
||||
|
||||
#[test]
|
||||
fn crash_active_tab_preserves_tab_metadata() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let tab_id = core.open_tab(UrlText::parse("https://example.com/form")?);
|
||||
let title = core.active_tab()?.title().to_string();
|
||||
core.set_tab_favicon_key(&tab_id, "favicons/example.ico")?;
|
||||
|
||||
core.crash_active_tab()?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(active_tab.id(), &tab_id);
|
||||
assert_eq!(active_tab.state(), &TabState::Crashed);
|
||||
assert_eq!(active_tab.url().as_str(), "https://example.com/form");
|
||||
assert_eq!(active_tab.title(), title);
|
||||
assert_eq!(active_tab.favicon_key(), Some("favicons/example.ico"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recover_crashed_tab_marks_tab_ready() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let tab_id = core.open_tab(UrlText::parse("https://example.com/recover")?);
|
||||
|
||||
core.crash_active_tab()?;
|
||||
core.recover_crashed_tab(&tab_id)?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(active_tab.id(), &tab_id);
|
||||
assert_eq!(active_tab.state(), &TabState::Ready);
|
||||
assert_eq!(active_tab.url().as_str(), "https://example.com/recover");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_tab_command_marks_active_tab_crashed() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let tab_id = core.open_tab(UrlText::parse("https://example.com/crash-loop")?);
|
||||
|
||||
core.set_command_query(">crash-tab");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("crash-tab".to_string())));
|
||||
assert_eq!(snapshot.active_tab_id, tab_id);
|
||||
assert_eq!(active_tab.state(), &TabState::Crashed);
|
||||
assert_eq!(active_tab.url().as_str(), "https://example.com/crash-loop");
|
||||
assert_eq!(snapshot.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recover_tab_command_restores_active_crashed_tab() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let tab_id = core.open_tab(UrlText::parse("https://example.com/recover-command")?);
|
||||
core.crash_active_tab()?;
|
||||
|
||||
core.set_command_query(">recover-tab");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("recover-tab".to_string())));
|
||||
assert_eq!(snapshot.active_tab_id, tab_id);
|
||||
assert_eq!(active_tab.state(), &TabState::Ready);
|
||||
assert_eq!(active_tab.url().as_str(), "https://example.com/recover-command");
|
||||
assert_eq!(snapshot.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_route_title_marks_internal_recovery_page() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let tab_id = core.active_tab()?.id().clone();
|
||||
let recovery_url = UrlText::parse(format!("ely://crash/{}", tab_id.as_str()))?;
|
||||
|
||||
core.open_tab(recovery_url);
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(active_tab.title(), "Tab Recovery");
|
||||
assert_eq!(active_tab.url().as_str(), format!("ely://crash/{}", tab_id.as_str()));
|
||||
Ok(())
|
||||
}
|
||||
@@ -161,6 +161,10 @@ impl BrowserTab {
|
||||
self.state = TabState::Ready;
|
||||
}
|
||||
|
||||
pub fn mark_crashed(&mut self) {
|
||||
self.state = TabState::Crashed;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn flags(&self) -> &TabFlags {
|
||||
&self.flags
|
||||
|
||||
Reference in New Issue
Block a user