Create a new crate `encodings` that will have all that is not related to

R Aadarsh created

UI. The `encodings_ui` crate will only have UI related components in the
future.

Change summary

Cargo.lock                              |  10 
Cargo.toml                              |   3 
crates/encodings/Cargo.toml             |  15 
crates/encodings/src/lib.rs             | 429 +++++++-------------------
crates/encodings_ui/Cargo.toml          |  25 +
crates/encodings_ui/LICENSE-GPL         |   0 
crates/encodings_ui/src/lib.rs          | 343 +++++++++++++++++++++
crates/encodings_ui/src/selectors.rs    |   0 
crates/project/src/invalid_item_view.rs |  16 
crates/zed/Cargo.toml                   |   2 
crates/zed/src/main.rs                  |   2 
crates/zed/src/zed.rs                   |   2 
crates/zed_actions/src/lib.rs           |   2 
13 files changed, 514 insertions(+), 335 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5526,6 +5526,14 @@ dependencies = [
 [[package]]
 name = "encodings"
 version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "encoding_rs",
+]
+
+[[package]]
+name = "encodings_ui"
+version = "0.1.0"
 dependencies = [
  "editor",
  "encoding_rs",
@@ -21208,7 +21216,7 @@ dependencies = [
  "edit_prediction_button",
  "editor",
  "encoding_rs",
- "encodings",
+ "encodings_ui",
  "env_logger 0.11.8",
  "extension",
  "extension_host",

Cargo.toml 🔗

@@ -60,6 +60,7 @@ members = [
     "crates/editor",
     "crates/eval",
     "crates/encodings",
+    "crates/encodings_ui",
     "crates/explorer_command_injector",
     "crates/extension",
     "crates/extension_api",
@@ -223,6 +224,7 @@ members = [
     "tooling/perf",
     "tooling/xtask",
     "crates/encodings",
+    "crates/encodings_ui",
 ]
 default-members = ["crates/zed"]
 
@@ -316,6 +318,7 @@ edit_prediction_button = { path = "crates/edit_prediction_button" }
 edit_prediction_context = { path = "crates/edit_prediction_context" }
 zeta2_tools = { path = "crates/zeta2_tools" }
 encodings = {path = "crates/encodings"}
+encodings_ui = {path = "crates/encodings_ui"}
 inspector_ui = { path = "crates/inspector_ui" }
 install_cli = { path = "crates/install_cli" }
 journal = { path = "crates/journal" }

crates/encodings/Cargo.toml 🔗

@@ -5,21 +5,8 @@ publish.workspace = true
 edition.workspace = true
 
 [dependencies]
-editor.workspace = true
 encoding_rs.workspace = true
-fs.workspace = true
-futures.workspace = true
-fuzzy.workspace = true
-gpui.workspace = true
-language.workspace = true
-picker.workspace = true
-settings.workspace = true
-ui.workspace = true
-util.workspace = true
-workspace.workspace = true
-workspace-hack.workspace = true
-zed_actions.workspace = true
-
+anyhow.workspace = true
 
 [lints]
 workspace = true

crates/encodings/src/lib.rs 🔗

@@ -1,343 +1,154 @@
-//! A crate for handling file encodings in the text editor.
-
-use crate::selectors::encoding::Action;
-use editor::Editor;
-use encoding_rs::Encoding;
-use gpui::{ClickEvent, Entity, Subscription, WeakEntity};
-use language::Buffer;
-use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div};
-use ui::{Clickable, ParentElement};
-use util::ResultExt;
-use workspace::{
-    CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace,
-    with_active_or_new_workspace,
+use encoding_rs;
+use std::{
+    fmt::Debug,
+    sync::{Arc, Mutex, atomic::AtomicBool},
 };
-use zed_actions::encodings::{ForceOpen, Toggle};
 
-use crate::selectors::encoding::EncodingSelector;
-use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector;
+pub use encoding_rs::{
+    BIG5, EUC_JP, EUC_KR, GB18030, GBK, IBM866, ISO_2022_JP, ISO_8859_2, ISO_8859_3, ISO_8859_4,
+    ISO_8859_5, ISO_8859_6, ISO_8859_7, ISO_8859_8, ISO_8859_8_I, ISO_8859_10, ISO_8859_13,
+    ISO_8859_14, ISO_8859_15, ISO_8859_16, KOI8_R, KOI8_U, MACINTOSH, SHIFT_JIS, UTF_8, UTF_16BE,
+    UTF_16LE, WINDOWS_874, WINDOWS_1250, WINDOWS_1251, WINDOWS_1252, WINDOWS_1253, WINDOWS_1254,
+    WINDOWS_1255, WINDOWS_1256, WINDOWS_1257, WINDOWS_1258, X_MAC_CYRILLIC,
+};
 
-/// A status bar item that shows the current file encoding and allows changing it.
-pub struct EncodingIndicator {
-    pub encoding: Option<&'static Encoding>,
-    pub workspace: WeakEntity<Workspace>,
+pub struct Encoding(Mutex<&'static encoding_rs::Encoding>);
 
-    /// Subscription to observe changes in the active editor
-    observe_editor: Option<Subscription>,
+impl Debug for Encoding {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_tuple(&format!("Encoding{:?}", self.0))
+            .field(&self.get().name())
+            .finish()
+    }
+}
 
-    /// Subscription to observe changes in the `encoding` field of the `Buffer` struct
-    observe_buffer_encoding: Option<Subscription>,
+impl Default for Encoding {
+    fn default() -> Self {
+        Encoding(Mutex::new(UTF_8))
+    }
+}
 
-    /// Whether to show the indicator or not, based on whether an editor is active
-    show: bool,
+unsafe impl Send for Encoding {}
+unsafe impl Sync for Encoding {}
 
-    /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when
-    /// the current buffer is associated with a file.
-    show_save_or_reopen_selector: bool,
-}
+impl Encoding {
+    pub fn new(encoding: &'static encoding_rs::Encoding) -> Self {
+        Self(Mutex::new(encoding))
+    }
 
-pub mod selectors;
+    pub fn set(&self, encoding: &'static encoding_rs::Encoding) {
+        *self.0.lock().unwrap() = encoding;
+    }
 
-impl Render for EncodingIndicator {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
-        let status_element = div();
-        let show_save_or_reopen_selector = self.show_save_or_reopen_selector;
+    pub fn get(&self) -> &'static encoding_rs::Encoding {
+        *self.0.lock().unwrap()
+    }
 
-        if !self.show {
-            return status_element;
+    pub async fn decode(
+        &self,
+        input: Vec<u8>,
+        force: bool,
+        detect_utf16: bool,
+        buffer_encoding: Option<Arc<Encoding>>,
+    ) -> anyhow::Result<String> {
+        // Check if the input starts with a BOM for UTF-16 encodings only if detect_utf16 is true.
+        if detect_utf16 {
+            if let Some(encoding) = match input.get(..2) {
+                Some([0xFF, 0xFE]) => Some(UTF_16LE),
+                Some([0xFE, 0xFF]) => Some(UTF_16BE),
+                _ => None,
+            } {
+                self.set(encoding);
+
+                if let Some(v) = buffer_encoding {
+                    v.set(encoding)
+                }
+            }
         }
 
-        status_element.child(
-            Button::new(
-                "encoding",
-                encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)),
-            )
-            .label_size(LabelSize::Small)
-            .tooltip(Tooltip::text("Select Encoding"))
-            .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| {
-                if let Some(workspace) = indicator.workspace.upgrade() {
-                    workspace.update(cx, move |workspace, cx| {
-                        // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file,
-                        if show_save_or_reopen_selector {
-                            EncodingSaveOrReopenSelector::toggle(workspace, window, cx)
-                        }
-                        // otherwise, open the `EncodingSelector` directly.
-                        else {
-                            let (_, buffer, _) = workspace
-                                .active_item(cx)
-                                .unwrap()
-                                .act_as::<Editor>(cx)
-                                .unwrap()
-                                .read(cx)
-                                .active_excerpt(cx)
-                                .unwrap();
-
-                            let weak_workspace = workspace.weak_handle();
+        let (cow, had_errors) = self.get().decode_with_bom_removal(&input);
 
-                            workspace.toggle_modal(window, cx, |window, cx| {
-                                let selector = EncodingSelector::new(
-                                    window,
-                                    cx,
-                                    Action::Save,
-                                    Some(buffer.downgrade()),
-                                    weak_workspace,
-                                    None,
-                                );
-                                selector
-                            })
-                        }
-                    })
-                }
-            })),
-        )
-    }
-}
+        if force {
+            return Ok(cow.to_string());
+        }
 
