From 080d9de7a8deed5d16debb33691117f3edcd021d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 02:06:43 -0400 Subject: [PATCH] Restore archived split views as groups --- crates/ely_browser_core/src/state.rs | 2 + crates/ely_browser_core/src/state/splits.rs | 99 +++++++++++++++++++-- crates/ely_browser_core/src/state/tabs.rs | 23 +++-- crates/ely_browser_core/tests/splits.rs | 77 +++++++++++++++- 4 files changed, 183 insertions(+), 18 deletions(-) diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index e70af1e..ee80989 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -81,6 +81,7 @@ pub struct BrowserCore { download_entries: Vec, history_entries: Vec, split_layouts: Vec, + archived_split_layouts: Vec, installed_plugins: Vec, plugin_audit_events: Vec, 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(), diff --git a/crates/ely_browser_core/src/state/splits.rs b/crates/ely_browser_core/src/state/splits.rs index 3a7dd48..b81933d 100644 --- a/crates/ely_browser_core/src/state/splits.rs +++ b/crates/ely_browser_core/src/state/splits.rs @@ -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 { - 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::>(); 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, 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, CoreError> { + fn remove_split_tabs( + &mut self, + pane_ids: &[TabId], + preserve_split_id: bool, + ) -> Result, 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 { + 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 { + let layout = self.archived_split_layout(split_id)?.clone(); + let pane_ids = layout.panes().iter().map(|pane| pane.tab_id().clone()).collect::>(); + 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, 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) { + 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); + } + } } diff --git a/crates/ely_browser_core/src/state/tabs.rs b/crates/ely_browser_core/src/state/tabs.rs index 72135e0..2cad9b9 100644 --- a/crates/ely_browser_core/src/state/tabs.rs +++ b/crates/ely_browser_core/src/state/tabs.rs @@ -138,9 +138,8 @@ impl BrowserCore { } pub fn restore_last_archived_tab(&mut self) -> Result { - 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 { @@ -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, 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 { + 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 { let visible_tab_ids = self .tabs diff --git a/crates/ely_browser_core/tests/splits.rs b/crates/ely_browser_core/tests/splits.rs index ef4b5e3..3124f54 100644 --- a/crates/ely_browser_core/tests/splits.rs +++ b/crates/ely_browser_core/tests/splits.rs @@ -91,7 +91,14 @@ fn close_active_tab_archives_saved_split_view() -> Result<(), Box> { 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> { + 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> { + 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> { + 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> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;