From cdb82bcaceab902b3d2f9e66e4961ce8523ee2a5 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 08:57:18 -0400 Subject: [PATCH] Add space switching shortcuts --- crates/ely_app/src/main.rs | 5 +++ crates/ely_app/src/shell/mod.rs | 1 + crates/ely_app/src/shell/render.rs | 2 + crates/ely_app/src/shell/spaces.rs | 34 ++++++++++++++++ crates/ely_app/src/shortcuts.rs | 53 ++++++++++++++++++++++++- crates/ely_browser_core/src/state.rs | 26 ++++++++++++ crates/ely_browser_core/tests/spaces.rs | 23 +++++++++++ 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 crates/ely_app/src/shell/spaces.rs diff --git a/crates/ely_app/src/main.rs b/crates/ely_app/src/main.rs index d8d7c02..ab430d1 100644 --- a/crates/ely_app/src/main.rs +++ b/crates/ely_app/src/main.rs @@ -23,7 +23,9 @@ actions!( OpenTaskManager, Quit, RestoreClosedTab, + SelectNextSpace, SelectNextTab, + SelectPreviousSpace, SelectPreviousTab, SplitRight, ToggleFavoriteTab, @@ -59,6 +61,9 @@ fn main() { MenuItem::separator(), MenuItem::action("Restore Closed Tab", RestoreClosedTab), MenuItem::separator(), + MenuItem::action("Next Space", SelectNextSpace), + MenuItem::action("Previous Space", SelectPreviousSpace), + MenuItem::separator(), MenuItem::action("Open Downloads", OpenDownloads), MenuItem::action("Open History", OpenHistory), MenuItem::action("Open Task Manager", OpenTaskManager), diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 2c15338..f807c2d 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -8,6 +8,7 @@ mod reading_list; mod render; mod sidebar; mod site_permissions; +mod spaces; mod splits; mod tab_groups; diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index 2d16dc7..b480616 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -51,7 +51,9 @@ impl ElyShell { .on_action(cx.listener(Self::on_open_settings)) .on_action(cx.listener(Self::on_open_task_manager)) .on_action(cx.listener(Self::on_restore_closed_tab)) + .on_action(cx.listener(Self::on_select_next_space)) .on_action(cx.listener(Self::on_select_next_tab)) + .on_action(cx.listener(Self::on_select_previous_space)) .on_action(cx.listener(Self::on_select_previous_tab)) .on_action(cx.listener(Self::on_split_right)) .on_action(cx.listener(Self::on_toggle_favorite_tab)) diff --git a/crates/ely_app/src/shell/spaces.rs b/crates/ely_app/src/shell/spaces.rs new file mode 100644 index 0000000..e992f9a --- /dev/null +++ b/crates/ely_app/src/shell/spaces.rs @@ -0,0 +1,34 @@ +use gpui::{Context, Window}; + +use super::{ElyShell, ShellState}; +use crate::{SelectNextSpace, SelectPreviousSpace}; + +impl ElyShell { + pub(super) fn on_select_next_space( + &mut self, + _: &SelectNextSpace, + window: &mut Window, + cx: &mut Context, + ) { + if let ShellState::Ready(core) = &mut self.state + && core.select_next_space().is_ok() + { + self.sync_address_input(window, cx); + cx.notify(); + } + } + + pub(super) fn on_select_previous_space( + &mut self, + _: &SelectPreviousSpace, + window: &mut Window, + cx: &mut Context, + ) { + if let ShellState::Ready(core) = &mut self.state + && core.select_previous_space().is_ok() + { + self.sync_address_input(window, cx); + cx.notify(); + } + } +} diff --git a/crates/ely_app/src/shortcuts.rs b/crates/ely_app/src/shortcuts.rs index f0ea04a..46bcede 100644 --- a/crates/ely_app/src/shortcuts.rs +++ b/crates/ely_app/src/shortcuts.rs @@ -4,8 +4,8 @@ use gpui::{App, KeyBinding}; use crate::{ CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab, - OpenSettings, OpenTaskManager, Quit, RestoreClosedTab, SelectNextTab, SelectPreviousTab, - SplitRight, ToggleFavoriteTab, ToggleSidebar, + OpenSettings, OpenTaskManager, Quit, RestoreClosedTab, SelectNextSpace, SelectNextTab, + SelectPreviousSpace, SelectPreviousTab, SplitRight, ToggleFavoriteTab, ToggleSidebar, }; #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -30,6 +30,8 @@ pub(crate) enum ShortcutAction { OpenNewTab, CloseCurrentTab, RestoreClosedTab, + SelectNextSpace, + SelectPreviousSpace, SelectNextTab, SelectPreviousTab, SplitRight, @@ -50,6 +52,8 @@ impl ShortcutAction { Self::OpenNewTab => "New Tab", Self::CloseCurrentTab => "Close Tab", Self::RestoreClosedTab => "Restore Closed Tab", + Self::SelectNextSpace => "Next Space", + Self::SelectPreviousSpace => "Previous Space", Self::SelectNextTab => "Next Tab", Self::SelectPreviousTab => "Previous Tab", Self::SplitRight => "Split Right", @@ -69,6 +73,8 @@ impl ShortcutAction { Self::OpenNewTab | Self::CloseCurrentTab | Self::RestoreClosedTab + | Self::SelectNextSpace + | Self::SelectPreviousSpace | Self::SelectNextTab | Self::SelectPreviousTab | Self::SplitRight @@ -87,6 +93,8 @@ impl ShortcutAction { Self::OpenNewTab => Some(">new-tab"), Self::CloseCurrentTab => Some(">close-tab"), Self::RestoreClosedTab => Some(">restore-tab"), + Self::SelectNextSpace => None, + Self::SelectPreviousSpace => None, Self::SelectNextTab => None, Self::SelectPreviousTab => None, Self::SplitRight => Some(">split-right"), @@ -127,6 +135,8 @@ pub(crate) const SHORTCUT_ACTIONS: &[ShortcutAction] = &[ ShortcutAction::OpenNewTab, ShortcutAction::CloseCurrentTab, ShortcutAction::RestoreClosedTab, + ShortcutAction::SelectNextSpace, + ShortcutAction::SelectPreviousSpace, ShortcutAction::SelectNextTab, ShortcutAction::SelectPreviousTab, ShortcutAction::SplitRight, @@ -164,6 +174,10 @@ pub(crate) const SHORTCUT_BINDINGS: &[ShortcutBinding] = &[ shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-f"), shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::Macos, "cmd-shift-p"), shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::WindowsLinux, "ctrl-shift-p"), + shortcut(ShortcutAction::SelectNextSpace, ShortcutPlatform::Macos, "cmd-alt-right"), + shortcut(ShortcutAction::SelectNextSpace, ShortcutPlatform::WindowsLinux, "ctrl-alt-right"), + shortcut(ShortcutAction::SelectPreviousSpace, ShortcutPlatform::Macos, "cmd-alt-left"), + shortcut(ShortcutAction::SelectPreviousSpace, ShortcutPlatform::WindowsLinux, "ctrl-alt-left"), shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::Macos, "cmd-shift-]"), shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::Macos, "cmd-alt-down"), shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::WindowsLinux, "ctrl-tab"), @@ -239,7 +253,13 @@ impl ShortcutBinding { ShortcutAction::RestoreClosedTab => { KeyBinding::new(self.keystroke, RestoreClosedTab, None) } + ShortcutAction::SelectNextSpace => { + KeyBinding::new(self.keystroke, SelectNextSpace, None) + } ShortcutAction::SelectNextTab => KeyBinding::new(self.keystroke, SelectNextTab, None), + ShortcutAction::SelectPreviousSpace => { + KeyBinding::new(self.keystroke, SelectPreviousSpace, None) + } ShortcutAction::SelectPreviousTab => { KeyBinding::new(self.keystroke, SelectPreviousTab, None) } @@ -310,6 +330,35 @@ mod tests { assert_eq!(bindings, vec!["Cmd + B".to_string(), "Ctrl + B".to_string()]); } + #[test] + fn space_switch_shortcuts_have_platform_bindings() { + let next_bindings = + bindings_for_action(ShortcutAction::SelectNextSpace, ShortcutPlatform::Macos) + .chain(bindings_for_action( + ShortcutAction::SelectNextSpace, + ShortcutPlatform::WindowsLinux, + )) + .map(|binding| binding.display_keystroke()) + .collect::>(); + let previous_bindings = + bindings_for_action(ShortcutAction::SelectPreviousSpace, ShortcutPlatform::Macos) + .chain(bindings_for_action( + ShortcutAction::SelectPreviousSpace, + ShortcutPlatform::WindowsLinux, + )) + .map(|binding| binding.display_keystroke()) + .collect::>(); + + assert_eq!( + next_bindings, + vec!["Cmd + Option + RIGHT".to_string(), "Ctrl + Alt + RIGHT".to_string()] + ); + assert_eq!( + previous_bindings, + vec!["Cmd + Option + LEFT".to_string(), "Ctrl + Alt + LEFT".to_string()] + ); + } + #[test] fn every_declared_action_has_a_binding() { for action in SHORTCUT_ACTIONS { diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index d4e2154..949dfb6 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -229,6 +229,32 @@ impl BrowserCore { Ok(tab_id) } + pub fn select_next_space(&mut self) -> Result { + let spaces = self.sorted_spaces(); + let Some(active_index) = + spaces.iter().position(|space| space.id() == &self.active_space_id) + else { + return Err(CoreError::SpaceNotFound { id: self.active_space_id.clone() }); + }; + + let next_index = (active_index + 1) % spaces.len(); + let space_id = spaces[next_index].id().clone(); + self.select_space(&space_id) + } + + pub fn select_previous_space(&mut self) -> Result { + let spaces = self.sorted_spaces(); + let Some(active_index) = + spaces.iter().position(|space| space.id() == &self.active_space_id) + else { + return Err(CoreError::SpaceNotFound { id: self.active_space_id.clone() }); + }; + + let previous_index = if active_index == 0 { spaces.len() - 1 } else { active_index - 1 }; + let space_id = spaces[previous_index].id().clone(); + self.select_space(&space_id) + } + pub fn set_active_space_archive_policy( &mut self, archive_policy: ArchivePolicy, diff --git a/crates/ely_browser_core/tests/spaces.rs b/crates/ely_browser_core/tests/spaces.rs index 7abe71d..edc63c6 100644 --- a/crates/ely_browser_core/tests/spaces.rs +++ b/crates/ely_browser_core/tests/spaces.rs @@ -152,6 +152,29 @@ fn space_sidebar_width_updates_selected_space() -> Result<(), Box> { Ok(()) } +#[test] +fn selecting_adjacent_spaces_uses_sort_order_with_wraparound() -> 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", 0x9fc9a2)?; + let personal_space_id = core.create_space("Personal", "P", 0x8eb7d4)?; + + core.set_space_sort_key(&work_space_id, 20)?; + core.set_space_sort_key(&research_space_id, 10)?; + core.set_space_sort_key(&personal_space_id, 30)?; + core.select_space(&work_space_id)?; + + core.select_next_space()?; + assert_eq!(core.snapshot()?.active_space_id, personal_space_id); + + core.select_next_space()?; + assert_eq!(core.snapshot()?.active_space_id, research_space_id); + + core.select_previous_space()?; + assert_eq!(core.snapshot()?.active_space_id, personal_space_id); + Ok(()) +} + fn active_space_updated_at( core: &BrowserCore, space_id: &ely_domain::SpaceId,