-impl EncodingIndicator {
-    pub fn new(
-        encoding: Option<&'static Encoding>,
-        workspace: WeakEntity<Workspace>,
-        observe_editor: Option<Subscription>,
-        observe_buffer_encoding: Option<Subscription>,
-    ) -> EncodingIndicator {
-        EncodingIndicator {
-            encoding,
-            workspace,
-            observe_editor,
-            show: false,
-            observe_buffer_encoding,
-            show_save_or_reopen_selector: false,
+        if !had_errors {
+            Ok(cow.to_string())
+        } else {
+            Err(anyhow::anyhow!(
+                "The file contains invalid bytes for the specified encoding: {}.\nThis usually means that the file is not a regular text file, or is encoded in a different encoding.\nContinuing to open it may result in data loss if saved.",
+                self.get().name()
+            ))
         }
     }
 
-    /// Update the encoding when the active editor is switched.
-    pub fn update_when_editor_is_switched(
-        &mut self,
-        editor: Entity<Editor>,
-        _: &mut Window,
-        cx: &mut Context<EncodingIndicator>,
-    ) {
-        let editor = editor.read(cx);
-        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
-            let encoding = buffer.read(cx).encoding.clone();
-            self.encoding = Some(&*encoding.lock().unwrap());
+    pub async fn encode(&self, input: String) -> anyhow::Result<Vec<u8>> {
+        if self.get() == UTF_16BE {
+            let mut data = Vec::<u8>::with_capacity(input.len() * 2);
 
-            if let Some(_) = buffer.read(cx).file() {
-                self.show_save_or_reopen_selector = true;
-            } else {
-                self.show_save_or_reopen_selector = false;
-            }
-        }
+            // Convert the input string to UTF-16BE bytes
+            let utf16be_bytes = input.encode_utf16().flat_map(|u| u.to_be_bytes());
 
-        cx.notify();
-    }
+            data.extend(utf16be_bytes);
+            return Ok(data);
+        } else if self.get() == UTF_16LE {
+            let mut data = Vec::<u8>::with_capacity(input.len() * 2);
 
-    /// Update the encoding when the `encoding` field of the `Buffer` struct changes.
-    pub fn update_when_buffer_encoding_changes(
-        &mut self,
-        buffer: Entity<Buffer>,
-        _: &mut Window,
-        cx: &mut Context<EncodingIndicator>,
-    ) {
-        let encoding = buffer.read(cx).encoding.clone();
-        self.encoding = Some(&*encoding.lock().unwrap());
-        cx.notify();
-    }
-}
+            // Convert the input string to UTF-16LE bytes
+            let utf16le_bytes = input.encode_utf16().flat_map(|u| u.to_le_bytes());
 
-impl StatusItemView for EncodingIndicator {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        match active_pane_item.and_then(|item| item.downcast::<Editor>()) {
-            Some(editor) => {
-                self.observe_editor =
-                    Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched));
-                if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) {
-                    self.observe_buffer_encoding = Some(cx.observe_in(
-                        buffer,
-                        window,
-                        Self::update_when_buffer_encoding_changes,
-                    ));
-                }
-                self.update_when_editor_is_switched(editor, window, cx);
-                self.show = true;
-            }
-            None => {
-                self.encoding = None;
-                self.observe_editor = None;
-                self.show = false;
-            }
+            data.extend(utf16le_bytes);
+            return Ok(data);
+        } else {
+            let (cow, _encoding_used, _had_errors) = self.get().encode(&input);
+
+            Ok(cow.into_owned())
         }
     }
