Convert split views to tab groups
This commit is contained in:
@@ -21,6 +21,9 @@ pub enum CoreError {
|
||||
#[error("split not found: {id}")]
|
||||
SplitNotFound { id: SplitId },
|
||||
|
||||
#[error("split pane belongs to a different space: {id}")]
|
||||
SplitPaneSpaceMismatch { id: SplitId },
|
||||
|
||||
#[error("tab group not found: {id}")]
|
||||
TabGroupNotFound { id: TabGroupId },
|
||||
|
||||
|
||||
@@ -104,6 +104,13 @@ pub(crate) fn tab_group_name(command: &str) -> Option<&str> {
|
||||
command_argument(command, &["group-tab ", "group tab ", "tab-group ", "tab group "])
|
||||
}
|
||||
|
||||
pub(crate) fn split_group_name(command: &str) -> Option<&str> {
|
||||
command_argument(
|
||||
command,
|
||||
&["group-split-view ", "group split view ", "split-view-to-group ", "split view to group "],
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_profile_name(command: &str) -> Option<&str> {
|
||||
command_argument(command, &["new-profile ", "new profile "])
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ use crate::{
|
||||
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name, note_body,
|
||||
notes_url, plugin_detail_url, plugins_url, reading_list_url, reading_progress_percent,
|
||||
search_url, settings_page_url, settings_url, shortcut_settings_url, space_icon,
|
||||
switch_profile_name, sync_status_url, tab_group_name, tab_note_body, task_manager_url,
|
||||
split_group_name, switch_profile_name, sync_status_url, tab_group_name, tab_note_body,
|
||||
task_manager_url,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -133,6 +134,12 @@ impl BrowserCore {
|
||||
self.set_active_tab_reading_progress(percent)?;
|
||||
return Ok(true);
|
||||
}
|
||||
if command.eq_ignore_ascii_case("tab group to split") {
|
||||
return Ok(self.split_active_tab_group()?.is_some());
|
||||
}
|
||||
if let Some(name) = split_group_name(command) {
|
||||
return Ok(self.group_active_split_view(Some(name))?.is_some());
|
||||
}
|
||||
if let Some(name) = tab_group_name(command) {
|
||||
self.group_active_tab(name)?;
|
||||
return Ok(true);
|
||||
@@ -159,6 +166,10 @@ impl BrowserCore {
|
||||
"split-tab-group" | "split tab group" | "tab-group-to-split" | "tab group to split" => {
|
||||
Ok(self.split_active_tab_group()?.is_some())
|
||||
}
|
||||
"group-split-view"
|
||||
| "group split view"
|
||||
| "split-view-to-group"
|
||||
| "split view to group" => Ok(self.group_active_split_view(None)?.is_some()),
|
||||
"toggle-tab-group" | "toggle tab group" => {
|
||||
Ok(self.toggle_active_tab_group_collapsed()?.is_some())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ely_domain::{
|
||||
MAX_SPLIT_PANES, SplitAxis, SplitId, SplitLayout, SplitPane, TabGroup, TabGroupId, TabId,
|
||||
BrowserTab, MAX_SPLIT_PANES, SpaceId, SplitAxis, SplitId, SplitLayout, SplitPane, TabGroup,
|
||||
TabGroupId, TabId,
|
||||
};
|
||||
|
||||
use crate::CoreError;
|
||||
@@ -89,6 +90,49 @@ impl BrowserCore {
|
||||
Ok(Some(split_id))
|
||||
}
|
||||
|
||||
pub fn group_active_split_view(
|
||||
&mut self,
|
||||
name: Option<&str>,
|
||||
) -> Result<Option<TabGroupId>, CoreError> {
|
||||
let Some(split_id) = self.active_tab()?.split_id().cloned() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let layout_index = self
|
||||
.split_layouts
|
||||
.iter()
|
||||
.position(|layout| layout.id() == &split_id)
|
||||
.ok_or_else(|| CoreError::SplitNotFound { id: split_id.clone() })?;
|
||||
let layout = self.split_layouts[layout_index].clone();
|
||||
if layout.pane_count() < 2 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let pane_ids = layout.panes().iter().map(|pane| pane.tab_id().clone()).collect::<Vec<_>>();
|
||||
let space_id = self.split_pane_space_id(&split_id, &pane_ids)?;
|
||||
let group_name = name
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map_or_else(|| layout.title().to_string(), ToString::to_string);
|
||||
let color_hex = self
|
||||
.spaces
|
||||
.iter()
|
||||
.find(|space| space.id() == &space_id)
|
||||
.map(|space| space.accent_hex())
|
||||
.ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })?;
|
||||
let sort_key = self.next_tab_group_sort_key_for_space(&space_id);
|
||||
let group = TabGroup::new(space_id, group_name, color_hex, sort_key)?;
|
||||
let group_id = group.id().clone();
|
||||
|
||||
for tab in self.tabs.iter_mut().filter(|tab| pane_ids.contains(tab.id())) {
|
||||
tab.clear_split_id();
|
||||
tab.set_group_id(group_id.clone());
|
||||
}
|
||||
|
||||
self.split_layouts.remove(layout_index);
|
||||
self.tab_groups.push(group);
|
||||
Ok(Some(group_id))
|
||||
}
|
||||
|
||||
pub fn toggle_active_tab_group_collapsed(&mut self) -> Result<Option<bool>, CoreError> {
|
||||
let Some(group_id) = self.active_tab_group_id()? else {
|
||||
return Ok(None);
|
||||
@@ -148,7 +192,7 @@ impl BrowserCore {
|
||||
|
||||
let active_space_id = self.active_space_id.clone();
|
||||
let color_hex = self.active_space()?.accent_hex();
|
||||
let sort_key = self.next_tab_group_sort_key();
|
||||
let sort_key = self.next_tab_group_sort_key_for_space(&active_space_id);
|
||||
let group = TabGroup::new(active_space_id, name, color_hex, sort_key)?;
|
||||
let group_id = group.id().clone();
|
||||
self.tab_groups.push(group);
|
||||
@@ -166,16 +210,16 @@ impl BrowserCore {
|
||||
.map(|group| group.id().clone())
|
||||
}
|
||||
|
||||
fn next_tab_group_sort_key(&self) -> u64 {
|
||||
fn next_tab_group_sort_key_for_space(&self, space_id: &SpaceId) -> u64 {
|
||||
self.tab_groups
|
||||
.iter()
|
||||
.filter(|group| group.space_id() == &self.active_space_id)
|
||||
.filter(|group| group.space_id() == space_id)
|
||||
.map(TabGroup::sort_key)
|
||||
.max()
|
||||
.map_or(0, |sort_key| sort_key.saturating_add(1))
|
||||
}
|
||||
|
||||
fn active_space_group_tabs(&self, group_id: &TabGroupId) -> Vec<ely_domain::BrowserTab> {
|
||||
fn active_space_group_tabs(&self, group_id: &TabGroupId) -> Vec<BrowserTab> {
|
||||
tab_order::sorted_tabs(
|
||||
self.tabs
|
||||
.iter()
|
||||
@@ -184,6 +228,34 @@ impl BrowserCore {
|
||||
)
|
||||
}
|
||||
|
||||
fn split_pane_space_id(
|
||||
&self,
|
||||
split_id: &SplitId,
|
||||
pane_ids: &[TabId],
|
||||
) -> Result<SpaceId, CoreError> {
|
||||
let first_pane_id =
|
||||
pane_ids.first().ok_or_else(|| CoreError::SplitNotFound { id: split_id.clone() })?;
|
||||
let first_tab = self
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|tab| tab.id() == first_pane_id)
|
||||
.ok_or_else(|| CoreError::TabNotFound { id: first_pane_id.clone() })?;
|
||||
let space_id = first_tab.space_id().clone();
|
||||
|
||||
for pane_id in pane_ids {
|
||||
let tab = self
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|tab| tab.id() == pane_id)
|
||||
.ok_or_else(|| CoreError::TabNotFound { id: pane_id.clone() })?;
|
||||
if tab.space_id() != &space_id {
|
||||
return Err(CoreError::SplitPaneSpaceMismatch { id: split_id.clone() });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(space_id)
|
||||
}
|
||||
|
||||
fn active_tab_group_id(&self) -> Result<Option<TabGroupId>, CoreError> {
|
||||
let tab = self.active_tab()?;
|
||||
let Some(group_id) = tab.group_id().cloned() else {
|
||||
|
||||
@@ -158,6 +158,72 @@ fn close_split_view_command_preserves_query_without_saved_split() -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_view_to_group_command_converts_active_split() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let first_tab_id = core.active_tab()?.id().clone();
|
||||
core.split_active_tab_right()?;
|
||||
let second_tab_id = core.active_tab()?.id().clone();
|
||||
core.save_active_split_view()?;
|
||||
|
||||
core.set_command_query(">split-view-to-group Research");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
let group = snapshot.tab_groups.first().ok_or("missing tab group")?;
|
||||
let grouped_tab_ids = snapshot
|
||||
.tabs
|
||||
.iter()
|
||||
.filter(|tab| tab.group_id() == Some(group.id()))
|
||||
.map(|tab| tab.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("split-view-to-group Research".to_string())));
|
||||
assert_eq!(snapshot.active_tab_id, second_tab_id);
|
||||
assert!(snapshot.split_layouts.is_empty());
|
||||
assert_eq!(snapshot.tab_groups.len(), 1);
|
||||
assert_eq!(group.name(), "Research");
|
||||
assert_eq!(grouped_tab_ids, vec![first_tab_id, second_tab_id]);
|
||||
assert!(snapshot.tabs.iter().all(|tab| tab.split_id().is_none()));
|
||||
assert_eq!(snapshot.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_view_to_group_command_uses_saved_split_title_without_name() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
core.split_active_tab_right()?;
|
||||
core.save_active_split_view()?;
|
||||
|
||||
core.set_command_query(">split-view-to-group");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
let group = snapshot.tab_groups.first().ok_or("missing tab group")?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("split-view-to-group".to_string())));
|
||||
assert!(snapshot.split_layouts.is_empty());
|
||||
assert_eq!(group.name(), "Split View: New Tab + New Tab");
|
||||
assert_eq!(snapshot.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_view_to_group_command_preserves_query_without_active_split() -> Result<(), Box<dyn Error>>
|
||||
{
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let active_tab_id = core.active_tab()?.id().clone();
|
||||
|
||||
core.set_command_query(">split-view-to-group Research");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("split-view-to-group Research".to_string())));
|
||||
assert_eq!(snapshot.active_tab_id, active_tab_id);
|
||||
assert!(snapshot.tab_groups.is_empty());
|
||||
assert!(snapshot.split_layouts.is_empty());
|
||||
assert_eq!(snapshot.command_query, ">split-view-to-group Research");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_last_archived_tab_restores_saved_split_view() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
|
||||
@@ -210,6 +210,24 @@ fn split_tab_group_command_converts_active_group_to_split_view() -> Result<(), B
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_group_to_split_alias_converts_active_group_to_split_view() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
core.group_active_tab("Research")?;
|
||||
core.open_tab(UrlText::parse("https://example.com")?);
|
||||
core.group_active_tab("Research")?;
|
||||
|
||||
core.set_command_query(">tab group to split");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("tab group to split".to_string())));
|
||||
assert!(snapshot.tab_groups.is_empty());
|
||||
assert_eq!(snapshot.split_layouts.len(), 1);
|
||||
assert_eq!(snapshot.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tab_group_command_preserves_query_for_single_tab_group() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
|
||||
Reference in New Issue
Block a user