Make all status bar tools able to hide its button via UI (#54971)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/discussions/53471

Adds a requirement on status bar items to provide a way to hide
themselves.

<img width="329" height="153" alt="image"
src="https://github.com/user-attachments/assets/b98ee5ba-a439-44d7-9ab5-f4511b66a574"
/>

<img width="464" height="40" alt="image"
src="https://github.com/user-attachments/assets/b41d9189-3475-4e61-b3a4-bc731dd52c53"
/>


Release Notes:

- Added a way to hide sidebar buttons

Change summary

crates/activity_indicator/src/activity_indicator.rs      |  5 
crates/agent_ui/src/agent_panel.rs                       |  6 
crates/collab_ui/src/collab_panel.rs                     |  6 
crates/debugger_ui/src/debugger_panel.rs                 |  6 
crates/diagnostics/src/items.rs                          | 12 
crates/edit_prediction_ui/src/edit_prediction_button.rs  |  9 
crates/encoding_selector/src/active_buffer_encoding.rs   | 15 +
crates/git_ui/src/conflict_view.rs                       | 11 +
crates/git_ui/src/git_panel.rs                           |  6 
crates/go_to_line/src/cursor_position.rs                 | 11 +
crates/image_viewer/src/image_info.rs                    |  9 
crates/language_selector/src/active_buffer_language.rs   | 15 +
crates/language_tools/src/lsp_button.rs                  |  8 
crates/line_ending_selector/src/line_ending_indicator.rs | 15 +
crates/outline_panel/src/outline_panel.rs                |  6 
crates/project_panel/src/project_panel.rs                |  6 
crates/search/src/search_status_button.rs                | 10 
crates/terminal_view/src/terminal_panel.rs               |  6 
crates/toolchain_selector/src/active_toolchain.rs        | 12 
crates/vim/src/mode_indicator.rs                         | 11 
crates/workspace/src/active_file_name.rs                 | 12 +
crates/workspace/src/dock.rs                             | 25 ++
crates/workspace/src/status_bar.rs                       | 99 +++++++++
crates/workspace/src/workspace.rs                        |  6 
24 files changed, 292 insertions(+), 35 deletions(-)

Detailed changes

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -817,4 +817,9 @@ impl StatusItemView for ActivityIndicator {
         _: &mut Context<Self>,
     ) {
     }
+
+    fn hide_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        // Activity indicator auto-hides when there's no work to display.
+        None
+    }
 }

crates/agent_ui/src/agent_panel.rs 🔗

@@ -3039,6 +3039,12 @@ impl Panel for AgentPanel {
         true
     }
 
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.agent.get_or_insert_default().button = Some(false);
+        }))
+    }
+
     fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
         self.zoomed
     }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -3830,6 +3830,12 @@ impl Panel for CollabPanel {
     fn activation_priority(&self) -> u32 {
         5
     }
+
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.collaboration_panel.get_or_insert_default().button = Some(false);
+        }))
+    }
 }
 
 impl Focusable for CollabPanel {

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1606,6 +1606,12 @@ impl Panel for DebugPanel {
         7
     }
 
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.debugger.get_or_insert_default().button = Some(false);
+        }))
+    }
+
     fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
 
     fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {

crates/diagnostics/src/items.rs 🔗

@@ -2,15 +2,15 @@ use std::time::Duration;
 
 use editor::{Editor, MultiBufferOffset};
 use gpui::{
-    Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task,
-    WeakEntity, Window,
+    App, Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription,
+    Task, WeakEntity, Window,
 };
 use language::Diagnostic;
 use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
 use settings::Settings;
 use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
 use util::ResultExt;
-use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
+use workspace::{HideStatusItem, StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
 
 use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
 
@@ -224,4 +224,10 @@ impl StatusItemView for DiagnosticIndicator {
         }
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings.diagnostics.get_or_insert_default().button = Some(false);
+        }))
+    }
 }

crates/edit_prediction_ui/src/edit_prediction_button.rs 🔗