-}
-
-/// Get a human-readable name for the given encoding.
-pub fn encoding_name(encoding: &'static Encoding) -> String {
-    let name = encoding.name();
 
-    match name {
-        "UTF-8" => "UTF-8",
-        "UTF-16LE" => "UTF-16 LE",
-        "UTF-16BE" => "UTF-16 BE",
-        "windows-1252" => "Windows-1252",
-        "windows-1251" => "Windows-1251",
-        "windows-1250" => "Windows-1250",
-        "ISO-8859-2" => "ISO 8859-2",
-        "ISO-8859-3" => "ISO 8859-3",
-        "ISO-8859-4" => "ISO 8859-4",
-        "ISO-8859-5" => "ISO 8859-5",
-        "ISO-8859-6" => "ISO 8859-6",
-        "ISO-8859-7" => "ISO 8859-7",
-        "ISO-8859-8" => "ISO 8859-8",
-        "ISO-8859-13" => "ISO 8859-13",
-        "ISO-8859-15" => "ISO 8859-15",
-        "KOI8-R" => "KOI8-R",
-        "KOI8-U" => "KOI8-U",
-        "macintosh" => "MacRoman",
-        "x-mac-cyrillic" => "Mac Cyrillic",
-        "windows-874" => "Windows-874",
-        "windows-1253" => "Windows-1253",
-        "windows-1254" => "Windows-1254",
-        "windows-1255" => "Windows-1255",
-        "windows-1256" => "Windows-1256",
-        "windows-1257" => "Windows-1257",
-        "windows-1258" => "Windows-1258",
-        "EUC-KR" => "Windows-949",
-        "EUC-JP" => "EUC-JP",
-        "ISO-2022-JP" => "ISO 2022-JP",
-        "GBK" => "GBK",
-        "gb18030" => "GB18030",
-        "Big5" => "Big5",
-        _ => name,
+    pub fn reset(&self) {
+        self.set(UTF_8);
     }
-    .to_string()
 }
 
-/// Get an encoding from its index in the predefined list.
-/// If the index is out of range, UTF-8 is returned as a default.
-pub fn encoding_from_index(index: usize) -> &'static Encoding {
-    match index {
-        0 => encoding_rs::UTF_8,
-        1 => encoding_rs::UTF_16LE,
-        2 => encoding_rs::UTF_16BE,
-        3 => encoding_rs::WINDOWS_1252,
-        4 => encoding_rs::WINDOWS_1251,
-        5 => encoding_rs::WINDOWS_1250,
-        6 => encoding_rs::ISO_8859_2,
-        7 => encoding_rs::ISO_8859_3,
-        8 => encoding_rs::ISO_8859_4,
-        9 => encoding_rs::ISO_8859_5,
-        10 => encoding_rs::ISO_8859_6,
-        11 => encoding_rs::ISO_8859_7,
-        12 => encoding_rs::ISO_8859_8,
-        13 => encoding_rs::ISO_8859_13,
-        14 => encoding_rs::ISO_8859_15,
-        15 => encoding_rs::KOI8_R,
-        16 => encoding_rs::KOI8_U,
-        17 => encoding_rs::MACINTOSH,
-        18 => encoding_rs::X_MAC_CYRILLIC,
-        19 => encoding_rs::WINDOWS_874,
-        20 => encoding_rs::WINDOWS_1253,
-        21 => encoding_rs::WINDOWS_1254,
-        22 => encoding_rs::WINDOWS_1255,
-        23 => encoding_rs::WINDOWS_1256,
-        24 => encoding_rs::WINDOWS_1257,
-        25 => encoding_rs::WINDOWS_1258,
-        26 => encoding_rs::EUC_KR,
-        27 => encoding_rs::EUC_JP,
-        28 => encoding_rs::ISO_2022_JP,
-        29 => encoding_rs::GBK,
-        30 => encoding_rs::GB18030,
-        31 => encoding_rs::BIG5,
-        _ => encoding_rs::UTF_8,
-    }
+/// Convert a byte vector from a specified encoding to a UTF-8 string.
+pub async fn to_utf8(
+    input: Vec<u8>,
+    encoding: Encoding,
+    force: bool,
+    detect_utf16: bool,
+    buffer_encoding: Option<Arc<Encoding>>,
+) -> anyhow::Result<String> {
+    encoding
+        .decode(input, force, detect_utf16, buffer_encoding)
+        .await
 }
 
-/// Get an encoding from its name.
-pub fn encoding_from_name(name: &str) -> &'static Encoding {
-    match name {
-        "UTF-8" => encoding_rs::UTF_8,
-        "UTF-16 LE" => encoding_rs::UTF_16LE,
-        "UTF-16 BE" => encoding_rs::UTF_16BE,
-        "Windows-1252" => encoding_rs::WINDOWS_1252,
-        "Windows-1251" => encoding_rs::WINDOWS_1251,
-        "Windows-1250" => encoding_rs::WINDOWS_1250,
-        "ISO 8859-2" => encoding_rs::ISO_8859_2,
-        "ISO 8859-3" => encoding_rs::ISO_8859_3,
-        "ISO 8859-4" => encoding_rs::ISO_8859_4,
-        "ISO 8859-5" => encoding_rs::ISO_8859_5,
-        "ISO 8859-6" => encoding_rs::ISO_8859_6,
-        "ISO 8859-7" => encoding_rs::ISO_8859_7,
-        "ISO 8859-8" => encoding_rs::ISO_8859_8,
-        "ISO 8859-13" => encoding_rs::ISO_8859_13,
-        "ISO 8859-15" => encoding_rs::ISO_8859_15,
-        "KOI8-R" => encoding_rs::KOI8_R,
-        "KOI8-U" => encoding_rs::KOI8_U,
-        "MacRoman" => encoding_rs::MACINTOSH,
-        "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC,
-        "Windows-874" => encoding_rs::WINDOWS_874,
-        "Windows-1253" => encoding_rs::WINDOWS_1253,
-        "Windows-1254" => encoding_rs::WINDOWS_1254,
-        "Windows-1255" => encoding_rs::WINDOWS_1255,
-        "Windows-1256" => encoding_rs::WINDOWS_1256,
-        "Windows-1257" => encoding_rs::WINDOWS_1257,
-        "Windows-1258" => encoding_rs::WINDOWS_1258,
-        "Windows-949" => encoding_rs::EUC_KR,
-        "EUC-JP" => encoding_rs::EUC_JP,
-        "ISO 2022-JP" => encoding_rs::ISO_2022_JP,
-        "GBK" => encoding_rs::GBK,
-        "GB18030" => encoding_rs::GB18030,
-        "Big5" => encoding_rs::BIG5,
-        _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names
-    }
+/// Convert a UTF-8 string to a byte vector in a specified encoding.
+pub async fn from_utf8(input: String, target: Encoding) -> anyhow::Result<Vec<u8>> {
+    target.encode(input).await
 }
 
-pub fn init(cx: &mut App) {
-    cx.on_action(|action: &Toggle, cx: &mut App| {
-        let Toggle(path) = action.clone();
-        let path = path.to_path_buf();
-
-        with_active_or_new_workspace(cx, |workspace, window, cx| {
-            let weak_workspace = workspace.weak_handle();
-            workspace.toggle_modal(window, cx, |window, cx| {
-                EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path))
-            });
-        });
-    });
-
-    cx.on_action(|action: &ForceOpen, cx: &mut App| {
-        let ForceOpen(path) = action.clone();
-        let path = path.to_path_buf();
-
-        with_active_or_new_workspace(cx, |workspace, window, cx| {
-            workspace.active_pane().update(cx, |pane, cx| {
-                pane.close_active_item(&CloseActiveItem::default(), window, cx)
-                    .detach();
-            });
-
-            {
-                let force = workspace.encoding_options.force.get_mut();
-
-                *force = true;
-            }
+pub struct EncodingOptions {
+    pub encoding: Arc<Mutex<Encoding>>,
+    pub force: AtomicBool,
+    pub detect_utf16: AtomicBool,
+}
 
-            let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx);
-            let weak_workspace = workspace.weak_handle();
+impl EncodingOptions {
+    pub fn reset(&mut self) {
+        self.encoding.lock().unwrap().reset();
+        *self.force.get_mut() = false;
+        *self.detect_utf16.get_mut() = true;
+    }
+}
 
-            cx.spawn(async move |_, cx| {
-                let workspace = weak_workspace.upgrade().unwrap();
-                open_task.await.log_err();
-                workspace
-                    .update(cx, |workspace: &mut Workspace, _| {
-                        *workspace.encoding_options.force.get_mut() = false;
-                    })
-                    .log_err();
-            })
-            .detach();
-        });
-    });
+impl Default for EncodingOptions {
+    fn default() -> Self {
+        EncodingOptions {
+            encoding: Arc::new(Mutex::new(Encoding::default())),
+            force: AtomicBool::new(false),
+            detect_utf16: AtomicBool::new(true),
+        }
+    }
 }

