Add option to activate left neighbour tab on tab close (#21800)

Finn Evers created

Closes #21738

Release Notes:

- Added `left_neighbour` option to the `tabs.activate_on_close` setting
to activate the left adjacent tab on tab close.

Change summary

assets/settings/default.json |  8 ++-
crates/workspace/src/item.rs |  3 +
crates/workspace/src/pane.rs | 70 +++++++++++++++++++++++++++++++++++++
docs/src/configuring-zed.md  | 10 ++++
4 files changed, 85 insertions(+), 6 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -561,9 +561,11 @@
     // What to do after closing the current tab.
     //
     // 1. Activate the tab that was open previously (default)
-    //     "History"
-    // 2. Activate the neighbour tab (prefers the right one, if present)
-    //     "Neighbour"
+    //     "history"
+    // 2. Activate the right neighbour tab if present
+    //     "neighbour"
+    // 3. Activate the left neighbour tab if present
+    //     "left_neighbour"
     "activate_on_close": "history",
     /// Which files containing diagnostic errors/warnings to mark in the tabs.
     /// Diagnostics are only shown when file icons are also active.

crates/workspace/src/item.rs 🔗

@@ -71,11 +71,12 @@ pub enum ShowDiagnostics {
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
+#[serde(rename_all = "snake_case")]
 pub enum ActivateOnClose {
     #[default]
     History,
     Neighbour,
+    LeftNeighbour,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]

crates/workspace/src/pane.rs 🔗

@@ -1506,6 +1506,7 @@ impl Pane {
             self.pinned_tab_count -= 1;
         }
         if item_index == self.active_item_index {
+            let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
             let index_to_activate = match activate_on_close {
                 ActivateOnClose::History => self
                     .activation_history
@@ -1517,7 +1518,7 @@ impl Pane {
                     })
                     // We didn't have a valid activation history entry, so fallback
                     // to activating the item to the left
-                    .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)),
+                    .unwrap_or_else(left_neighbour_index),
                 ActivateOnClose::Neighbour => {
                     self.activation_history.pop();
                     if item_index + 1 < self.items.len() {
@@ -1526,6 +1527,10 @@ impl Pane {
                         item_index.saturating_sub(1)
                     }
                 }
+                ActivateOnClose::LeftNeighbour => {
+                    self.activation_history.pop();
+                    left_neighbour_index()
+                }
             };
 
             let should_activate = activate_pane || self.has_focus(cx);
@@ -3666,6 +3671,69 @@ mod tests {
         assert_item_labels(&pane, ["A*"], cx);
     }
 
+    #[gpui::test]
+    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
+        init_test(cx);
+        cx.update_global::<SettingsStore, ()>(|s, cx| {
+            s.update_user_settings::<ItemSettings>(cx, |s| {
+                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
+            });
+        });
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+
+        add_labeled_item(&pane, "A", false, cx);
+        add_labeled_item(&pane, "B", false, cx);
+        add_labeled_item(&pane, "C", false, cx);
+        add_labeled_item(&pane, "D", false, cx);
+        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+
+        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
+        add_labeled_item(&pane, "1", false, cx);
+        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
+
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
+
+        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
+        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+        pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
+        assert_item_labels(&pane, ["A*", "B", "C"], cx);
+
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+        assert_item_labels(&pane, ["B*", "C"], cx);
+
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+        assert_item_labels(&pane, ["C*"], cx);
+    }
+
     #[gpui::test]
     async fn test_close_inactive_items(cx: &mut TestAppContext) {
         init_test(cx);

docs/src/configuring-zed.md 🔗

@@ -691,7 +691,7 @@ List of `string` values
 }
 ```
 
-2. Activate the neighbour tab (prefers the right one, if present):
+2. Activate the right neighbour tab if present:
 
 ```json
 {
@@ -699,6 +699,14 @@ List of `string` values
 }
 ```
 
+3. Activate the left neighbour tab if present:
+
+```json
+{
+  "activate_on_close": "left_neighbour"
+}
+```
+
 ### Always show the close button
 
 - Description: Whether to always show the close button on tabs.