@@ -37,7 +37,7 @@ use ui::{
 use util::ResultExt as _;
 
 use workspace::{
-    StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
+    HideStatusItem, StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
     notifications::NotificationId,
 };
 use zed_actions::{OpenBrowser, OpenSettingsAt};
@@ -1364,6 +1364,13 @@ impl StatusItemView for EditPredictionButton {
         }
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        // This button is already gated on having a non-disabled edit
+        // prediction provider, which the user manages through provider/AI
+        // settings.
+        None
+    }
 }
 
 async fn open_disabled_globs_setting_in_editor(

crates/encoding_selector/src/active_buffer_encoding.rs 🔗

@@ -3,13 +3,13 @@ use crate::{EncodingSelector, Toggle};
 use editor::Editor;
 use encoding_rs::{Encoding, UTF_8};
 use gpui::{
-    Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window,
-    div,
+    App, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity,
+    Window, div,
 };
 use project::Project;
 use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
 use workspace::{
-    StatusBarSettings, StatusItemView, Workspace,
+    EncodingDisplayOptions, HideStatusItem, StatusBarSettings, StatusItemView, Workspace,
     item::{ItemHandle, Settings},
 };
 
@@ -131,4 +131,13 @@ impl StatusItemView for ActiveBufferEncoding {
 
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings
+                .status_bar
+                .get_or_insert_default()
+                .active_encoding_button = Some(EncodingDisplayOptions::Disabled);
+        }))
+    }
 }

crates/git_ui/src/conflict_view.rs 🔗

@@ -18,7 +18,7 @@ use settings::Settings;
 use std::{ops::Range, sync::Arc};
 use ui::{ButtonLike, Divider, Tooltip, prelude::*};
 use util::{ResultExt as _, debug_panic, maybe};
-use workspace::{StatusItemView, Workspace, item::ItemHandle};
+use workspace::{HideStatusItem, StatusItemView, Workspace, item::ItemHandle};
 use zed_actions::agent::{
     ConflictContent, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent,
 };
@@ -678,4 +678,13 @@ impl StatusItemView for MergeConflictIndicator {
         _: &mut Context<Self>,
     ) {
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings
+                .agent
+                .get_or_insert_default()
+                .show_merge_conflict_indicator = Some(false);
+        }))
+    }
 }

crates/git_ui/src/git_panel.rs 🔗

@@ -6214,6 +6214,12 @@ impl Panel for GitPanel {
     fn activation_priority(&self) -> u32 {
         3
     }
+
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.git_panel.get_or_insert_default().button = Some(false);
+        }))
+    }
 }
 
 impl PanelHeader for GitPanel {}

crates/go_to_line/src/cursor_position.rs 🔗

@@ -8,7 +8,7 @@ use ui::{
     Render, Tooltip, Window, div,
 };
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{StatusBarSettings, StatusItemView, Workspace, item::ItemHandle};
+use workspace::{HideStatusItem, StatusBarSettings, StatusItemView, Workspace, item::ItemHandle};
 
 #[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
 pub(crate) struct SelectionStats {
@@ -290,6 +290,15 @@ impl StatusItemView for CursorPosition {
 
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings
+                .status_bar
+                .get_or_insert_default()
+                .cursor_position_button = Some(false);
+        }))
+    }
 }
 
 #[derive(Clone, Copy, PartialEq, Eq, RegisterSetting)]

crates/image_viewer/src/image_info.rs 🔗

@@ -1,9 +1,9 @@
-use gpui::{Context, Entity, IntoElement, ParentElement, Render, Subscription, div};
+use gpui::{App, Context, Entity, IntoElement, ParentElement, Render, Subscription, div};
 use project::image_store::{ImageFormat, ImageMetadata};
 use settings::Settings;
 use ui::prelude::*;
 use util::size::format_file_size;
-use workspace::{ItemHandle, StatusItemView, Workspace};
+use workspace::{HideStatusItem, ItemHandle, StatusItemView, Workspace};
 
 use crate::{ImageFileSizeUnit, ImageView, ImageViewerSettings};
 