crates/encodings_ui/Cargo.toml 🔗

@@ -0,0 +1,25 @@
+[package]
+name = "encodings_ui"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+
+[dependencies]
+editor.workspace = true
+encoding_rs.workspace = true
+fs.workspace = true
+futures.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+language.workspace = true
+picker.workspace = true
+settings.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
+workspace-hack.workspace = true
+zed_actions.workspace = true
+
+
+[lints]
+workspace = true

crates/encodings_ui/src/lib.rs 🔗

@@ -0,0 +1,343 @@
+//! A crate for handling file encodings in the text editor.
+
+use crate::selectors::encoding::Action;
+use editor::Editor;
+use encoding_rs::Encoding;
+use gpui::{ClickEvent, Entity, Subscription, WeakEntity};
+use language::Buffer;
+use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div};
+use ui::{Clickable, ParentElement};
+use util::ResultExt;
+use workspace::{
+    CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace,
+    with_active_or_new_workspace,
+};
+use zed_actions::encodings_ui::{ForceOpen, Toggle};
+
+use crate::selectors::encoding::EncodingSelector;
+use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector;
+
+/// A status bar item that shows the current file encoding and allows changing it.
+pub struct EncodingIndicator {
+    pub encoding: Option<&'static Encoding>,
+    pub workspace: WeakEntity<Workspace>,
+
+    /// Subscription to observe changes in the active editor
+    observe_editor: Option<Subscription>,
+
+    /// Subscription to observe changes in the `encoding` field of the `Buffer` struct
+    observe_buffer_encoding: Option<Subscription>,
+
+    /// Whether to show the indicator or not, based on whether an editor is active
+    show: bool,
+
+    /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when
+    /// the current buffer is associated with a file.
+    show_save_or_reopen_selector: bool,
+}
+
+pub mod selectors;
+
+impl Render for EncodingIndicator {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+        let status_element = div();
+        let show_save_or_reopen_selector = self.show_save_or_reopen_selector;
+
+        if !self.show {
+            return status_element;
+        }
+
+        status_element.child(
+            Button::new(
+                "encoding",
+                encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)),
+            )
+            .label_size(LabelSize::Small)
+            .tooltip(Tooltip::text("Select Encoding"))
+            .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| {
+                if let Some(workspace) = indicator.workspace.upgrade() {
+                    workspace.update(cx, move |workspace, cx| {
+                        // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file,
+                        if show_save_or_reopen_selector {
+                            EncodingSaveOrReopenSelector::toggle(workspace, window, cx)
+                        }
+                        // otherwise, open the `EncodingSelector` directly.
+                        else {
+                            let (_, buffer, _) = workspace
+                                .active_item(cx)
+                                .unwrap()
+                                .act_as::<Editor>(cx)
+                                .unwrap()
+                                .read(cx)
+                                .active_excerpt(cx)
+                                .unwrap();
+
+                            let weak_workspace = workspace.weak_handle();
+
+                            workspace.toggle_modal(window, cx, |window, cx| {
+                                let selector = EncodingSelector::new(
+                                    window,
+                                    cx,
+                                    Action::Save,
+                                    Some(buffer.downgrade()),
+                                    weak_workspace,
+                                    None,
+                                );
+                                selector
+                            })
+                        }
+                    })
+                }
+            })),
+        )
+    }
+}
+
+impl EncodingIndicator {
+    pub fn new(
+        encoding: Option<&'static Encoding>,
+        workspace: WeakEntity<Workspace>,
+        observe_editor: Option<Subscription>,
+        observe_buffer_encoding: Option<Subscription>,
+    ) -> EncodingIndicator {
+        EncodingIndicator {
+            encoding,
+            workspace,
+            observe_editor,
+            show: false,
+            observe_buffer_encoding,
+            show_save_or_reopen_selector: false,
+        }
+    }
+
+    /// Update the encoding when the active editor is switched.
+    pub fn update_when_editor_is_switched(
+        &mut self,
+        editor: Entity<Editor>,
+        _: &mut Window,
+        cx: &mut Context<EncodingIndicator>,
+    ) {
+        let editor = editor.read(cx);
+        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
+            let encoding = buffer.read(cx).encoding.clone();
+            self.encoding = Some(&*encoding.lock().unwrap());
+
+            if let Some(_) = buffer.read(cx).file() {
+                self.show_save_or_reopen_selector = true;
+            } else {
+                self.show_save_or_reopen_selector = false;
+            }
+        }
+
+        cx.notify();
+    }
+
+    /// Update the encoding when the `encoding` field of the `Buffer` struct changes.
+    pub fn update_when_buffer_encoding_changes(
+        &mut self,
+        buffer: Entity<Buffer>,
+        _: &mut Window,
+        cx: &mut Context<EncodingIndicator>,
+    ) {
+        let encoding = buffer.read(cx).encoding.clone();
+        self.encoding = Some(&*encoding.lock().unwrap());
+        cx.notify();
+    }
+}
+
+impl StatusItemView for EncodingIndicator {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+            Some(editor) => {
+                self.observe_editor =
+                    Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched));
+                if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) {
+                    self.observe_buffer_encoding = Some(cx.observe_in(
+                        buffer,
+                        window,
+                        Self::update_when_buffer_encoding_changes,
+                    ));
+                }
+                self.update_when_editor_is_switched(editor, window, cx);
+                self.show = true;
+            }
+            None => {
+                self.encoding = None;
+                self.observe_editor = None;
+                self.show = false;
+            }
+        }
+    }
+}
+
+/// Get a human-readable name for the given encoding.
+pub fn encoding_name(encoding: &'static Encoding) -> String {
+    let name = encoding.name();
+
+    match name {
+        "UTF-8" => "UTF-8",
+        "UTF-16LE" => "UTF-16 LE",
+        "UTF-16BE" => "UTF-16 BE",
+        "windows-1252" => "Windows-1252",
+        "windows-1251" => "Windows-1251",
+        "windows-1250" => "Windows-1250",
+        "ISO-8859-2" => "ISO 8859-2",
+        "ISO-8859-3" => "ISO 8859-3",
+        "ISO-8859-4" => "ISO 8859-4",
+        "ISO-8859-5" => "ISO 8859-5",
+        "ISO-8859-6" => "ISO 8859-6",
+        "ISO-8859-7" => "ISO 8859-7",
+        "ISO-8859-8" => "ISO 8859-8",
+        "ISO-8859-13" => "ISO 8859-13",
+        "ISO-8859-15" => "ISO 8859-15",
+        "KOI8-R" => "KOI8-R",
+        "KOI8-U" => "KOI8-U",
+        "macintosh" => "MacRoman",
+        "x-mac-cyrillic" => "Mac Cyrillic",
+        "windows-874" => "Windows-874",
+        "windows-1253" => "Windows-1253",
+        "windows-1254" => "Windows-1254",
+        "windows-1255" => "Windows-1255",
+        "windows-1256" => "Windows-1256",
+        "windows-1257" => "Windows-1257",
+        "windows-1258" => "Windows-1258",
+        "EUC-KR" => "Windows-949",
+        "EUC-JP" => "EUC-JP",
+        "ISO-2022-JP" => "ISO 2022-JP",
+        "GBK" => "GBK",
+        "gb18030" => "GB18030",
+        "Big5" => "Big5",
+        _ => name,
+    }
+    .to_string()
+}
+
+/// Get an encoding from its index in the predefined list.
+/// If the index is out of range, UTF-8 is returned as a default.
+pub fn encoding_from_index(index: usize) -> &'static Encoding {
+    match index {
+        0 => encoding_rs::UTF_8,
+        1 => encoding_rs::UTF_16LE,
+        2 => encoding_rs::UTF_16BE,
+        3 => encoding_rs::WINDOWS_1252,
+        4 => encoding_rs::WINDOWS_1251,
+        5 => encoding_rs::WINDOWS_1250,
+        6 => encoding_rs::ISO_8859_2,
+        7 => encoding_rs::ISO_8859_3,
+        8 => encoding_rs::ISO_8859_4,
+        9 => encoding_rs::ISO_8859_5,
+        10 => encoding_rs::ISO_8859_6,
+        11 => encoding_rs::ISO_8859_7,
+        12 => encoding_rs::ISO_8859_8,
+        13 => encoding_rs::ISO_8859_13,
+        14 => encoding_rs::ISO_8859_15,
+        15 => encoding_rs::KOI8_R,
+        16 => encoding_rs::KOI8_U,
+        17 => encoding_rs::MACINTOSH,
+        18 => encoding_rs::X_MAC_CYRILLIC,
+        19 => encoding_rs::WINDOWS_874,
+        20 => encoding_rs::WINDOWS_1253,
+        21 => encoding_rs::WINDOWS_1254,
+        22 => encoding_rs::WINDOWS_1255,
+        23 => encoding_rs::WINDOWS_1256,
+        24 => encoding_rs::WINDOWS_1257,
+        25 => encoding_rs::WINDOWS_1258,
+        26 => encoding_rs::EUC_KR,
+        27 => encoding_rs::EUC_JP,
+        28 => encoding_rs::ISO_2022_JP,
+        29 => encoding_rs::GBK,
+        30 => encoding_rs::GB18030,
+        31 => encoding_rs::BIG5,
+        _ => encoding_rs::UTF_8,
+    }
+}
+
+/// Get an encoding from its name.
+pub fn encoding_from_name(name: &str) -> &'static Encoding {
+    match name {
+        "UTF-8" => encoding_rs::UTF_8,
+        "UTF-16 LE" => encoding_rs::UTF_16LE,
+        "UTF-16 BE" => encoding_rs::UTF_16BE,
+        "Windows-1252" => encoding_rs::WINDOWS_1252,
+        "Windows-1251" => encoding_rs::WINDOWS_1251,
+        "Windows-1250" => encoding_rs::WINDOWS_1250,
+        "ISO 8859-2" => encoding_rs::ISO_8859_2,
+        "ISO 8859-3" => encoding_rs::ISO_8859_3,
+        "ISO 8859-4" => encoding_rs::ISO_8859_4,
+        "ISO 8859-5" => encoding_rs::ISO_8859_5,
+        "ISO 8859-6" => encoding_rs::ISO_8859_6,
+        "ISO 8859-7" => encoding_rs::ISO_8859_7,
+        "ISO 8859-8" => encoding_rs::ISO_8859_8,
+        "ISO 8859-13" => encoding_rs::ISO_8859_13,
+        "ISO 8859-15" => encoding_rs::ISO_8859_15,
+        "KOI8-R" => encoding_rs::KOI8_R,
+        "KOI8-U" => encoding_rs::KOI8_U,
+        "MacRoman" => encoding_rs::MACINTOSH,
+        "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC,
+        "Windows-874" => encoding_rs::WINDOWS_874,
+        "Windows-1253" => encoding_rs::WINDOWS_1253,
+        "Windows-1254" => encoding_rs::WINDOWS_1254,
+        "Windows-1255" => encoding_rs::WINDOWS_1255,
+        "Windows-1256" => encoding_rs::WINDOWS_1256,
+        "Windows-1257" => encoding_rs::WINDOWS_1257,
+        "Windows-1258" => encoding_rs::WINDOWS_1258,
+        "Windows-949" => encoding_rs::EUC_KR,
+        "EUC-JP" => encoding_rs::EUC_JP,
+        "ISO 2022-JP" => encoding_rs::ISO_2022_JP,
+        "GBK" => encoding_rs::GBK,
+        "GB18030" => encoding_rs::GB18030,
+        "Big5" => encoding_rs::BIG5,
+        _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names
+    }
+}
+
+pub fn init(cx: &mut App) {
+    cx.on_action(|action: &Toggle, cx: &mut App| {
+        let Toggle(path) = action.clone();
+        let path = path.to_path_buf();
+
+        with_active_or_new_workspace(cx, |workspace, window, cx| {
+            let weak_workspace = workspace.weak_handle();
+            workspace.toggle_modal(window, cx, |window, cx| {
+                EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path))
+            });
+        });
+    });
+
+    cx.on_action(|action: &ForceOpen, cx: &mut App| {
+        let ForceOpen(path) = action.clone();
+        let path = path.to_path_buf();
+
+        with_active_or_new_workspace(cx, |workspace, window, cx| {
+            workspace.active_pane().update(cx, |pane, cx| {
+                pane.close_active_item(&CloseActiveItem::default(), window, cx)
+                    .detach();
+            });
+
+            {
+                let force = workspace.encoding_options.force.get_mut();
+
+                *force = true;
+            }
+
+            let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx);
+            let weak_workspace = workspace.weak_handle();
+
+            cx.spawn(async move |_, cx| {
+                let workspace = weak_workspace.upgrade().unwrap();
+                open_task.await.log_err();
+                workspace
+                    .update(cx, |workspace: &mut Workspace, _| {
+                        *workspace.encoding_options.force.get_mut() = false;
+                    })
+                    .log_err();
+            })
+            .detach();
+        });
+    });
+}

