workspace: Add actions for cycling between windows (#23356)

tims created

Closes #22740

I haven't assigned any default keybindings to these actions because it
might conflict with existing OS bindings.

Preview:


https://github.com/user-attachments/assets/7c62cb34-2747-4674-a278-f0998e7d17f9

Release Notes:

- Added `workspace::ActivateNextWindow` and
`workspace::ActivatePreviousWindow` actions for cycling between windows.

Change summary

crates/workspace/src/workspace.rs | 41 +++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)

Detailed changes

crates/workspace/src/workspace.rs 🔗

@@ -125,6 +125,8 @@ actions!(
     [
         ActivateNextPane,
         ActivatePreviousPane,
+        ActivateNextWindow,
+        ActivatePreviousWindow,
         AddFolderToProject,
         ClearAllNotifications,
         CloseAllDocks,
@@ -4542,6 +4544,12 @@ impl Workspace {
             .on_action(
                 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
             )
+            .on_action(cx.listener(|workspace, _: &ActivateNextWindow, cx| {
+                workspace.activate_next_window(cx)
+            }))
+            .on_action(cx.listener(|workspace, _: &ActivatePreviousWindow, cx| {
+                workspace.activate_previous_window(cx)
+            }))
             .on_action(
                 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
                     workspace.activate_pane_in_direction(action.0, cx)
@@ -4700,6 +4708,39 @@ impl Workspace {
     pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
         self.zoomed.as_ref()
     }
+
+    pub fn activate_next_window(&mut self, cx: &mut ViewContext<Self>) {
+        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
+            return;
+        };
+        let windows = cx.windows();
+        let Some(next_window) = windows
+            .iter()
+            .cycle()
+            .skip_while(|window| window.window_id() != current_window_id)
+            .nth(1)
+        else {
+            return;
+        };
+        next_window.update(cx, |_, cx| cx.activate_window()).ok();
+    }
+
+    pub fn activate_previous_window(&mut self, cx: &mut ViewContext<Self>) {
+        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
+            return;
+        };
+        let windows = cx.windows();
+        let Some(prev_window) = windows
+            .iter()
+            .rev()
+            .cycle()
+            .skip_while(|window| window.window_id() != current_window_id)
+            .nth(1)
+        else {
+            return;
+        };
+        prev_window.update(cx, |_, cx| cx.activate_window()).ok();
+    }
 }
 
 fn leader_border_for_pane(