@@ -102,4 +102,9 @@ impl StatusItemView for ImageInfo {
         }
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        // The image info is only visible when an image viewer item is active.
+        None
+    }
 }

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -1,12 +1,12 @@
 use editor::Editor;
 use gpui::{
-    Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window,
-    div,
+    App, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity,
+    Window, div,
 };
 use language::LanguageName;
 use settings::Settings as _;
 use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip};
-use workspace::{StatusBarSettings, StatusItemView, Workspace, item::ItemHandle};
+use workspace::{HideStatusItem, StatusBarSettings, StatusItemView, Workspace, item::ItemHandle};
 
 use crate::{LanguageSelector, Toggle};
 
@@ -86,4 +86,13 @@ impl StatusItemView for ActiveBufferLanguage {
 
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings
+                .status_bar
+                .get_or_insert_default()
+                .active_language_button = Some(false);
+        }))
+    }
 }

crates/language_tools/src/lsp_button.rs 🔗

@@ -13,7 +13,7 @@ use language::language_settings::{EditPredictionProvider, all_language_settings}
 use client::proto;
 use collections::HashSet;
 use editor::{Editor, EditorEvent};
-use gpui::{Anchor, Entity, Subscription, Task, TaskExt, WeakEntity, actions};
+use gpui::{Anchor, App, Entity, Subscription, Task, TaskExt, WeakEntity, actions};
 use language::{BinaryStatus, BufferId, ServerHealth};
 use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
 use project::{
@@ -1248,6 +1248,12 @@ impl StatusItemView for LspButton {
             self.refresh_lsp_menu(false, window, cx);
         }
     }
+
+    fn hide_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.global_lsp_settings.get_or_insert_default().button = Some(false);
+        }))
+    }
 }
 
 impl Render for LspButton {

crates/line_ending_selector/src/line_ending_indicator.rs 🔗

@@ -1,8 +1,10 @@
 use editor::Editor;
-use gpui::{Entity, Subscription, WeakEntity};
+use gpui::{App, Entity, Subscription, WeakEntity};
 use language::LineEnding;
 use ui::{Tooltip, prelude::*};
-use workspace::{StatusBarSettings, StatusItemView, item::ItemHandle, item::Settings};
+use workspace::{
+    HideStatusItem, StatusBarSettings, StatusItemView, item::ItemHandle, item::Settings,
+};
 
 use crate::{LineEndingSelector, Toggle};
 
@@ -65,4 +67,13 @@ impl StatusItemView for LineEndingIndicator {
         }
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings
+                .status_bar
+                .get_or_insert_default()
+                .line_endings_button = Some(false);
+        }))
+    }
 }

crates/outline_panel/src/outline_panel.rs 🔗

@@ -4963,6 +4963,12 @@ impl Panel for OutlinePanel {
     fn activation_priority(&self) -> u32 {
         6
     }
+
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.outline_panel.get_or_insert_default().button = Some(false);
+        }))
+    }
 }
 
 impl Focusable for OutlinePanel {

crates/project_panel/src/project_panel.rs 🔗

@@ -7291,6 +7291,12 @@ impl Panel for ProjectPanel {
     fn activation_priority(&self) -> u32 {
         1
     }
+
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.project_panel.get_or_insert_default().button = Some(false);
+        }))
+    }
 }
 
 impl ProjectPanel {

crates/search/src/search_status_button.rs 🔗

@@ -1,8 +1,8 @@
 use editor::EditorSettings;
-use gpui::FocusHandle;
+use gpui::{App, FocusHandle};
 use settings::Settings as _;
 use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*};
-use workspace::{ItemHandle, StatusItemView};
+use workspace::{HideStatusItem, ItemHandle, StatusItemView};
 
 pub const SEARCH_ICON: IconName = IconName::MagnifyingGlass;
 