crates/project/src/invalid_item_view.rs 🔗

@@ -2,9 +2,9 @@ use std::{path::Path, sync::Arc};
 
 use gpui::{EventEmitter, FocusHandle, Focusable};
 use ui::{
-    App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement,
-    KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _,
-    TintColor, Window, h_flex, v_flex,
+    h_flex, v_flex, App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder,
+    InteractiveElement, KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render,
+    SharedString, Styled as _, TintColor, Window,
 };
 use zed_actions::workspace::OpenWithSystem;
 
@@ -127,9 +127,11 @@ impl Render for InvalidItemView {
                                             .on_click(
                                                 move |_, window, cx| {
                                                     window.dispatch_action(
-                                                        Box::new(zed_actions::encodings::Toggle(
-                                                            path0.clone(),
-                                                        )),
+                                                        Box::new(
+                                                            zed_actions::encodings_ui::Toggle(
+                                                                path0.clone(),
+                                                            ),
+                                                        ),
                                                         cx,
                                                     )
                                                 },
@@ -145,7 +147,7 @@ impl Render for InvalidItemView {
                                                 move |_, window, cx| {
                                                     window.dispatch_action(
                                                         Box::new(
-                                                            zed_actions::encodings::ForceOpen(
+                                                            zed_actions::encodings_ui::ForceOpen(
                                                                 path1.clone(),
                                                             ),
                                                         ),

crates/zed/Cargo.toml 🔗

@@ -53,7 +53,7 @@ diagnostics.workspace = true
 editor.workspace = true
 zeta2_tools.workspace = true
 encoding.workspace = true
-encodings.workspace = true
+encodings_ui.workspace = true
 env_logger.workspace = true
 extension.workspace = true
 extension_host.workspace = true

crates/zed/src/main.rs 🔗

@@ -630,7 +630,7 @@ pub fn main() {
         zeta::init(cx);
         inspector_ui::init(app_state.clone(), cx);
         json_schema_store::init(cx);
-        encodings::init(cx);
+        encodings_ui::init(cx);
 
         cx.observe_global::<SettingsStore>({
             let http = app_state.client.http_client();

crates/zed/src/zed.rs 🔗

@@ -444,7 +444,7 @@ pub fn initialize_workspace(
         });
 
         let encoding_indicator = cx.new(|_cx| {
-            encodings::EncodingIndicator::new(None, workspace.weak_handle(), None, None)
+            encodings_ui::EncodingIndicator::new(None, workspace.weak_handle(), None, None)
         });
 
         let cursor_position =

crates/zed_actions/src/lib.rs 🔗

@@ -299,7 +299,7 @@ pub mod settings_profile_selector {
     pub struct Toggle;
 }
 
-pub mod encodings {
+pub mod encodings_ui {
     use std::sync::Arc;
 
     use gpui::Action;