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 <img width="487" height="104" alt="image" src="https://github.com/user-attachments/assets/041f096d-ac69-4bad-ac53-20cdcb41f733" /> <img width="454" height="99" alt="image" src="https://github.com/user-attachments/assets/ed76daa2-2733-484f-bb1f-4688357c035a" /> ## 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). <img width="1347" height="415" alt="image" src="https://github.com/user-attachments/assets/7f4f4938-3320-4d21-852c-53ee886d9a44" /> ## 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"}}`
This commit is contained in:
1 parent
7274a96154
commit
6b90aaa1bd
16 files changed
+210
-4
No files matched your search
Generated
+12
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -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<Workspace>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
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<Editor>, _: &mut Window, cx: &mut Context<Self>) {
|
||||
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<Self>) -> 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<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod active_buffer_encoding;
|
||||
pub use active_buffer_encoding::ActiveBufferEncoding;
|
||||
|
||||
pub fn init() {}
|
||||
@@ -435,6 +435,44 @@ pub struct StatusBarSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
pub line_endings_button: Option<bool>,
|
||||
/// Whether to show the active encoding button in the status bar.
|
||||
///
|
||||
/// Default: non_utf8
|
||||
pub active_encoding_button: Option<EncodingDisplayOptions>,
|
||||
}
|
||||
|
||||
#[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(
|
||||
|
||||
@@ -655,6 +655,7 @@ impl VsCodeSettings {
|
||||
active_language_button: None,
|
||||
cursor_position_button: None,
|
||||
line_endings_button: None,
|
||||
active_encoding_button: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2835,6 +2835,28 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
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.",
|
||||
|
||||
@@ -498,6 +498,7 @@ fn init_renderers(cx: &mut App) {
|
||||
.add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
|
||||
.add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
|
||||
.add_basic_renderer::<settings::StatusStyle>(render_dropdown)
|
||||
.add_basic_renderer::<settings::EncodingDisplayOptions>(render_dropdown)
|
||||
.add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
|
||||
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
|
||||
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user