@@ -62,4 +62,10 @@ impl StatusItemView for SearchButton {
     ) {
         self.pane_item_focus_handle = active_pane_item.map(|item| item.item_focus_handle(cx));
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings.editor.search.get_or_insert_default().button = Some(false);
+        }))
+    }
 }

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1666,6 +1666,12 @@ impl Panel for TerminalPanel {
     fn activation_priority(&self) -> u32 {
         2
     }
+
+    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
+        Some(workspace::HideStatusItem::new(|settings| {
+            settings.terminal.get_or_insert_default().button = Some(false);
+        }))
+    }
 }
 
 struct TerminalProvider(Entity<TerminalPanel>);

crates/toolchain_selector/src/active_toolchain.rs 🔗

@@ -2,14 +2,14 @@ use std::sync::Arc;
 
 use editor::Editor;
 use gpui::{
-    AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription,
-    Task, WeakEntity, Window, div,
+    App, AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Styled,
+    Subscription, Task, WeakEntity, Window, div,
 };
 use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope};
 use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent};
 use ui::{Button, ButtonCommon, Clickable, LabelSize, SharedString, Tooltip};
 use util::{maybe, rel_path::RelPath};
-use workspace::{StatusItemView, Workspace, item::ItemHandle};
+use workspace::{HideStatusItem, StatusItemView, Workspace, item::ItemHandle};
 
 use crate::ToolchainSelector;
 
@@ -264,4 +264,10 @@ impl StatusItemView for ActiveToolchain {
         }
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        // The toolchain selector only appears when the active buffer has a
+        // language with toolchain support.
+        None
+    }
 }

crates/vim/src/mode_indicator.rs 🔗

@@ -1,6 +1,8 @@
-use gpui::{Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div};
+use gpui::{
+    App, Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div,
+};
 use ui::text_for_keystrokes;
-use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*};
+use workspace::{HideStatusItem, StatusItemView, item::ItemHandle, ui::prelude::*};
 
 use crate::{Vim, VimEvent, VimGlobals};
 
@@ -186,4 +188,9 @@ impl StatusItemView for ModeIndicator {
         _cx: &mut Context<Self>,
     ) {
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        // The Vim mode indicator is only visible while Vim mode is on.
+        None
+    }
 }

crates/workspace/src/active_file_name.rs 🔗

@@ -1,11 +1,13 @@
 use gpui::{
-    Context, Empty, EventEmitter, IntoElement, ParentElement, Render, SharedString, Window,
+    App, Context, Empty, EventEmitter, IntoElement, ParentElement, Render, SharedString, Window,
 };
 use settings::Settings;
 use ui::{Button, Tooltip, prelude::*};
 use util::paths::PathStyle;
 
-use crate::{StatusItemView, item::ItemHandle, workspace_settings::StatusBarSettings};
+use crate::{
+    HideStatusItem, StatusItemView, item::ItemHandle, workspace_settings::StatusBarSettings,
+};
 
 pub struct ActiveFileName {
     project_path: Option<SharedString>,
@@ -66,4 +68,10 @@ impl StatusItemView for ActiveFileName {
         }
         cx.notify();
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        Some(HideStatusItem::new(|settings| {
+            settings.status_bar.get_or_insert_default().show_active_file = Some(false);
+        }))
+    }
 }

crates/workspace/src/dock.rs 🔗

@@ -1,5 +1,6 @@
 use crate::focus_follows_mouse::FocusFollowsMouse as _;
 use crate::persistence::model::DockData;
+use crate::status_bar::HideStatusItem;
 use crate::{DraggedDock, Event, FocusFollowsMouse, ModalLayer, Pane, WorkspaceSettings};
 use crate::{Workspace, status_bar::StatusItemView};
 use anyhow::Context as _;
@@ -86,6 +87,12 @@ pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
     fn is_agent_panel(&self) -> bool {
         false
     }
