Restore archived split views as groups

This commit is contained in:
2026-05-08 02:06:43 -04:00
parent 9163d8b514
commit 080d9de7a8
4 changed files with 183 additions and 18 deletions
+2
View File
@@ -81,6 +81,7 @@ pub struct BrowserCore {
download_entries: Vec<DownloadEntry>,
history_entries: Vec<HistoryEntry>,
split_layouts: Vec<SplitLayout>,
archived_split_layouts: Vec<SplitLayout>,
installed_plugins: Vec<InstalledPlugin>,
plugin_audit_events: Vec<PluginAuditEvent>,
active_space_id: SpaceId,
@@ -130,6 +131,7 @@ impl BrowserCore {
download_entries: Vec::new(),
history_entries: Vec::new(),
split_layouts: Vec::new(),
archived_split_layouts: Vec::new(),
installed_plugins: Vec::new(),
plugin_audit_events: Vec::new(),
command_query: String::new(),
+91 -8
View File
@@ -1,6 +1,6 @@
use ely_domain::{
ArchiveSource, ArchivedTab, MAX_SPLIT_PANES, ProfileId, SpaceId, SplitAxis, SplitId,
SplitLayout, SplitPane, TabId,
ArchiveSource, ArchivedTab, BrowserTab, MAX_SPLIT_PANES, ProfileId, SpaceId, SplitAxis,
SplitId, SplitLayout, SplitPane, TabId,
};
use crate::CoreError;
@@ -20,7 +20,8 @@ impl BrowserCore {
}
pub fn close_saved_split_view(&mut self, split_id: &SplitId) -> Result<TabId, CoreError> {
let pane_ids = self.saved_split_pane_ids(split_id)?;
let layout = self.saved_split_layout(split_id)?.clone();
let pane_ids = layout.panes().iter().map(|pane| pane.tab_id().clone()).collect::<Vec<_>>();
self.validate_split_panes_are_open(&pane_ids)?;
let first_close_index = self.first_split_tab_index(&pane_ids)?;
@@ -37,9 +38,10 @@ impl BrowserCore {
.get(&(closed_space_id.clone(), closed_profile_id.clone()))
.is_some_and(|tab_id| pane_ids.contains(tab_id));
let archived_tabs = self.remove_split_tabs(&pane_ids)?;
let archived_tabs = self.remove_split_tabs(&pane_ids, true)?;
self.archived_tabs.extend(archived_tabs);
self.split_layouts.retain(|layout| layout.id() != split_id);
self.archived_split_layouts.push(layout);
self.reselect_after_split_close(
closed_space_id,
@@ -194,11 +196,10 @@ impl BrowserCore {
self.split_layouts.iter().any(|layout| layout.id() == split_id && layout.saved())
}
fn saved_split_pane_ids(&self, split_id: &SplitId) -> Result<Vec<TabId>, CoreError> {
fn saved_split_layout(&self, split_id: &SplitId) -> Result<&SplitLayout, CoreError> {
self.split_layouts
.iter()
.find(|layout| layout.id() == split_id && layout.saved())
.map(|layout| layout.panes().iter().map(|pane| pane.tab_id().clone()).collect())
.ok_or_else(|| CoreError::SplitNotFound { id: split_id.clone() })
}
@@ -219,7 +220,11 @@ impl BrowserCore {
.ok_or(CoreError::MissingActiveTab)
}
fn remove_split_tabs(&mut self, pane_ids: &[TabId]) -> Result<Vec<ArchivedTab>, CoreError> {
fn remove_split_tabs(
&mut self,
pane_ids: &[TabId],
preserve_split_id: bool,
) -> Result<Vec<ArchivedTab>, CoreError> {
let mut archived_tabs = Vec::with_capacity(pane_ids.len());
for pane_id in pane_ids {
let tab_index = self
@@ -228,7 +233,9 @@ impl BrowserCore {
.position(|tab| tab.id() == pane_id)
.ok_or_else(|| CoreError::TabNotFound { id: pane_id.clone() })?;
let mut tab = self.tabs.remove(tab_index);
tab.clear_split_id();
if !preserve_split_id {
tab.clear_split_id();
}
archived_tabs.push(ArchivedTab::new(tab, ArchiveSource::ManualClose));
}
@@ -283,4 +290,80 @@ impl BrowserCore {
Ok(self.active_tab_id.clone())
}
pub(super) fn archived_split_id_for_tab(&self, tab_id: &TabId) -> Option<SplitId> {
let split_id = self
.archived_tabs
.iter()
.find(|archived| archived.tab().id() == tab_id)
.and_then(|archived| archived.tab().split_id())?;
self.archived_split_exists(split_id).then(|| split_id.clone())
}
pub(super) fn restore_archived_split(
&mut self,
split_id: &SplitId,
focus_tab_id: &TabId,
) -> Result<TabId, CoreError> {
let layout = self.archived_split_layout(split_id)?.clone();
let pane_ids = layout.panes().iter().map(|pane| pane.tab_id().clone()).collect::<Vec<_>>();
self.validate_archived_split_panes(&pane_ids)?;
let restored_tabs = self.remove_archived_split_tabs(&pane_ids)?;
self.archived_split_layouts.retain(|layout| layout.id() != split_id);
self.insert_restored_split_tabs(restored_tabs);
self.split_layouts.push(layout);
self.select_tab(focus_tab_id)?;
Ok(focus_tab_id.clone())
}
fn archived_split_exists(&self, split_id: &SplitId) -> bool {
self.archived_split_layouts.iter().any(|layout| layout.id() == split_id)
}
fn archived_split_layout(&self, split_id: &SplitId) -> Result<&SplitLayout, CoreError> {
self.archived_split_layouts
.iter()
.find(|layout| layout.id() == split_id)
.ok_or_else(|| CoreError::SplitNotFound { id: split_id.clone() })
}
fn validate_archived_split_panes(&self, pane_ids: &[TabId]) -> Result<(), CoreError> {
if let Some(missing_tab_id) = pane_ids.iter().find(|tab_id| {
!self.archived_tabs.iter().any(|archived| archived.tab().id() == *tab_id)
}) {
return Err(CoreError::TabNotFound { id: missing_tab_id.clone() });
}
Ok(())
}
fn remove_archived_split_tabs(
&mut self,
pane_ids: &[TabId],
) -> Result<Vec<BrowserTab>, CoreError> {
let mut tabs = Vec::with_capacity(pane_ids.len());
for pane_id in pane_ids {
let archived_index = self
.archived_tabs
.iter()
.position(|archived| archived.tab().id() == pane_id)
.ok_or_else(|| CoreError::TabNotFound { id: pane_id.clone() })?;
tabs.push(self.archived_tabs.remove(archived_index).into_tab());
}
Ok(tabs)
}
fn insert_restored_split_tabs(&mut self, tabs: Vec<BrowserTab>) {
let insert_index = self
.tabs
.iter()
.position(|existing| existing.id() == &self.active_tab_id)
.map_or(self.tabs.len(), |index| index + 1);
for (offset, tab) in tabs.into_iter().enumerate() {
self.tabs.insert(insert_index + offset, tab);
}
}
}
+14 -9
View File
@@ -138,9 +138,8 @@ impl BrowserCore {
}
pub fn restore_last_archived_tab(&mut self) -> Result<TabId, CoreError> {
let archived_tab = self.archived_tabs.pop().ok_or(CoreError::NoArchivedTabs)?;
let tab = archived_tab.into_tab();
self.restore_tab(tab)
let index = self.archived_tabs.len().checked_sub(1).ok_or(CoreError::NoArchivedTabs)?;
self.restore_archived_tab_at_index(index)
}
pub fn restore_archived_tab(&mut self, tab_id: &TabId) -> Result<TabId, CoreError> {
@@ -149,9 +148,7 @@ impl BrowserCore {
.iter()
.position(|archived| archived.tab().id() == tab_id)
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
let archived_tab = self.archived_tabs.remove(index);
let tab = archived_tab.into_tab();
self.restore_tab(tab)
self.restore_archived_tab_at_index(index)
}
pub fn restore_archived_tab_match(&mut self, query: &str) -> Result<Option<TabId>, CoreError> {
@@ -168,9 +165,7 @@ impl BrowserCore {
return Ok(None);
};
let archived_tab = self.archived_tabs.remove(index);
let tab = archived_tab.into_tab();
self.restore_tab(tab).map(Some)
self.restore_archived_tab_at_index(index).map(Some)
}
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
@@ -264,6 +259,16 @@ impl BrowserCore {
Ok(tab_id)
}
fn restore_archived_tab_at_index(&mut self, index: usize) -> Result<TabId, CoreError> {
let tab_id = self.archived_tabs[index].tab().id().clone();
if let Some(split_id) = self.archived_split_id_for_tab(&tab_id) {
return self.restore_archived_split(&split_id, &tab_id);
}
let archived_tab = self.archived_tabs.remove(index);
self.restore_tab(archived_tab.into_tab())
}
fn select_tab_by_offset(&mut self, offset: isize) -> Result<TabId, CoreError> {
let visible_tab_ids = self
.tabs
+76 -1
View File
@@ -91,7 +91,14 @@ fn close_active_tab_archives_saved_split_view() -> Result<(), Box<dyn Error>> {
assert!(snapshot.split_layouts.is_empty());
assert_eq!(snapshot.tabs.len(), 1);
assert_eq!(snapshot.archived_tabs.len(), 2);
assert!(snapshot.archived_tabs.iter().all(|archived| archived.tab().split_id().is_none()));
let archived_split_id =
snapshot.archived_tabs[0].tab().split_id().ok_or("missing archived split id")?;
assert!(
snapshot
.archived_tabs
.iter()
.all(|archived| archived.tab().split_id() == Some(archived_split_id))
);
Ok(())
}
@@ -151,6 +158,74 @@ fn close_split_view_command_preserves_query_without_saved_split() -> Result<(),
Ok(())
}
#[test]
fn restore_last_archived_tab_restores_saved_split_view() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let remaining_tab_id = core.active_tab()?.id().clone();
core.open_tab(UrlText::parse("https://example.com/saved-split")?);
let split_id = core.split_active_tab_right()?;
let focused_pane_id = core.active_tab()?.id().clone();
core.save_active_split_view()?;
core.close_active_tab()?;
let restored_tab_id = core.restore_last_archived_tab()?;
let snapshot = core.snapshot()?;
assert_eq!(restored_tab_id, focused_pane_id);
assert_eq!(snapshot.active_tab_id, focused_pane_id);
assert_eq!(snapshot.tabs.len(), 3);
assert!(snapshot.archived_tabs.is_empty());
let restored_layout = snapshot
.split_layouts
.iter()
.find(|layout| layout.id() == &split_id)
.ok_or("missing restored split layout")?;
assert!(restored_layout.saved());
assert_eq!(restored_layout.pane_count(), 2);
assert!(snapshot.tabs.iter().any(|tab| tab.id() == &remaining_tab_id));
Ok(())
}
#[test]
fn restore_archived_split_member_restores_entire_saved_split_view() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
core.open_tab(UrlText::parse("https://example.com/saved-split")?);
let split_id = core.split_active_tab_right()?;
core.save_active_split_view()?;
let left_pane_id = core.snapshot()?.split_layouts[0].panes()[0].tab_id().clone();
core.close_active_tab()?;
let restored_tab_id = core.restore_archived_tab(&left_pane_id)?;
let snapshot = core.snapshot()?;
assert_eq!(restored_tab_id, left_pane_id);
assert_eq!(snapshot.active_tab_id, left_pane_id);
assert!(snapshot.archived_tabs.is_empty());
assert_eq!(snapshot.split_layouts.len(), 1);
assert_eq!(snapshot.split_layouts[0].id(), &split_id);
assert_eq!(snapshot.split_layouts[0].pane_count(), 2);
Ok(())
}
#[test]
fn archived_split_search_restores_entire_saved_split_view() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
core.open_tab(UrlText::parse("https://example.com/saved-split")?);
let split_id = core.split_active_tab_right()?;
core.save_active_split_view()?;
let left_pane_id = core.snapshot()?.split_layouts[0].panes()[0].tab_id().clone();
core.close_active_tab()?;
let restored_tab_id = core.restore_archived_tab_match("saved-split")?;
let snapshot = core.snapshot()?;
assert_eq!(restored_tab_id, Some(left_pane_id));
assert!(snapshot.archived_tabs.is_empty());
assert_eq!(snapshot.split_layouts.len(), 1);
assert_eq!(snapshot.split_layouts[0].id(), &split_id);
Ok(())
}
#[test]
fn closing_split_pane_dissolves_two_pane_layout() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;