From 475be06edccc3cc1e323c4403cbc57438761934a 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 10:49:45 -0400 Subject: [PATCH] Add tab group close command --- crates/ely_browser_core/src/state/commands.rs | 3 ++ .../ely_browser_core/src/state/tab_groups.rs | 19 +++++++ crates/ely_browser_core/tests/tab_groups.rs | 52 ++++++++++++++++++- docs/ui-shell.md | 15 ++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/crates/ely_browser_core/src/state/commands.rs b/crates/ely_browser_core/src/state/commands.rs index 71cc928..1012eeb 100644 --- a/crates/ely_browser_core/src/state/commands.rs +++ b/crates/ely_browser_core/src/state/commands.rs @@ -216,6 +216,9 @@ impl BrowserCore { "sleep-tab-group" | "sleep tab group" | "discard-tab-group" | "discard tab group" => { Ok(self.discard_active_tab_group()?.is_some()) } + "close-tab-group" | "close tab group" | "archive-tab-group" | "archive tab group" => { + Ok(self.close_active_tab_group()?.is_some()) + } "downloads" | "open-downloads" | "open downloads" => { self.open_tab(downloads_url()?); Ok(true) diff --git a/crates/ely_browser_core/src/state/tab_groups.rs b/crates/ely_browser_core/src/state/tab_groups.rs index f733c59..757e8d9 100644 --- a/crates/ely_browser_core/src/state/tab_groups.rs +++ b/crates/ely_browser_core/src/state/tab_groups.rs @@ -93,6 +93,25 @@ impl BrowserCore { Ok(Some(tab_ids.len())) } + pub fn close_active_tab_group(&mut self) -> Result, CoreError> { + let Some(group_id) = self.active_tab_group_id()? else { + return Ok(None); + }; + let tab_ids = self.active_space_group_tab_ids(&group_id); + + for tab_id in &tab_ids { + self.clear_tab_group(tab_id)?; + } + + self.tab_groups.retain(|group| group.id() != &group_id); + + for tab_id in &tab_ids { + self.close_tab(tab_id)?; + } + + Ok(Some(tab_ids.len())) + } + pub fn split_active_tab_group(&mut self) -> Result, CoreError> { let Some(group_id) = self.active_tab_group_id()? else { return Ok(None); diff --git a/crates/ely_browser_core/tests/tab_groups.rs b/crates/ely_browser_core/tests/tab_groups.rs index a7ebc9d..45713dc 100644 --- a/crates/ely_browser_core/tests/tab_groups.rs +++ b/crates/ely_browser_core/tests/tab_groups.rs @@ -1,7 +1,7 @@ use std::error::Error; use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig}; -use ely_domain::{CommandIntent, MAX_SPLIT_PANES, SplitAxis, TabState, UrlText}; +use ely_domain::{ArchiveSource, CommandIntent, MAX_SPLIT_PANES, SplitAxis, TabState, UrlText}; #[test] fn group_active_tab_creates_visible_space_group() -> Result<(), Box> { @@ -203,6 +203,56 @@ fn sleep_tab_group_command_preserves_query_without_active_group() -> Result<(), Ok(()) } +#[test] +fn close_tab_group_command_archives_active_group_tabs() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let ungrouped_tab_id = core.open_tab(UrlText::parse("https://servo.org")?); + let first_group_tab_id = core.open_tab(UrlText::parse("https://example.com/a")?); + core.group_active_tab("Docs")?; + let second_group_tab_id = core.open_tab(UrlText::parse("https://example.com/b")?); + core.group_active_tab("Docs")?; + + core.set_command_query(">close-tab-group"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + let archived_tab_ids = + snapshot.archived_tabs.iter().map(|archived| archived.tab().id()).collect::>(); + + assert_eq!(intent, Some(CommandIntent::Command("close-tab-group".to_string()))); + assert_eq!(snapshot.command_query, ""); + assert!(snapshot.tab_groups.is_empty()); + assert!(snapshot.tabs.iter().any(|tab| tab.id() == &snapshot.active_tab_id)); + assert_ne!(snapshot.active_tab_id, first_group_tab_id); + assert_ne!(snapshot.active_tab_id, second_group_tab_id); + assert_eq!(tab_state(&snapshot, &ungrouped_tab_id), Some(&TabState::Ready)); + assert!(archived_tab_ids.contains(&&first_group_tab_id)); + assert!(archived_tab_ids.contains(&&second_group_tab_id)); + assert!( + snapshot + .archived_tabs + .iter() + .all(|archived| archived.source() == &ArchiveSource::ManualClose) + ); + assert!(snapshot.archived_tabs.iter().all(|archived| archived.tab().group_id().is_none())); + Ok(()) +} + +#[test] +fn close_tab_group_command_preserves_query_without_active_group() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let active_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + + core.set_command_query(">close-tab-group"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + + assert_eq!(intent, Some(CommandIntent::Command("close-tab-group".to_string()))); + assert_eq!(snapshot.command_query, ">close-tab-group"); + assert_eq!(snapshot.active_tab_id, active_tab_id); + assert!(snapshot.archived_tabs.is_empty()); + Ok(()) +} + #[test] fn tab_group_collapse_commands_update_active_group() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/docs/ui-shell.md b/docs/ui-shell.md index 143dae6..cff768b 100644 --- a/docs/ui-shell.md +++ b/docs/ui-shell.md @@ -103,3 +103,18 @@ Group sleep applies the sleeping state to every tab in the active group: │ servo.org │ [Restore] │ └──────────────────────────────┴───────────────────────────────────────────────┘ ``` + +Group close archives every tab in the active group and removes the empty group row: + +```text +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ELY Browser [ >close-tab-group............................ ] [pin] [*] [+] │ +├──────────────────────────────┬───────────────────────────────────────────────┤ +│ Tabs │ servo.org │ +│ New Tab │ https://servo.org │ +│ servo.org │ │ +│ Archive │ Closed group tabs appear in Archive │ +│ example.com │ │ +│ example.com │ │ +└──────────────────────────────┴───────────────────────────────────────────────┘ +```