tab: Add setting to hide the close button entirely (#23880)

Morgan Metz , Peter Tripp , Danilo Leal , and smit created

Closes #23744

Release Notes:

- Changed the `always_show_close_button` key to `show_close_button` and
introduced a new `hidden` value, that allows never displaying the close
button.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: smit <0xtimsb@gmail.com>

Change summary

assets/settings/default.json    | 12 ++++
crates/migrator/src/migrator.rs | 85 ++++++++++++++++++++++++++++++++--
crates/workspace/src/item.rs    | 13 ++++
crates/workspace/src/pane.rs    | 31 +++++++-----
docs/src/configuring-zed.md     | 36 ++++++++++++--
5 files changed, 149 insertions(+), 28 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -648,11 +648,19 @@
     // Show git status colors in the editor tabs.
     "git_status": false,
     // Position of the close button on the editor tabs.
+    // One of: ["right", "left", "hidden"]
     "close_position": "right",
     // Whether to show the file icon for a tab.
     "file_icons": false,
-    // Whether to always show the close button on tabs.
-    "always_show_close_button": false,
+    // Controls the appearance behavior of the tab's close button.
+    //
+    // 1. Show it just upon hovering the tab. (default)
+    //     "hover"
+    // 2. Show it persistently.
+    //     "always"
+    // 3. Never show it, even if hovering it.
+    //     "hidden"
+    "show_close_button": "hover",
     // What to do after closing the current tab.
     //
     // 1. Activate the tab that was open previously (default)

crates/migrator/src/migrator.rs 🔗

@@ -72,7 +72,7 @@ pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<St
     migrate(
         &text,
         &[(
-            SETTINGS_REPLACE_NESTED_KEY,
+            SETTINGS_NESTED_KEY_VALUE_PATTERN,
             replace_edit_prediction_provider_setting,
         )],
         &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
@@ -571,9 +571,17 @@ pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> =
 const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[
     (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name),
     (
-        SETTINGS_REPLACE_NESTED_KEY,
+        SETTINGS_NESTED_KEY_VALUE_PATTERN,
         replace_edit_prediction_provider_setting,
     ),
+    (
+        SETTINGS_NESTED_KEY_VALUE_PATTERN,
+        replace_tab_close_button_setting_key,
+    ),
+    (
+        SETTINGS_NESTED_KEY_VALUE_PATTERN,
+        replace_tab_close_button_setting_value,
+    ),
     (
         SETTINGS_REPLACE_IN_LANGUAGES_QUERY,
         replace_setting_in_languages,
@@ -594,7 +602,7 @@ static SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
     Query::new(
         &tree_sitter_json::LANGUAGE.into(),
-        SETTINGS_REPLACE_NESTED_KEY,
+        SETTINGS_NESTED_KEY_VALUE_PATTERN,
     )
     .unwrap()
 });
@@ -639,14 +647,14 @@ pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>
         ])
     });
 
