Restore archived split views as groups
This commit is contained in:
@@ -81,6 +81,7 @@ pub struct BrowserCore {
|
|||||||
download_entries: Vec<DownloadEntry>,
|
download_entries: Vec<DownloadEntry>,
|
||||||
history_entries: Vec<HistoryEntry>,
|
history_entries: Vec<HistoryEntry>,
|
||||||
split_layouts: Vec<SplitLayout>,
|
split_layouts: Vec<SplitLayout>,
|
||||||
|
archived_split_layouts: Vec<SplitLayout>,
|
||||||
installed_plugins: Vec<InstalledPlugin>,
|
installed_plugins: Vec<InstalledPlugin>,
|
||||||
plugin_audit_events: Vec<PluginAuditEvent>,
|
plugin_audit_events: Vec<PluginAuditEvent>,
|
||||||
active_space_id: SpaceId,
|
active_space_id: SpaceId,
|
||||||
@@ -130,6 +131,7 @@ impl BrowserCore {
|
|||||||
download_entries: Vec::new(),
|
download_entries: Vec::new(),
|
||||||
history_entries: Vec::new(),
|
history_entries: Vec::new(),
|
||||||
split_layouts: Vec::new(),
|
split_layouts: Vec::new(),
|
||||||
|
archived_split_layouts: Vec::new(),
|
||||||
installed_plugins: Vec::new(),
|
installed_plugins: Vec::new(),
|
||||||
plugin_audit_events: Vec::new(),
|
plugin_audit_events: Vec::new(),
|
||||||
command_query: String::new(),
|
command_query: String::new(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
ArchiveSource, ArchivedTab, MAX_SPLIT_PANES, ProfileId, SpaceId, SplitAxis, SplitId,
|
ArchiveSource, ArchivedTab, BrowserTab, MAX_SPLIT_PANES, ProfileId, SpaceId, SplitAxis,
|
||||||
SplitLayout, SplitPane, TabId,
|
SplitId, SplitLayout, SplitPane, TabId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::CoreError;
|
use crate::CoreError;
|
||||||
@@ -20,7 +20,8 @@ impl BrowserCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_saved_split_view(&mut self, split_id: &SplitId) -> Result<TabId, CoreError> {
|
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)?;
|
self.validate_split_panes_are_open(&pane_ids)?;
|
||||||
|
|
||||||
let first_close_index = self.first_split_tab_index(&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()))
|
.get(&(closed_space_id.clone(), closed_profile_id.clone()))
|
||||||
.is_some_and(|tab_id| pane_ids.contains(tab_id));
|
.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.archived_tabs.extend(archived_tabs);
|
||||||
self.split_layouts.retain(|layout| layout.id() != split_id);
|
self.split_layouts.retain(|layout| layout.id() != split_id);
|
||||||
|
self.archived_split_layouts.push(layout);
|
||||||
|
|
||||||
self.reselect_after_split_close(
|
self.reselect_after_split_close(
|
||||||
closed_space_id,
|
closed_space_id,
|
||||||
@@ -194,11 +196,10 @@ impl BrowserCore {
|
|||||||
self.split_layouts.iter().any(|layout| layout.id() == split_id && layout.saved())
|
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
|
self.split_layouts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|layout| layout.id() == split_id && layout.saved())
|
.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() })
|
.ok_or_else(|| CoreError::SplitNotFound { id: split_id.clone() })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +220,11 @@ impl BrowserCore {
|
|||||||
.ok_or(CoreError::MissingActiveTab)
|
.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());
|
let mut archived_tabs = Vec::with_capacity(pane_ids.len());
|
||||||
for pane_id in pane_ids {
|
for pane_id in pane_ids {
|
||||||
let tab_index = self
|
let tab_index = self
|
||||||
@@ -228,7 +233,9 @@ impl BrowserCore {
|
|||||||
.position(|tab| tab.id() == pane_id)
|
.position(|tab| tab.id() == pane_id)
|
||||||
.ok_or_else(|| CoreError::TabNotFound { id: pane_id.clone() })?;
|
.ok_or_else(|| CoreError::TabNotFound { id: pane_id.clone() })?;
|
||||||
let mut tab = self.tabs.remove(tab_index);
|
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));
|
archived_tabs.push(ArchivedTab::new(tab, ArchiveSource::ManualClose));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,4 +290,80 @@ impl BrowserCore {
|
|||||||
|
|
||||||
Ok(self.active_tab_id.clone())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,9 +138,8 @@ impl BrowserCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_last_archived_tab(&mut self) -> Result<TabId, CoreError> {
|
pub fn restore_last_archived_tab(&mut self) -> Result<TabId, CoreError> {
|
||||||
let archived_tab = self.archived_tabs.pop().ok_or(CoreError::NoArchivedTabs)?;
|
let index = self.archived_tabs.len().checked_sub(1).ok_or(CoreError::NoArchivedTabs)?;
|
||||||
let tab = archived_tab.into_tab();
|
self.restore_archived_tab_at_index(index)
|
||||||
self.restore_tab(tab)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_archived_tab(&mut self, tab_id: &TabId) -> Result<TabId, CoreError> {
|
pub fn restore_archived_tab(&mut self, tab_id: &TabId) -> Result<TabId, CoreError> {
|
||||||
@@ -149,9 +148,7 @@ impl BrowserCore {
|
|||||||
.iter()
|
.iter()
|
||||||
.position(|archived| archived.tab().id() == tab_id)
|
.position(|archived| archived.tab().id() == tab_id)
|
||||||
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
|
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
|
||||||
let archived_tab = self.archived_tabs.remove(index);
|
self.restore_archived_tab_at_index(index)
|
||||||
let tab = archived_tab.into_tab();
|
|
||||||
self.restore_tab(tab)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_archived_tab_match(&mut self, query: &str) -> Result<Option<TabId>, CoreError> {
|
pub fn restore_archived_tab_match(&mut self, query: &str) -> Result<Option<TabId>, CoreError> {
|
||||||
@@ -168,9 +165,7 @@ impl BrowserCore {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let archived_tab = self.archived_tabs.remove(index);
|
self.restore_archived_tab_at_index(index).map(Some)
|
||||||
let tab = archived_tab.into_tab();
|
|
||||||
self.restore_tab(tab).map(Some)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
|
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
|
||||||
@@ -264,6 +259,16 @@ impl BrowserCore {
|
|||||||
Ok(tab_id)
|
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> {
|
fn select_tab_by_offset(&mut self, offset: isize) -> Result<TabId, CoreError> {
|
||||||
let visible_tab_ids = self
|
let visible_tab_ids = self
|
||||||
.tabs
|
.tabs
|
||||||
|
|||||||
@@ -91,7 +91,14 @@ fn close_active_tab_archives_saved_split_view() -> Result<(), Box<dyn Error>> {
|
|||||||
assert!(snapshot.split_layouts.is_empty());
|
assert!(snapshot.split_layouts.is_empty());
|
||||||
assert_eq!(snapshot.tabs.len(), 1);
|
assert_eq!(snapshot.tabs.len(), 1);
|
||||||
assert_eq!(snapshot.archived_tabs.len(), 2);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +158,74 @@ fn close_split_view_command_preserves_query_without_saved_split() -> Result<(),
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn closing_split_pane_dissolves_two_pane_layout() -> Result<(), Box<dyn Error>> {
|
fn closing_split_pane_dissolves_two_pane_layout() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user