+    /// Returns metadata describing how to hide this panel's button from the
+    /// status bar by writing to user settings. Implementors should return
+    /// `None` if the panel button cannot be hidden through settings.
+    fn hide_button_setting(&self, _: &App) -> Option<HideStatusItem> {
+        None
+    }
 }
 
 pub trait PanelHandle: Send + Sync {
@@ -116,6 +123,7 @@ pub trait PanelHandle: Send + Sync {
     fn activation_priority(&self, cx: &App) -> u32;
     fn enabled(&self, cx: &App) -> bool;
     fn is_agent_panel(&self, cx: &App) -> bool;
+    fn hide_button_setting(&self, cx: &App) -> Option<HideStatusItem>;
     fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
         let current_position = self.position(window, cx);
         let next_position = [
@@ -244,6 +252,10 @@ where
     fn is_agent_panel(&self, cx: &App) -> bool {
         self.read(cx).is_agent_panel()
     }
+
+    fn hide_button_setting(&self, cx: &App) -> Option<HideStatusItem> {
+        self.read(cx).hide_button_setting(cx)
+    }
 }
 
 impl From<&dyn PanelHandle> for AnyView {
@@ -1251,6 +1263,7 @@ impl Render for PanelButtons {
                                 DockPosition::Bottom,
                             ];
 
+                            let panel_hide = panel.hide_button_setting(cx);
                             ContextMenu::build(window, cx, |mut menu, _, cx| {
                                 let mut has_position_entries = false;
                                 for position in POSITIONS {
@@ -1322,6 +1335,12 @@ impl Render for PanelButtons {
                                         },
                                     );
                                 }
+                                if let Some(hide) = panel_hide {
+                                    menu = crate::status_bar::add_hide_button_entry(
+                                        menu.separator(),
+                                        hide,
+                                    );
+                                }
                                 menu
                             })
                         })
@@ -1388,6 +1407,12 @@ impl StatusItemView for PanelButtons {
     ) {
         // Nothing to do, panel buttons don't depend on the active center item
     }
+
+    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
+        // Panel buttons are hidden on a per-panel basis through each panel
+        // button's own context menu.
+        None
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/workspace/src/status_bar.rs 🔗

@@ -3,12 +3,41 @@ use crate::{
     sidebar_side_context_menu,
 };
 use gpui::{
-    Anchor, AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled,
-    Subscription, WeakEntity, Window,
+    Anchor, AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render,
+    SharedString, Styled, Subscription, WeakEntity, Window,
 };
-use std::any::TypeId;
+use settings::{SettingsContent, update_settings_file};
+use std::{any::TypeId, sync::Arc};
 use theme::CLIENT_SIDE_DECORATION_ROUNDING;
-use ui::{Divider, Indicator, Tooltip, prelude::*};
+use ui::{ContextMenu, Divider, IconPosition, Indicator, Tooltip, prelude::*, right_click_menu};
+
+/// Describes how a status-bar item can be hidden by the user.
+///
+/// Every [`StatusItemView`] must either provide this (so that the user gets a
+/// "Hide Button" entry in the right-click menu) or explicitly return `None`
+/// to opt out. Returning `None` should be reserved for items that are
+/// already conditional on some other setting exposed elsewhere (e.g., the
+/// activity indicator, which disappears on its own once there's no work to
+/// display).
+#[derive(Clone)]
+pub struct HideStatusItem {
+    hide: Arc<dyn Fn(&mut SettingsContent) + Send + Sync>,
+}
+
+impl HideStatusItem {
+    pub fn new(hide: impl Fn(&mut SettingsContent) + Send + Sync + 'static) -> Self {
+        Self {
+            hide: Arc::new(hide),
+        }
+    }
+
+    /// Persists the hide by updating the user settings file.
+    pub fn apply(&self, cx: &App) {
+        let hide = self.hide.clone();
+        let fs = <dyn fs::Fs>::global(cx);
+        update_settings_file(fs, cx, move |settings, _cx| (hide)(settings));
+    }
+}
 
 pub trait StatusItemView: Render {
     /// Event callback that is triggered when the active pane item changes.
@@ -18,6 +47,15 @@ pub trait StatusItemView: Render {
         window: &mut Window,
         cx: &mut Context<Self>,
     );
+
+    /// Returns metadata describing how this item can be hidden from the
+    /// status bar by writing to the user settings file.
+    ///
+    /// Implementors that return `None` must be inherently conditional on
+    /// another user-exposed setting; otherwise, they should return `Some` so
+    /// that the status bar can show a "Hide Button" entry in its
+    /// right-click menu.
+    fn hide_setting(&self, cx: &App) -> Option<HideStatusItem>;
 }
 
 trait StatusItemViewHandle: Send {
@@ -29,6 +67,7 @@ trait StatusItemViewHandle: Send {
         cx: &mut App,
     );
     fn item_type(&self) -> TypeId;
+    fn hide_setting(&self, cx: &App) -> Option<HideStatusItem>;
 }
 
 #[derive(Default)]
