diff --git a/crates/ely_browser_core/src/navigation.rs b/crates/ely_browser_core/src/navigation.rs index b3a1d86..cfeb401 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -30,6 +30,17 @@ pub(crate) fn new_space_name(command: &str) -> Option<&str> { None } +pub(crate) fn move_tab_space_name(command: &str) -> Option<&str> { + let normalized_command = command.to_ascii_lowercase(); + for prefix in ["move-tab ", "move tab ", "move-tab-to-space ", "move tab to space "] { + if normalized_command.starts_with(prefix) { + let name = command[prefix.len()..].trim(); + return (!name.is_empty()).then_some(name); + } + } + None +} + pub(crate) fn space_icon(name: &str) -> String { name.chars().next().map_or_else(String::new, |value| value.to_string()) } diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 3219f22..9962503 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -162,6 +162,38 @@ impl BrowserCore { Ok(tab_id) } + pub fn move_active_tab_to_space(&mut self, space_id: &SpaceId) -> Result { + if !self.spaces.iter().any(|space| space.id() == space_id) { + return Err(CoreError::SpaceNotFound { id: space_id.clone() }); + } + + let tab_index = self.active_tab_index()?; + let tab_id = self.active_tab_id.clone(); + let source_space_id = self.tabs[tab_index].space_id().clone(); + if &source_space_id == space_id { + return Ok(tab_id); + } + + self.tabs[tab_index].move_to_space(space_id.clone()); + self.active_tabs_by_space.insert(space_id.clone(), tab_id.clone()); + + if let Some(next_tab_id) = self.nearest_tab_in_space(&source_space_id, tab_index) { + self.active_tabs_by_space.insert(source_space_id, next_tab_id); + } else { + let tab = self.build_tab_for( + source_space_id.clone(), + self.active_profile_id.clone(), + self.new_tab_url.clone(), + ); + let replacement_id = tab.id().clone(); + self.tabs.insert(tab_index, tab); + self.active_tabs_by_space.insert(source_space_id, replacement_id); + } + + self.select_tab(&tab_id)?; + Ok(tab_id) + } + pub fn close_active_tab(&mut self) -> Result { let tab_id = self.active_tab_id.clone(); self.close_tab(&tab_id) diff --git a/crates/ely_browser_core/src/state/commands.rs b/crates/ely_browser_core/src/state/commands.rs index a9dfe66..aad2a46 100644 --- a/crates/ely_browser_core/src/state/commands.rs +++ b/crates/ely_browser_core/src/state/commands.rs @@ -1,8 +1,8 @@ -use ely_domain::{CommandIntent, CommandScope}; +use ely_domain::{CommandIntent, CommandScope, SpaceId}; use crate::{ CoreError, - navigation::{new_space_name, search_url, space_icon}, + navigation::{move_tab_space_name, new_space_name, search_url, space_icon}, }; use super::BrowserCore; @@ -36,12 +36,7 @@ impl BrowserCore { } } CommandIntent::ScopedSearch { scope: CommandScope::Spaces, query } => { - let query = query.trim().to_lowercase(); - if let Some(space_id) = self.spaces.iter().find_map(|space| { - (space.name().to_lowercase().contains(&query) - || space.icon().to_lowercase().contains(&query)) - .then(|| space.id().clone()) - }) { + if let Some(space_id) = self.find_space_match(query) { self.select_space(&space_id)?; self.command_query.clear(); } @@ -63,6 +58,13 @@ impl BrowserCore { self.create_space(name.to_string(), space_icon(name), 0xf54e00)?; return Ok(true); } + if let Some(name) = move_tab_space_name(command) { + let Some(space_id) = self.find_space_match(name) else { + return Ok(false); + }; + self.move_active_tab_to_space(&space_id)?; + return Ok(true); + } match command.to_ascii_lowercase().as_str() { "new-tab" => { @@ -88,4 +90,13 @@ impl BrowserCore { _ => Ok(false), } } + + fn find_space_match(&self, query: &str) -> Option { + let query = query.trim().to_lowercase(); + self.spaces.iter().find_map(|space| { + (space.name().to_lowercase().contains(&query) + || space.icon().to_lowercase().contains(&query)) + .then(|| space.id().clone()) + }) + } } diff --git a/crates/ely_browser_core/tests/commands.rs b/crates/ely_browser_core/tests/commands.rs index afa4907..6f9187d 100644 --- a/crates/ely_browser_core/tests/commands.rs +++ b/crates/ely_browser_core/tests/commands.rs @@ -116,6 +116,49 @@ fn spaces_scoped_search_preserves_query_without_match() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let work_space_id = core.snapshot()?.active_space_id; + let research_space_id = core.create_space("Research", "R", 0xf54e00)?; + core.select_space(&work_space_id)?; + let moved_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + + core.set_command_query(">move-tab Research"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + let active_tab = core.active_tab()?; + + assert_eq!(intent, Some(CommandIntent::Command("move-tab Research".to_string()))); + assert_eq!(snapshot.active_space_id, research_space_id); + assert_eq!(snapshot.active_tab_id, moved_tab_id); + assert_eq!(active_tab.space_id(), &snapshot.active_space_id); + assert_eq!(active_tab.url().as_str(), "https://example.com"); + assert_eq!(snapshot.command_query, ""); + + core.select_space(&work_space_id)?; + let work_snapshot = core.snapshot()?; + assert!(work_snapshot.tabs.iter().all(|tab| tab.id() != &moved_tab_id)); + Ok(()) +} + +#[test] +fn move_tab_command_preserves_query_without_matching_space() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let active_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + let active_space_id = core.snapshot()?.active_space_id; + + core.set_command_query(">move-tab Missing"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + + assert_eq!(intent, Some(CommandIntent::Command("move-tab Missing".to_string()))); + assert_eq!(snapshot.active_tab_id, active_tab_id); + assert_eq!(snapshot.active_space_id, active_space_id); + assert_eq!(snapshot.command_query, ">move-tab Missing"); + Ok(()) +} + #[test] fn close_tab_command_closes_active_tab() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/crates/ely_domain/src/tab.rs b/crates/ely_domain/src/tab.rs index 9e7738c..bd62b5a 100644 --- a/crates/ely_domain/src/tab.rs +++ b/crates/ely_domain/src/tab.rs @@ -106,6 +106,10 @@ impl BrowserTab { self.flags.pinned = pinned; } + pub fn move_to_space(&mut self, space_id: SpaceId) { + self.space_id = space_id; + } + #[must_use] pub fn split_id(&self) -> Option<&SplitId> { self.split_id.as_ref()