-const SETTINGS_REPLACE_NESTED_KEY: &str = r#"
+const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"
 (object
   (pair
     key: (string (string_content) @parent_key)
     value: (object
         (pair
             key: (string (string_content) @setting_name)
-            value: (_) @value
+            value: (_) @setting_value
         )
     )
   )
@@ -679,6 +687,73 @@ fn replace_edit_prediction_provider_setting(
     None
 }
 
+fn replace_tab_close_button_setting_key(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
+    let parent_object_range = mat
+        .nodes_for_capture_index(parent_object_capture_ix)
+        .next()?
+        .byte_range();
+    let parent_object_name = contents.get(parent_object_range.clone())?;
+
+    let setting_name_ix = query.capture_index_for_name("setting_name")?;
+    let setting_range = mat
+        .nodes_for_capture_index(setting_name_ix)
+        .next()?
+        .byte_range();
+    let setting_name = contents.get(setting_range.clone())?;
+
+    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
+        return Some((setting_range, "show_close_button".into()));
+    }
+
+    None
+}
+
+fn replace_tab_close_button_setting_value(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
+    let parent_object_range = mat
+        .nodes_for_capture_index(parent_object_capture_ix)
+        .next()?
+        .byte_range();
+    let parent_object_name = contents.get(parent_object_range.clone())?;
+
+    let setting_name_ix = query.capture_index_for_name("setting_name")?;
+    let setting_name_range = mat
+        .nodes_for_capture_index(setting_name_ix)
+        .next()?
+        .byte_range();
+    let setting_name = contents.get(setting_name_range.clone())?;
+
+    let setting_value_ix = query.capture_index_for_name("setting_value")?;
+    let setting_value_range = mat
+        .nodes_for_capture_index(setting_value_ix)
+        .next()?
+        .byte_range();
+    let setting_value = contents.get(setting_value_range.clone())?;
+
+    if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
+        match setting_value {
+            "true" => {
+                return Some((setting_value_range, "\"always\"".to_string()));
+            }
+            "false" => {
+                return Some((setting_value_range, "\"hover\"".to_string()));
+            }
+            _ => {}
+        }
+    }
+
+    None
+}
+
 const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#"
 (object
   (pair

crates/workspace/src/item.rs 🔗

@@ -42,7 +42,7 @@ pub struct ItemSettings {
     pub activate_on_close: ActivateOnClose,
     pub file_icons: bool,
     pub show_diagnostics: ShowDiagnostics,
-    pub always_show_close_button: bool,
+    pub show_close_button: ShowCloseButton,
 }
 
 #[derive(Deserialize)]
@@ -60,6 +60,15 @@ pub enum ClosePosition {
     Right,
 }
 
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ShowCloseButton {
+    Always,
+    #[default]
+    Hover,
+    Hidden,
+}
+
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum ShowDiagnostics {
@@ -104,7 +113,7 @@ pub struct ItemSettingsContent {
     /// Whether to always show the close button on tabs.
     ///
     /// Default: false
-    always_show_close_button: Option<bool>,
+    show_close_button: Option<ShowCloseButton>,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]

crates/workspace/src/pane.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     item::{
         ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
-        ShowDiagnostics, TabContentParams, TabTooltipContent, WeakItemHandle,
+        ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent, WeakItemHandle,
     },
     move_item,
     notifications::NotifyResultExt,
@@ -2269,7 +2269,7 @@ impl Pane {
 
         let settings = ItemSettings::get_global(cx);
         let close_side = &settings.close_position;
-        let always_show_close_button = settings.always_show_close_button;
+        let show_close_button = &settings.show_close_button;
         let indicator = render_item_indicator(item.boxed_clone(), cx);
         let item_id = item.item_id();
         let is_first_item = ix == 0;
@@ -2373,18 +2373,21 @@ impl Pane {
                         close_pinned: false,
                     };
                     end_slot_tooltip_text = "Close Tab";
-                    IconButton::new("close tab", IconName::Close)
-                        .when(!always_show_close_button, |button| {
-                            button.visible_on_hover("")
-                        })
-                        .shape(IconButtonShape::Square)
-                        .icon_color(Color::Muted)
-                        .size(ButtonSize::None)
-                        .icon_size(IconSize::XSmall)
-                        .on_click(cx.listener(move |pane, _, window, cx| {
-                            pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
-                                .detach_and_log_err(cx);
-                        }))
+                    match show_close_button {
+                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
+                        ShowCloseButton::Hover => {
+                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
+                        }
+                        ShowCloseButton::Hidden => return this,
+                    }
+                    .shape(IconButtonShape::Square)
+                    .icon_color(Color::Muted)
+                    .size(ButtonSize::None)
+                    .icon_size(IconSize::XSmall)
+                    .on_click(cx.listener(move |pane, _, window, cx| {
+                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
+                            .detach_and_log_err(cx);
+                    }))
                 }
                 .map(|this| {
                     if is_active {

docs/src/configuring-zed.md 🔗

@@ -784,7 +784,7 @@ List of `string` values
   "file_icons": false,
   "git_status": false,
   "activate_on_close": "history",
-  "always_show_close_button": false
+  "show_close_button": "hover"
 },
 ```
 
@@ -856,11 +856,37 @@ List of `string` values
 }
 ```
 
-### Always show the close button
+### Show close button
 
-- Description: Whether to always show the close button on tabs.
-- Setting: `always_show_close_button`
-- Default: `false`
+- Description: Controls the appearance behavior of the tab's close button.
+- Setting: `show_close_button`
+- Default: `hover`
+
+**Options**
+
+1.  Show it just upon hovering the tab:
+
+```json
+{
+  "show_close_button": "hover"
+}
+```
+
+2. Show it persistently:
+
+```json
+{
+  "show_close_button": "always"
+}
+```
+
+3. Never show it, even if hovering it:
+
+```json
+{
+  "show_close_button": "hidden"
+}
+```
 
 ## Editor Toolbar