Add an action listener to workspace for ActivatePreviousItem and ActivateNextItem (#42588)

Josh Piasecki created

Release Notes:

- pane::ActivatePreviousItem and pane::ActivateNextItem now toggle the
most recent pane when called from a dock panel


a couple months ago i posted a work around that used `SendKeystrokes` to
cycle through pane items when focused on a dock.
#35253

this pr would add this functionality to the these actions by default.
i implemented this by adding an action listener to the workspace level.

------
if the current context is a dock that does not hold a pane
it retrieves the most recent pane from `activation_history` and
activates the next item on that pane instead.

- `"Pane > Editor"`
cycles through the current pane like normal
- `"Dock > Pane > Terminal"`
also cycles through the pane items like normal
- `"Dock > (Any Child that is not a child of Pane)"`
cycles through the items of the most recent pane.

this is the standard behavior in VS Code i believe.

in the video below you can see the actions cycling through the editor
like normal when focus is on the editor.
then you can see the editor continue to cycle when the focus is on the
project panel.
and that the focus stays on the project panel.
and you can see the action cycle the terminal items when the focus is
moved to the terminal


https://github.com/user-attachments/assets/999ab740-d2fa-4d00-9e53-f7605217e6ac

the only thing i noticed is that for this to work the keybindings must
be set above `Pane`
so they have to be set globally or on workspace. otherwise they do not
match in the context

Change summary

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

Detailed changes

crates/workspace/src/workspace.rs 🔗

@@ -5967,6 +5967,88 @@ impl Workspace {
                 },
             ))
             .on_action(cx.listener(Workspace::toggle_centered_layout))
+            .on_action(cx.listener(
+                |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
+                    if let Some(active_dock) = workspace.active_dock(window, cx) {
+                        let dock = active_dock.read(cx);
+                        if let Some(active_panel) = dock.active_panel() {
+                            if active_panel.pane(cx).is_none() {
+                                let mut recent_pane: Option<Entity<Pane>> = None;
+                                let mut recent_timestamp = 0;
+                                for pane_handle in workspace.panes() {
+                                    let pane = pane_handle.read(cx);
+                                    for entry in pane.activation_history() {
+                                        if entry.timestamp > recent_timestamp {
+                                            recent_timestamp = entry.timestamp;
+                                            recent_pane = Some(pane_handle.clone());
+                                        }
+                                    }
+                                }
+
+                                if let Some(pane) = recent_pane {
+                                    pane.update(cx, |pane, cx| {
+                                        let current_index = pane.active_item_index();
+                                        let items_len = pane.items_len();
+                                        if items_len > 0 {
+                                            let next_index = if current_index + 1 < items_len {
+                                                current_index + 1
+                                            } else {
+                                                0
+                                            };
+                                            pane.activate_item(
+                                                next_index, false, false, window, cx,
+                                            );
+                                        }
+                                    });
+                                    return;
+                                }
+                            }
+                        }
+                    }
+                    cx.propagate();
+                },
+            ))
+            .on_action(cx.listener(
+                |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
+                    if let Some(active_dock) = workspace.active_dock(window, cx) {
+                        let dock = active_dock.read(cx);
+                        if let Some(active_panel) = dock.active_panel() {
+                            if active_panel.pane(cx).is_none() {
+                                let mut recent_pane: Option<Entity<Pane>> = None;
+                                let mut recent_timestamp = 0;
+                                for pane_handle in workspace.panes() {
+                                    let pane = pane_handle.read(cx);
+                                    for entry in pane.activation_history() {
+                                        if entry.timestamp > recent_timestamp {
+                                            recent_timestamp = entry.timestamp;
+                                            recent_pane = Some(pane_handle.clone());
+                                        }
+                                    }
+                                }
+
+                                if let Some(pane) = recent_pane {
+                                    pane.update(cx, |pane, cx| {
+                                        let current_index = pane.active_item_index();
+                                        let items_len = pane.items_len();
+                                        if items_len > 0 {
+                                            let prev_index = if current_index > 0 {
+                                                current_index - 1
+                                            } else {
+                                                items_len.saturating_sub(1)
+                                            };
+                                            pane.activate_item(
+                                                prev_index, false, false, window, cx,
+                                            );
+                                        }
+                                    });
+                                    return;
+                                }
+                            }
+                        }
+                    }
+                    cx.propagate();
+                },
+            ))
             .on_action(cx.listener(Workspace::cancel))
     }