Move active tabs between spaces

This commit is contained in:
2026-05-07 20:34:38 -04:00
parent 73670bbd85
commit b558b1d979
5 changed files with 109 additions and 8 deletions
+11
View File
@@ -30,6 +30,17 @@ pub(crate) fn new_space_name(command: &str) -> Option<&str> {
None 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 { pub(crate) fn space_icon(name: &str) -> String {
name.chars().next().map_or_else(String::new, |value| value.to_string()) name.chars().next().map_or_else(String::new, |value| value.to_string())
} }
+32
View File
@@ -162,6 +162,38 @@ impl BrowserCore {
Ok(tab_id) Ok(tab_id)
} }
pub fn move_active_tab_to_space(&mut self, space_id: &SpaceId) -> Result<TabId, CoreError> {
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<TabId, CoreError> { pub fn close_active_tab(&mut self) -> Result<TabId, CoreError> {
let tab_id = self.active_tab_id.clone(); let tab_id = self.active_tab_id.clone();
self.close_tab(&tab_id) self.close_tab(&tab_id)
+19 -8
View File
@@ -1,8 +1,8 @@
use ely_domain::{CommandIntent, CommandScope}; use ely_domain::{CommandIntent, CommandScope, SpaceId};
use crate::{ use crate::{
CoreError, CoreError,
navigation::{new_space_name, search_url, space_icon}, navigation::{move_tab_space_name, new_space_name, search_url, space_icon},
}; };
use super::BrowserCore; use super::BrowserCore;
@@ -36,12 +36,7 @@ impl BrowserCore {
} }
} }
CommandIntent::ScopedSearch { scope: CommandScope::Spaces, query } => { CommandIntent::ScopedSearch { scope: CommandScope::Spaces, query } => {
let query = query.trim().to_lowercase(); if let Some(space_id) = self.find_space_match(query) {
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())
}) {
self.select_space(&space_id)?; self.select_space(&space_id)?;
self.command_query.clear(); self.command_query.clear();
} }
@@ -63,6 +58,13 @@ impl BrowserCore {
self.create_space(name.to_string(), space_icon(name), 0xf54e00)?; self.create_space(name.to_string(), space_icon(name), 0xf54e00)?;
return Ok(true); 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() { match command.to_ascii_lowercase().as_str() {
"new-tab" => { "new-tab" => {
@@ -88,4 +90,13 @@ impl BrowserCore {
_ => Ok(false), _ => Ok(false),
} }
} }
fn find_space_match(&self, query: &str) -> Option<SpaceId> {
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())
})
}
} }
+43
View File
@@ -116,6 +116,49 @@ fn spaces_scoped_search_preserves_query_without_match() -> Result<(), Box<dyn Er
Ok(()) Ok(())
} }
#[test]
fn move_tab_command_moves_active_tab_to_named_space() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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] #[test]
fn close_tab_command_closes_active_tab() -> Result<(), Box<dyn Error>> { fn close_tab_command_closes_active_tab() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
+4
View File
@@ -106,6 +106,10 @@ impl BrowserTab {
self.flags.pinned = pinned; self.flags.pinned = pinned;
} }
pub fn move_to_space(&mut self, space_id: SpaceId) {
self.space_id = space_id;
}
#[must_use] #[must_use]
pub fn split_id(&self) -> Option<&SplitId> { pub fn split_id(&self) -> Option<&SplitId> {
self.split_id.as_ref() self.split_id.as_ref()