@@ -124,7 +163,9 @@ impl StatusBar {
                 sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Left,
                 |this| this.child(self.render_sidebar_toggle(sidebar, cx)),
             )
-            .children(self.left_items.iter().map(|item| item.to_any()))
+            .children(self.left_items.iter().enumerate().map(|(index, item)| {
+                render_hideable_item("status-bar-left", index, item.as_ref(), cx)
+            }))
     }
 
     fn render_right_tools(
@@ -136,7 +177,15 @@ impl StatusBar {
             .flex_shrink_0()
             .gap_1()
             .overflow_x_hidden()
-            .children(self.right_items.iter().rev().map(|item| item.to_any()))
+            .children(
+                self.right_items
+                    .iter()
+                    .enumerate()
+                    .rev()
+                    .map(|(index, item)| {
+                        render_hideable_item("status-bar-right", index, item.as_ref(), cx)
+                    }),
+            )
             .when(
                 sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Right,
                 |this| this.child(self.render_sidebar_toggle(sidebar, cx)),
@@ -201,6 +250,40 @@ impl StatusBar {
     }
 }
 
+fn render_hideable_item(
+    side: &'static str,
+    index: usize,
+    item: &dyn StatusItemViewHandle,
+    cx: &App,
+) -> impl IntoElement {
+    let view = item.to_any();
+    let Some(hide) = item.hide_setting(cx) else {
+        return view.into_any_element();
+    };
+
+    let menu_id: SharedString = format!("{side}-item-menu-{index}").into();
+    right_click_menu(menu_id)
+        .trigger(move |_is_active, _window, _cx| view)
+        .menu(move |window, cx| {
+            let hide = hide.clone();
+            ContextMenu::build(window, cx, move |menu, _window, _cx| {
+                add_hide_button_entry(menu, hide)
+            })
+        })
+        .into_any_element()
+}
+
+/// Appends a "Hide Button" entry aligned with surrounding toggleable entries.
+pub fn add_hide_button_entry(menu: ContextMenu, hide: HideStatusItem) -> ContextMenu {
+    menu.toggleable_entry(
+        "Hide Button",
+        false,
+        IconPosition::Start,
+        None,
+        move |_window, cx| hide.apply(cx),
+    )
+}
+
 impl StatusBar {
     pub fn new(
         active_pane: &Entity<Pane>,
@@ -350,6 +433,10 @@ impl<T: StatusItemView> StatusItemViewHandle for Entity<T> {
     fn item_type(&self) -> TypeId {
         TypeId::of::<T>()
     }
+
+    fn hide_setting(&self, cx: &App) -> Option<HideStatusItem> {
+        self.read(cx).hide_setting(cx)
+    }
 }
 
 impl From<&dyn StatusItemViewHandle> for AnyView {

crates/workspace/src/workspace.rs 🔗

@@ -118,7 +118,7 @@ use sqlez::{
     statement::Statement,
 };
 use status_bar::StatusBar;
-pub use status_bar::StatusItemView;
+pub use status_bar::{HideStatusItem, StatusItemView, add_hide_button_entry};
 use std::{
     any::TypeId,
     borrow::Cow,
@@ -152,8 +152,8 @@ use util::{
 };
 use uuid::Uuid;
 pub use workspace_settings::{
-    AutosaveSetting, BottomDockLayout, FocusFollowsMouse, RestoreOnStartupBehavior,
-    StatusBarSettings, TabBarSettings, WorkspaceSettings,
+    AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, FocusFollowsMouse,
+    RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings, WorkspaceSettings,
 };
 use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};