Add domain tab auto grouping
This commit is contained in:
@@ -195,6 +195,10 @@ impl BrowserCore {
|
|||||||
"split-tab-group" | "split tab group" | "tab-group-to-split" | "tab group to split" => {
|
"split-tab-group" | "split tab group" | "tab-group-to-split" | "tab group to split" => {
|
||||||
Ok(self.split_active_tab_group()?.is_some())
|
Ok(self.split_active_tab_group()?.is_some())
|
||||||
}
|
}
|
||||||
|
"auto-group-domains"
|
||||||
|
| "auto group domains"
|
||||||
|
| "auto-group-domain-tabs"
|
||||||
|
| "auto group domain tabs" => Ok(self.auto_group_active_space_tabs_by_domain()? > 0),
|
||||||
"group-split-view"
|
"group-split-view"
|
||||||
| "group split view"
|
| "group split view"
|
||||||
| "split-view-to-group"
|
| "split-view-to-group"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
BrowserTab, MAX_SPLIT_PANES, SpaceId, SplitAxis, SplitId, SplitLayout, SplitPane, TabGroup,
|
BrowserTab, MAX_SPLIT_PANES, SpaceId, SplitAxis, SplitId, SplitLayout, SplitPane, TabGroup,
|
||||||
TabGroupId, TabId,
|
TabGroupId, TabId,
|
||||||
@@ -15,6 +17,25 @@ impl BrowserCore {
|
|||||||
Ok(group_id)
|
Ok(group_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auto_group_active_space_tabs_by_domain(&mut self) -> Result<usize, CoreError> {
|
||||||
|
let domain_tab_ids = self.active_space_domain_tab_ids();
|
||||||
|
let mut grouped_count = 0;
|
||||||
|
|
||||||
|
for (domain, tab_ids) in domain_tab_ids {
|
||||||
|
if tab_ids.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let group_id = self.find_or_create_active_space_tab_group(domain)?;
|
||||||
|
for tab_id in tab_ids {
|
||||||
|
self.assign_tab_to_group(&tab_id, &group_id)?;
|
||||||
|
grouped_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(grouped_count)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn assign_tab_to_group(
|
pub fn assign_tab_to_group(
|
||||||
&mut self,
|
&mut self,
|
||||||
tab_id: &TabId,
|
tab_id: &TabId,
|
||||||
@@ -210,6 +231,26 @@ impl BrowserCore {
|
|||||||
.map(|group| group.id().clone())
|
.map(|group| group.id().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_space_domain_tab_ids(&self) -> BTreeMap<String, Vec<TabId>> {
|
||||||
|
let mut domain_tab_ids = BTreeMap::new();
|
||||||
|
for tab in self.tabs.iter().filter(|tab| self.tab_is_domain_group_candidate(tab)) {
|
||||||
|
let Some(domain) = domain_group_name(tab) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
domain_tab_ids.entry(domain).or_insert_with(Vec::new).push(tab.id().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
domain_tab_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_is_domain_group_candidate(&self, tab: &BrowserTab) -> bool {
|
||||||
|
tab.space_id() == &self.active_space_id
|
||||||
|
&& tab.group_id().is_none()
|
||||||
|
&& tab.split_id().is_none()
|
||||||
|
&& !tab.flags().favorite
|
||||||
|
&& !tab.flags().pinned
|
||||||
|
}
|
||||||
|
|
||||||
fn next_tab_group_sort_key_for_space(&self, space_id: &SpaceId) -> u64 {
|
fn next_tab_group_sort_key_for_space(&self, space_id: &SpaceId) -> u64 {
|
||||||
self.tab_groups
|
self.tab_groups
|
||||||
.iter()
|
.iter()
|
||||||
@@ -275,3 +316,11 @@ impl BrowserCore {
|
|||||||
.ok_or_else(|| CoreError::TabGroupNotFound { id: group_id.clone() })
|
.ok_or_else(|| CoreError::TabGroupNotFound { id: group_id.clone() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn domain_group_name(tab: &BrowserTab) -> Option<String> {
|
||||||
|
if tab.url().as_str().starts_with("ely://") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.url().host()
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,6 +103,70 @@ fn group_tab_command_groups_active_tab() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_group_active_space_tabs_by_domain_groups_matching_hosts() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let first_tab_id = core.open_tab(UrlText::parse("https://example.com/a")?);
|
||||||
|
let second_tab_id = core.open_tab(UrlText::parse("https://example.com/b")?);
|
||||||
|
let singleton_tab_id = core.open_tab(UrlText::parse("https://servo.org")?);
|
||||||
|
|
||||||
|
let grouped_count = core.auto_group_active_space_tabs_by_domain()?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
let group = snapshot.tab_groups.first().ok_or("missing domain tab group")?;
|
||||||
|
|
||||||
|
assert_eq!(grouped_count, 2);
|
||||||
|
assert_eq!(snapshot.tab_groups.len(), 1);
|
||||||
|
assert_eq!(group.name(), "example.com");
|
||||||
|
assert_eq!(group.space_id(), &snapshot.active_space_id);
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &first_tab_id), Some(group.id()));
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &second_tab_id), Some(group.id()));
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &singleton_tab_id), None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_group_domain_command_groups_matching_hosts() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let first_tab_id = core.open_tab(UrlText::parse("https://example.com/one")?);
|
||||||
|
let second_tab_id = core.open_tab(UrlText::parse("https://example.com/two")?);
|
||||||
|
|
||||||
|
core.set_command_query(">auto-group-domains");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
let group = snapshot.tab_groups.first().ok_or("missing domain tab group")?;
|
||||||
|
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("auto-group-domains".to_string())));
|
||||||
|
assert_eq!(snapshot.command_query, "");
|
||||||
|
assert_eq!(snapshot.tab_groups.len(), 1);
|
||||||
|
assert_eq!(group.name(), "example.com");
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &first_tab_id), Some(group.id()));
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &second_tab_id), Some(group.id()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_group_domains_preserves_existing_manual_groups() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let manual_tab_id = core.open_tab(UrlText::parse("https://example.com/manual")?);
|
||||||
|
let manual_group_id = core.group_active_tab("Manual")?;
|
||||||
|
let first_auto_tab_id = core.open_tab(UrlText::parse("https://example.com/auto-a")?);
|
||||||
|
let second_auto_tab_id = core.open_tab(UrlText::parse("https://example.com/auto-b")?);
|
||||||
|
|
||||||
|
let grouped_count = core.auto_group_active_space_tabs_by_domain()?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
let domain_group = snapshot
|
||||||
|
.tab_groups
|
||||||
|
.iter()
|
||||||
|
.find(|group| group.name() == "example.com")
|
||||||
|
.ok_or("missing domain tab group")?;
|
||||||
|
|
||||||
|
assert_eq!(grouped_count, 2);
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &manual_tab_id), Some(&manual_group_id));
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &first_auto_tab_id), Some(domain_group.id()));
|
||||||
|
assert_eq!(tab_group_id(&snapshot, &second_auto_tab_id), Some(domain_group.id()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tab_group_collapse_commands_update_active_group() -> Result<(), Box<dyn Error>> {
|
fn tab_group_collapse_commands_update_active_group() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
@@ -262,3 +326,10 @@ fn split_tab_group_rejects_groups_above_pane_limit() -> Result<(), Box<dyn Error
|
|||||||
assert_eq!(error, CoreError::SplitPaneLimitReached { limit: MAX_SPLIT_PANES });
|
assert_eq!(error, CoreError::SplitPaneLimitReached { limit: MAX_SPLIT_PANES });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tab_group_id<'a>(
|
||||||
|
snapshot: &'a ely_browser_core::BrowserSnapshot,
|
||||||
|
tab_id: &ely_domain::TabId,
|
||||||
|
) -> Option<&'a ely_domain::TabGroupId> {
|
||||||
|
snapshot.tabs.iter().find(|tab| tab.id() == tab_id).and_then(|tab| tab.group_id())
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,3 +68,21 @@ Sleeping tabs keep the same layout rhythm:
|
|||||||
│ │ └─────────────────────────────────────────┘ │
|
│ │ └─────────────────────────────────────────┘ │
|
||||||
└──────────────────────────────┴───────────────────────────────────────────────┘
|
└──────────────────────────────┴───────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Domain auto grouping keeps manual groups intact and groups matching ungrouped hosts:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ELY Browser [ >auto-group-domains......................... ] [pin] [*] [+] │
|
||||||
|
├──────────────────────────────┬───────────────────────────────────────────────┤
|
||||||
|
│ Tabs │ example.com │
|
||||||
|
│ [folder] example.com │ https://example.com/a │
|
||||||
|
│ 2 tabs - Expanded │ │
|
||||||
|
│ example.com │ │
|
||||||
|
│ example.com │ │
|
||||||
|
│ example.com │ │
|
||||||
|
│ example.com │ │
|
||||||
|
│ servo.org │ │
|
||||||
|
│ servo.org │ │
|
||||||
|
└──────────────────────────────┴───────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user