From 6b90aaa1bd055ad08f90aed49bf515f3756e88ee Mon Sep 17 00:00:00 2001 From: Ichimura Tomoo Date: Wed, 7 Jan 2026 16:24:46 +0900 Subject: [PATCH] status_bar: Add encoding indicator (#45476) ## Context / Related PRs This PR is the third part of the encoding support improvements, following: - #44819: Introduced initial legacy encoding support (Shift-JIS, etc.). - #45243: Fixed UTF-16 saving behavior and improved binary detection. ## Summary This PR implements a status bar item that displays the character encoding of the active buffer (e.g., `UTF-8`, `Shift_JIS`). It provides visibility into the file's encoding and indicates the presence of a Byte Order Mark (BOM). ## Features - **Encoding Indicator**: Displays the encoding name in the status bar. - **BOM Support**: Appends `(BOM)` to the encoding name if a BOM is detected (e.g., `UTF-8 (BOM)`). - **Configuration**: The active_encoding_button setting in status_bar accepts "enabled", "disabled", or "non_utf8". The default is "non_utf8", which displays the indicator for all encodings except standard UTF-8 (without BOM). - **Settings UI**: Provides a dropdown menu in the Settings UI to control this behavior. - **Documentation**: Updated `configuring-zed.md` and `visual-customization.md`. ## Implementation Details - Created `ActiveBufferEncoding` component in `crates/encoding_selector`. - The click handler for the button is currently a **no-op**. Implementing the functionality to reopen files with a specific encoding has potential implications for real-time collaboration (e.g., syncing buffer interpretation across peers). Therefore, this PR focuses strictly on the visualization and configuration aspects to keep the scope simple and focused. - Updated schema and default settings to include `active_encoding_button`. ## Screenshots image image ## Configuration To hide the button, add the following to `settings.json`: ```json "status_bar": { "active_encoding_button": "disabled" } ``` - **enabled**: Always show the encoding. - **disabled**: Never show the encoding. - **non_utf8**: Shows for non-UTF-8 encodings and UTF-8 with BOM. Only hides for standard UTF-8 (Default). image ## Heuristic Limitations: The underlying detection logic (implemented in #44819 and #45243) prioritizes UTF-8 opening performance and does not guarantee perfect detection for all encodings. We consider this margin of error acceptable, similar to the behavior seen in VS Code. A future "Reopen with Encoding" feature would serve as the primary fallback for any misdetections. Release Notes: - Added a status bar item to display the active file's character encoding (e.g. `UTF-16`). This shows for non-utf8 files by default and can be configured with `{"status_bar":{"active_encoding_button":"disabled|enabled|non_utf8"}}` --- Cargo.lock | 12 +++ Cargo.toml | 2 + assets/settings/default.json | 2 + crates/encoding_selector/Cargo.toml | 20 ++++ crates/encoding_selector/LICENSE-GPL | 1 + .../src/active_buffer_encoding.rs | 91 +++++++++++++++++++ .../src/encoding_selector.rs | 4 + .../src/settings_content/workspace.rs | 38 ++++++++ crates/settings/src/vscode_import.rs | 1 + crates/settings_ui/src/page_data.rs | 22 +++++ crates/settings_ui/src/settings_ui.rs | 1 + crates/workspace/src/workspace_settings.rs | 7 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 3 + docs/src/configuring-zed.md | 3 +- docs/src/visual-customization.md | 6 +- 16 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 crates/encoding_selector/Cargo.toml create mode 120000 crates/encoding_selector/LICENSE-GPL create mode 100644 crates/encoding_selector/src/active_buffer_encoding.rs create mode 100644 crates/encoding_selector/src/encoding_selector.rs diff --git a/Cargo.lock b/Cargo.lock index 1bdc8aa373..1b4adcdf45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5583,6 +5583,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encoding_selector" +version = "0.1.0" +dependencies = [ + "editor", + "encoding_rs", + "gpui", + "ui", + "workspace", +] + [[package]] name = "endi" version = "1.1.0" @@ -20710,6 +20721,7 @@ dependencies = [ "edit_prediction", "edit_prediction_ui", "editor", + "encoding_selector", "env_logger 0.11.8", "extension", "extension_host", diff --git a/Cargo.toml b/Cargo.toml index 69d02faeb1..a502232485 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "crates/edit_prediction_ui", "crates/edit_prediction_context", "crates/editor", + "crates/encoding_selector", "crates/eval", "crates/eval_utils", "crates/explorer_command_injector", @@ -292,6 +293,7 @@ deepseek = { path = "crates/deepseek" } derive_refineable = { path = "crates/refineable/derive_refineable" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } +encoding_selector = { path = "crates/encoding_selector" } eval_utils = { path = "crates/eval_utils" } extension = { path = "crates/extension" } extension_host = { path = "crates/extension_host" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 25e0b1a6db..26789e5c3e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1472,6 +1472,8 @@ "cursor_position_button": true, // Whether to show active line endings button in the status bar. "line_endings_button": false, + // Control when to show the active encoding in the status bar. + "active_encoding_button": "non_utf8", }, // Settings specific to the terminal "terminal": { diff --git a/crates/encoding_selector/Cargo.toml b/crates/encoding_selector/Cargo.toml new file mode 100644 index 0000000000..abafc845eb --- /dev/null +++ b/crates/encoding_selector/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "encoding_selector" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/encoding_selector.rs" +doctest = false + +[dependencies] +editor.workspace = true +encoding_rs.workspace = true +gpui.workspace = true +ui.workspace = true +workspace.workspace = true diff --git a/crates/encoding_selector/LICENSE-GPL b/crates/encoding_selector/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/encoding_selector/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/encoding_selector/src/active_buffer_encoding.rs b/crates/encoding_selector/src/active_buffer_encoding.rs new file mode 100644 index 0000000000..a4c8eb6548 --- /dev/null +++ b/crates/encoding_selector/src/active_buffer_encoding.rs @@ -0,0 +1,91 @@ +use editor::Editor; +use encoding_rs::{Encoding, UTF_8}; +use gpui::{ + Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window, div, +}; +use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip}; +use workspace::{ + StatusBarSettings, StatusItemView, Workspace, + item::{ItemHandle, Settings}, +}; + +pub struct ActiveBufferEncoding { + active_encoding: Option<&'static Encoding>, + //workspace: WeakEntity, + _observe_active_editor: Option, + has_bom: bool, +} + +impl ActiveBufferEncoding { + pub fn new(_workspace: &Workspace) -> Self { + Self { + active_encoding: None, + //workspace: workspace.weak_handle(), + _observe_active_editor: None, + has_bom: false, + } + } + + fn update_encoding(&mut self, editor: Entity, _: &mut Window, cx: &mut Context) { + self.active_encoding = None; + + let editor = editor.read(cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) { + let buffer = buffer.read(cx); + + self.active_encoding = Some(buffer.encoding()); + self.has_bom = buffer.has_bom(); + } + + cx.notify(); + } +} + +impl Render for ActiveBufferEncoding { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(active_encoding) = self.active_encoding else { + return div().hidden(); + }; + + let display_option = StatusBarSettings::get_global(cx).active_encoding_button; + let is_utf8 = active_encoding == UTF_8; + if !display_option.should_show(is_utf8, self.has_bom) { + return div().hidden(); + } + + let mut text = active_encoding.name().to_string(); + if self.has_bom { + text.push_str(" (BOM)"); + } + + div().child( + Button::new("change-encoding", text) + .label_size(LabelSize::Small) + .on_click(|_, _, _cx| { + // No-op + }) + .tooltip(Tooltip::text("Current Encoding")), + ) + } +} + +impl StatusItemView for ActiveBufferEncoding { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { + self._observe_active_editor = + Some(cx.observe_in(&editor, window, Self::update_encoding)); + self.update_encoding(editor, window, cx); + } else { + self.active_encoding = None; + self.has_bom = false; + self._observe_active_editor = None; + } + + cx.notify(); + } +} diff --git a/crates/encoding_selector/src/encoding_selector.rs b/crates/encoding_selector/src/encoding_selector.rs new file mode 100644 index 0000000000..bf4f59bbaa --- /dev/null +++ b/crates/encoding_selector/src/encoding_selector.rs @@ -0,0 +1,4 @@ +mod active_buffer_encoding; +pub use active_buffer_encoding::ActiveBufferEncoding; + +pub fn init() {} diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 73c94561e0..8f793c21d4 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -435,6 +435,44 @@ pub struct StatusBarSettingsContent { /// /// Default: false pub line_endings_button: Option, + /// Whether to show the active encoding button in the status bar. + /// + /// Default: non_utf8 + pub active_encoding_button: Option, +} + +#[derive( + Copy, + Clone, + Debug, + Eq, + PartialEq, + Default, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + strum::VariantNames, + strum::VariantArray, +)] +#[serde(rename_all = "snake_case")] +pub enum EncodingDisplayOptions { + Enabled, + Disabled, + #[default] + NonUtf8, +} +impl EncodingDisplayOptions { + pub fn should_show(&self, is_utf8: bool, has_bom: bool) -> bool { + match self { + Self::Disabled => false, + Self::Enabled => true, + Self::NonUtf8 => { + let is_standard_utf8 = is_utf8 && !has_bom; + !is_standard_utf8 + } + } + } } #[derive( diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 4e7f9abaf1..d04f9cafbe 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -655,6 +655,7 @@ impl VsCodeSettings { active_language_button: None, cursor_position_button: None, line_endings_button: None, + active_encoding_button: None, }) } diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 7b451b0769..7d234d0535 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -2835,6 +2835,28 @@ pub(crate) fn settings_data(cx: &App) -> Vec { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Active Encoding Button", + description: "Control when to show the active encoding in the status bar.", + field: Box::new(SettingField { + json_path: Some("status_bar.active_encoding_button"), + pick: |settings_content| { + settings_content + .status_bar + .as_ref()? + .active_encoding_button + .as_ref() + }, + write: |settings_content, value| { + settings_content + .status_bar + .get_or_insert_default() + .active_encoding_button = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Cursor Position Button", description: "Show the cursor position button in the status bar.", diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 29900a0ad1..4144adbd29 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -498,6 +498,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index ed915e4a96..75969e4534 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -4,8 +4,9 @@ use crate::DockPosition; use collections::HashMap; use serde::Deserialize; pub use settings::{ - AutosaveSetting, BottomDockLayout, InactiveOpacity, PaneSplitDirectionHorizontal, - PaneSplitDirectionVertical, RegisterSetting, RestoreOnStartupBehavior, Settings, + AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, InactiveOpacity, + PaneSplitDirectionHorizontal, PaneSplitDirectionVertical, RegisterSetting, + RestoreOnStartupBehavior, Settings, }; #[derive(RegisterSetting)] @@ -130,6 +131,7 @@ pub struct StatusBarSettings { pub active_language_button: bool, pub cursor_position_button: bool, pub line_endings_button: bool, + pub active_encoding_button: EncodingDisplayOptions, } impl Settings for StatusBarSettings { @@ -140,6 +142,7 @@ impl Settings for StatusBarSettings { active_language_button: status_bar.active_language_button.unwrap(), cursor_position_button: status_bar.cursor_position_button.unwrap(), line_endings_button: status_bar.line_endings_button.unwrap(), + active_encoding_button: status_bar.active_encoding_button.unwrap(), } } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4b67eeeb92..ed7a9b5378 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -92,6 +92,7 @@ debugger_tools.workspace = true debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true +encoding_selector.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cd4adf92be..85d144f771 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -433,6 +433,8 @@ pub fn initialize_workspace( window, cx, ); + let active_buffer_encoding = + cx.new(|_| encoding_selector::ActiveBufferEncoding::new(workspace)); let active_buffer_language = cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace)); let active_toolchain_language = @@ -459,6 +461,7 @@ pub fn initialize_workspace( status_bar.add_left_item(diagnostic_summary, window, cx); status_bar.add_left_item(activity_indicator, window, cx); status_bar.add_right_item(edit_prediction_ui, window, cx); + status_bar.add_right_item(active_buffer_encoding, window, cx); status_bar.add_right_item(active_buffer_language, window, cx); status_bar.add_right_item(active_toolchain_language, window, cx); status_bar.add_right_item(line_ending_indicator, window, cx); diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 57762a1d9d..62de731fd9 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1615,7 +1615,8 @@ Positive `integer` value between 1 and 32. Values outside of this range will be "status_bar": { "active_language_button": true, "cursor_position_button": true, - "line_endings_button": false + "line_endings_button": false, + "active_encoding_button": "non_utf8" }, ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 234776b1d3..cbd39637d8 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -332,7 +332,11 @@ TBD: Centered layout related settings // Show/hide a button that displays the buffer's line-ending mode. // Clicking the button brings up the line-ending selector. // Defaults to false. - "line_endings_button": false + "line_endings_button": false, + // Show/hide a button that displays the buffer's character encoding. + // If set to "non_utf8", the button is hidden only for UTF-8 without BOM. + // Defaults to "non_utf8". + "active_encoding_button": "non_utf8" }, "global_lsp_settings": { // Show/hide the LSP button in the status bar.