Cleanup

Conrad Irwin created

* Remove the mutexes and have methods return the detected
  encoding.
* Try to handle the BOM safely...
* Clean up a bunch of code to make it more Zeddy

Change summary

Cargo.lock                                         |   2 
crates/copilot/src/copilot.rs                      |   9 
crates/edit_prediction_context/src/syntax_index.rs |   2 
crates/encodings/Cargo.toml                        |   5 
crates/encodings/src/encodings.rs                  | 214 +++
crates/encodings/src/lib.rs                        | 188 ---
crates/encodings_ui/Cargo.toml                     |   5 
crates/encodings_ui/src/encodings_ui.rs            | 108 +
crates/encodings_ui/src/selectors.rs               | 945 ++++++---------
crates/fs/src/fs.rs                                |  32 
crates/language/src/buffer.rs                      |  47 
crates/languages/src/json.rs                       |   2 
crates/multi_buffer/src/multi_buffer.rs            |   4 
crates/project/src/buffer_store.rs                 |  22 
crates/project/src/image_store.rs                  |   2 
crates/project/src/invalid_item_view.rs            | 122 --
crates/project/src/project.rs                      |  10 
crates/project/src/project_tests.rs                |  46 
crates/workspace/src/invalid_item_view.rs          |  55 
crates/workspace/src/workspace.rs                  |  48 
crates/worktree/src/worktree.rs                    |  84 -
crates/worktree/src/worktree_tests.rs              |   4 
crates/zed/src/zed.rs                              |   5 
crates/zed_actions/src/lib.rs                      |   2 
crates/zeta/src/zeta.rs                            |   2 
25 files changed, 799 insertions(+), 1,166 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -5601,7 +5601,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "editor",
- "encoding_rs",
+ "encodings",
  "fs",
  "futures 0.3.31",
  "fuzzy",

crates/copilot/src/copilot.rs πŸ”—

@@ -1241,7 +1241,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
 #[cfg(test)]
 mod tests {
     use super::*;
-    use encodings::{Encoding, EncodingOptions};
+    use encodings::Encoding;
     use gpui::TestAppContext;
     use util::{path, paths::PathStyle, rel_path::rel_path};
 
@@ -1452,12 +1452,7 @@ mod tests {
             self.abs_path.clone()
         }
 
-        fn load(
-            &self,
-            _: &App,
-            _: &EncodingOptions,
-            _: Option<Arc<Encoding>>,
-        ) -> Task<Result<String>> {
+        fn load(&self, _: &App, _: Encoding) -> Task<Result<String>> {
             unimplemented!()
         }
 

crates/edit_prediction_context/src/syntax_index.rs πŸ”—

@@ -523,7 +523,7 @@ impl SyntaxIndex {
         };
 
         let snapshot_task = worktree.update(cx, |worktree, cx| {
-            let load_task = worktree.load_file(&project_path.path, &Default::default(), None, cx);
+            let load_task = worktree.load_file(&project_path.path, &Default::default(), cx);
             let worktree_abs_path = worktree.abs_path();
 
             cx.spawn(async move |_this, cx| {

crates/encodings/Cargo.toml πŸ”—

@@ -4,6 +4,11 @@ version = "0.1.0"
 publish.workspace = true
 edition.workspace = true
 
+
+[lib]
+path = "src/encodings.rs"
+doctest = false
+
 [dependencies]
 anyhow.workspace = true
 encoding_rs.workspace = true

crates/encodings/src/encodings.rs πŸ”—

@@ -0,0 +1,214 @@
+use encoding_rs;
+use std::{borrow::Cow, fmt::Debug};
+
+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,
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Encoding {
+    pub encoding: &'static encoding_rs::Encoding,
+    pub with_bom: bool,
+}
+
+impl Default for Encoding {
+    fn default() -> Self {
+        Encoding {
+            encoding: UTF_8,
+            with_bom: false,
+        }
+    }
+}
+
+impl Encoding {
+    pub fn decode(&self, input: Vec<u8>) -> anyhow::Result<String> {
+        if self.encoding == UTF_8 && !self.with_bom {
+            return Ok(String::from_utf8(input)?);
+        }
+        let Some(result) = self
+            .encoding
+            .decode_without_bom_handling_and_without_replacement(&input)
+        else {
+            return Err(anyhow::anyhow!(
+                "input is not valid {}",
+                self.encoding.name()
+            ));
+        };
+
+        if self.with_bom && result.starts_with("\u{FEFF}") {
+            Ok(result[3..].to_string())
+        } else {
+            Ok(result.into_owned())
+        }
+    }
+
+    pub fn bom(&self) -> Option<&'static [u8]> {
+        if !self.with_bom {
+            return None;
+        }
+        if self.encoding == UTF_8 {
+            Some(&[0xEF, 0xBB, 0xBF])
+        } else if self.encoding == UTF_16BE {
+            Some(&[0xFE, 0xFF])
+        } else if self.encoding == UTF_16LE {
+            Some(&[0xFF, 0xFE])
+        } else {
+            None
+        }
+    }
+
+    pub fn encode_chunk<'a>(&self, input: &'a str) -> anyhow::Result<Cow<'a, [u8]>> {
+        if self.encoding == UTF_8 {
+            Ok(Cow::Borrowed(input.as_bytes()))
+        } else if self.encoding == UTF_16BE {
+            let mut data = Vec::<u8>::with_capacity(input.len() * 2);
+
+            // Convert the input string to UTF-16BE bytes
+            let utf16be_bytes = input.encode_utf16().flat_map(|u| u.to_be_bytes());
+
+            data.extend(utf16be_bytes);
+            Ok(Cow::Owned(data))
+        } else if self.encoding == UTF_16LE {
+            let mut data = Vec::<u8>::with_capacity(input.len() * 2);
+
+            // Convert the input string to UTF-16LE bytes
+            let utf16le_bytes = input.encode_utf16().flat_map(|u| u.to_le_bytes());
+
+            data.extend(utf16le_bytes);
+            Ok(Cow::Owned(data))
+        } else {
+            // todo: should we error on invalid content when encoding?
+            let (cow, _encoding_used, _had_errors) = self.encoding.encode(&input);
+
+            Ok(cow)
+        }
+    }
+
+    pub fn name(&self) -> &'static str {
+        let name = self.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 from_name(name: &str) -> Self {
+        let 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
+        };
+
+        Encoding {
+            encoding,
+            with_bom: false,
+        }
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct EncodingOptions {
+    pub expected: Encoding,
+    pub auto_detect: bool,
+}
+
+impl EncodingOptions {
+    pub fn process(&self, bytes: Vec<u8>) -> anyhow::Result<(Encoding, String)> {
+        let encoding = if self.auto_detect
+            && let Some(encoding) = Self::detect(&bytes)
+        {
+            encoding
+        } else {
+            self.expected
+        };
+
+        Ok((encoding, encoding.decode(bytes)?))
+    }
+
+    fn detect(bytes: &[u8]) -> Option<Encoding> {
+        if bytes.starts_with(&[0xFE, 0xFF]) {
+            Some(Encoding {
+                encoding: UTF_8,
+                with_bom: true,
+            })
+        } else if bytes.starts_with(&[0xFF, 0xFE]) {
+            Some(Encoding {
+                encoding: UTF_16LE,
+                with_bom: true,
+            })
+        } else if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
+            Some(Encoding {
+                encoding: UTF_8,
+                with_bom: true,
+            })
+        } else {
+            None
+        }
+    }
+}

crates/encodings/src/lib.rs πŸ”—

@@ -1,188 +0,0 @@
-use encoding_rs;
-use std::{
-    fmt::Debug,
-    sync::{Arc, Mutex, atomic::AtomicBool},
-};
-
-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,
-};
-
-pub struct Encoding(Mutex<&'static encoding_rs::Encoding>);
-
-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()
-    }
-}
-
-impl Clone for Encoding {
-    fn clone(&self) -> Self {
-        Encoding(Mutex::new(self.get()))
-    }
-}
-
-impl Default for Encoding {
-    fn default() -> Self {
-        Encoding(Mutex::new(UTF_8))
-    }
-}
-
-impl From<&'static encoding_rs::Encoding> for Encoding {
-    fn from(encoding: &'static encoding_rs::Encoding) -> Self {
-        Encoding::new(encoding)
-    }
-}
-
-unsafe impl Send for Encoding {}
-unsafe impl Sync for Encoding {}
-
-impl Encoding {
-    pub fn new(encoding: &'static encoding_rs::Encoding) -> Self {
-        Self(Mutex::new(encoding))
-    }
-
-    pub fn set(&self, encoding: &'static encoding_rs::Encoding) {
-        *self.0.lock().unwrap() = encoding;
-    }
-
-    pub fn get(&self) -> &'static encoding_rs::Encoding {
-        *self.0.lock().unwrap()
-    }
-
-    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)
-                }
-            }
-        }
-
-        let (cow, had_errors) = self.get().decode_with_bom_removal(&input);
-
-        if force {
-            return Ok(cow.to_string());
-        }
-
-        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()
-            ))
-        }
-    }
-
-    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);
-
-            // Convert the input string to UTF-16BE bytes
-            let utf16be_bytes = input.encode_utf16().flat_map(|u| u.to_be_bytes());
-
-            data.extend(utf16be_bytes);
-            return Ok(data);
-        } else if self.get() == UTF_16LE {
-            let mut data = Vec::<u8>::with_capacity(input.len() * 2);
-
-            // Convert the input string to UTF-16LE bytes
-            let utf16le_bytes = input.encode_utf16().flat_map(|u| u.to_le_bytes());
-
-            data.extend(utf16le_bytes);
-            return Ok(data);
-        } else {
-            let (cow, _encoding_used, _had_errors) = self.get().encode(&input);
-
-            Ok(cow.into_owned())
-        }
-    }
-
-    pub fn reset(&self) {
-        self.set(UTF_8);
-    }
-}
-
-/// Convert a byte vector from a specified encoding to a UTF-8 string.
-pub async fn to_utf8(
-    input: Vec<u8>,
-    options: &EncodingOptions,
-    buffer_encoding: Option<Arc<Encoding>>,
-) -> anyhow::Result<String> {
-    options
-        .encoding
-        .decode(
-            input,
-            options.force.load(std::sync::atomic::Ordering::Acquire),
-            options
-                .detect_utf16
-                .load(std::sync::atomic::Ordering::Acquire),
-            buffer_encoding,
-        )
-        .await
-}
-
-/// 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 struct EncodingOptions {
-    pub encoding: Arc<Encoding>,
-    pub force: AtomicBool,
-    pub detect_utf16: AtomicBool,
-}
-
-impl EncodingOptions {
-    pub fn reset(&self) {
-        self.encoding.reset();
-
-        self.force
-            .store(false, std::sync::atomic::Ordering::Release);
-
-        self.detect_utf16
-            .store(true, std::sync::atomic::Ordering::Release);
-    }
-}
-
-impl Default for EncodingOptions {
-    fn default() -> Self {
-        EncodingOptions {
-            encoding: Arc::new(Encoding::default()),
-            force: AtomicBool::new(false),
-            detect_utf16: AtomicBool::new(true),
-        }
-    }
-}
-
-impl Clone for EncodingOptions {
-    fn clone(&self) -> Self {
-        EncodingOptions {
-            encoding: Arc::new(self.encoding.get().into()),
-            force: AtomicBool::new(self.force.load(std::sync::atomic::Ordering::Acquire)),
-            detect_utf16: AtomicBool::new(
-                self.detect_utf16.load(std::sync::atomic::Ordering::Acquire),
-            ),
-        }
-    }
-}

crates/encodings_ui/Cargo.toml πŸ”—

@@ -7,7 +7,7 @@ edition.workspace = true
 [dependencies]
 anyhow.workspace = true
 editor.workspace = true
-encoding_rs.workspace = true
+encodings.workspace = true
 fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
@@ -20,6 +20,9 @@ util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 
+[lib]
+path = "src/encodings_ui.rs"
+doctest = false
 
 [lints]
 workspace = true

crates/encodings_ui/src/encodings_ui.rs πŸ”—

@@ -0,0 +1,108 @@
+//! A crate for handling file encodings in the text editor.
+
+use editor::Editor;
+use gpui::{Entity, Subscription, WeakEntity};
+use language::{Buffer, BufferEvent};
+use ui::{
+    App, Button, ButtonCommon, Context, IntoElement, LabelSize, Render, Tooltip, Window, div,
+};
+use ui::{Clickable, ParentElement};
+use workspace::notifications::NotifyTaskExt;
+use workspace::{ItemHandle, StatusItemView, Workspace};
+use zed_actions::encodings_ui::OpenWithEncoding;
+// use zed_actions::encodings_ui::Toggle;
+
+/// A status bar item that shows the current file encoding and allows changing it.
+pub struct EncodingIndicator {
+    pub buffer: Option<WeakEntity<Buffer>>,
+    pub workspace: WeakEntity<Workspace>,
+    observe_buffer: Option<Subscription>,
+}
+
+pub mod selectors;
+
+impl Render for EncodingIndicator {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+        let Some(buffer) = self.buffer() else {
+            return gpui::Empty.into_any_element();
+        };
+
+        div()
+            .child(
+                Button::new("encoding", buffer.read(cx).encoding().name())
+                    .label_size(LabelSize::Small)
+                    .tooltip(Tooltip::text("Select Encoding"))
+                    .on_click(cx.listener(move |this, _, window, cx| {
+                        let Some(buffer) = this.buffer() else {
+                            return;
+                        };
+                        this.workspace
+                            .update(cx, move |workspace, cx| {
+                                if buffer.read(cx).file().is_some() {
+                                    selectors::save_or_reopen(buffer, workspace, window, cx)
+                                } else {
+                                    // todo!()
+                                }
+                            })
+                            .ok();
+                    })),
+            )
+            .into_any_element()
+    }
+}
+
+impl EncodingIndicator {
+    pub fn new(workspace: WeakEntity<Workspace>) -> EncodingIndicator {
+        EncodingIndicator {
+            workspace,
+            buffer: None,
+            observe_buffer: None,
+        }
+    }
+
+    fn buffer(&self) -> Option<Entity<Buffer>> {
+        self.buffer.as_ref().and_then(|b| b.upgrade())
+    }
+
+    /// Update the encoding when the `encoding` field of the `Buffer` struct changes.
+    pub fn on_buffer_event(
+        &mut self,
+        _: Entity<Buffer>,
+        e: &BufferEvent,
+        cx: &mut Context<EncodingIndicator>,
+    ) {
+        if matches!(e, BufferEvent::EncodingChanged) {
+            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>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx))
+            && let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
+        {
+            self.buffer = Some(buffer.downgrade());
+            self.observe_buffer = Some(cx.subscribe(&buffer, Self::on_buffer_event));
+        } else {
+            self.buffer = None;
+            self.observe_buffer = None;
+        }
+        cx.notify();
+    }
+}
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _, _| {
+        workspace.register_action(|workspace, action: &OpenWithEncoding, window, cx| {
+            selectors::open_with_encoding(action.0.clone(), workspace, window, cx)
+                .detach_and_notify_err(window, cx);
+        });
+    })
+    .detach();
+}

crates/encodings_ui/src/selectors.rs πŸ”—

@@ -1,632 +1,409 @@
-/// This module contains the encoding selectors for saving or reopening files with a different encoding.
-/// It provides a modal view that allows the user to choose between saving with a different encoding
-/// or reopening with a different encoding.
-pub mod save_or_reopen {
-    use editor::Editor;
-    use gpui::Styled;
-    use gpui::{AppContext, ParentElement};
-    use picker::Picker;
-    use picker::PickerDelegate;
-    use std::sync::atomic::AtomicBool;
-    use util::ResultExt;
-
-    use fuzzy::{StringMatch, StringMatchCandidate};
-    use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
-
-    use ui::{Context, HighlightedLabel, ListItem, Render, Window, rems, v_flex};
-    use workspace::{ModalView, Workspace};
-
-    use crate::selectors::encoding::{Action, EncodingSelector};
-
-    /// A modal view that allows the user to select between saving with a different encoding or
-    /// reopening with a different encoding.
-    pub struct EncodingSaveOrReopenSelector {
-        picker: Entity<Picker<EncodingSaveOrReopenDelegate>>,
-        pub current_selection: usize,
-    }
-
-    impl EncodingSaveOrReopenSelector {
-        pub fn new(
-            window: &mut Window,
-            cx: &mut Context<EncodingSaveOrReopenSelector>,
-            workspace: WeakEntity<Workspace>,
-        ) -> Self {
-            let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace);
-
-            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+use anyhow::Result;
+use editor::Editor;
+use encodings::Encoding;
+use encodings::EncodingOptions;
+use futures::channel::oneshot;
+use gpui::ParentElement;
+use gpui::Task;
+use language::Buffer;
+use picker::Picker;
+use picker::PickerDelegate;
+use std::path::Path;
+use std::sync::Arc;
+use std::sync::atomic::AtomicBool;
+use ui::Label;
+use ui::ListItemSpacing;
+use ui::rems;
+use util::ResultExt;
+
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{DismissEvent, Entity, WeakEntity};
+
+use ui::{Context, HighlightedLabel, ListItem, Window};
+use workspace::Workspace;
+
+pub fn save_or_reopen(
+    buffer: Entity<Buffer>,
+    workspace: &mut Workspace,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    let weak_workspace = cx.weak_entity();
+    workspace.toggle_modal(window, cx, |window, cx| {
+        let delegate = EncodingSaveOrReopenDelegate::new(buffer, weak_workspace);
+        Picker::nonsearchable_uniform_list(delegate, window, cx)
+            .modal(true)
+            .width(rems(34.0))
+    })
+}
 
-            Self {
-                picker,
-                current_selection: 0,
-            }
-        }
+pub fn open_with_encoding(
+    path: Arc<Path>,
+    workspace: &mut Workspace,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) -> Task<Result<()>> {
+    let (tx, rx) = oneshot::channel();
+    workspace.toggle_modal(window, cx, |window, cx| {
+        let delegate = EncodingSelectorDelegate::new(None, tx);
+        Picker::uniform_list(delegate, window, cx)
+    });
+    let project = workspace.project().clone();
+    cx.spawn_in(window, async move |workspace, cx| {
+        let encoding = rx.await.unwrap();
+
+        let (worktree, rel_path) = project
+            .update(cx, |project, cx| {
+                project.find_or_create_worktree(path, false, cx)
+            })?
+            .await?;
+
+        let project_path = (worktree.update(cx, |worktree, _| worktree.id())?, rel_path).into();
+
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.buffer_store().update(cx, |buffer_store, cx| {
+                    buffer_store.open_buffer(
+                        project_path,
+                        &EncodingOptions {
+                            expected: encoding,
+                            auto_detect: true,
+                        },
+                        cx,
+                    )
+                })
+            })?
+            .await?;
+        workspace.update_in(cx, |workspace, window, cx| {
+            workspace.open_project_item::<Editor>(
+                workspace.active_pane().clone(),
+                buffer,
+                true,
+                true,
+                window,
+                cx,
+            )
+        })?;
 
-        /// Toggle the modal view for selecting between saving with a different encoding or
-        /// reopening with a different encoding.
-        pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
-            let weak_workspace = workspace.weak_handle();
-            workspace.toggle_modal(window, cx, |window, cx| {
-                EncodingSaveOrReopenSelector::new(window, cx, weak_workspace)
-            });
-        }
-    }
+        Ok(())
+    })
+}
 
-    impl Focusable for EncodingSaveOrReopenSelector {
-        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
-            self.picker.focus_handle(cx)
+pub fn reopen_with_encoding(
+    buffer: Entity<Buffer>,
+    workspace: &mut Workspace,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    let encoding = buffer.read(cx).encoding();
+    let (tx, rx) = oneshot::channel();
+    workspace.toggle_modal(window, cx, |window, cx| {
+        let delegate = EncodingSelectorDelegate::new(Some(encoding), tx);
+        Picker::uniform_list(delegate, window, cx)
+    });
+    cx.spawn(async move |_, cx| {
+        let encoding = rx.await.unwrap();
+
+        let (task, prev) = buffer.update(cx, |buffer, cx| {
+            let prev = buffer.encoding();
+            buffer.set_encoding(encoding, cx);
+            (buffer.reload(cx), prev)
+        })?;
+
+        if task.await.is_err() {
+            buffer.update(cx, |buffer, cx| {
+                buffer.set_encoding(prev, cx);
+            })?;
         }
-    }
 
-    impl Render for EncodingSaveOrReopenSelector {
-        fn render(
-            &mut self,
-            _window: &mut Window,
-            _cx: &mut Context<Self>,
-        ) -> impl ui::IntoElement {
-            v_flex().w(rems(34.0)).child(self.picker.clone())
-        }
-    }
+        anyhow::Ok(())
+    })
+    .detach();
+}
 
-    impl ModalView for EncodingSaveOrReopenSelector {}
+pub fn save_with_encoding(
+    buffer: Entity<Buffer>,
+    workspace: &mut Workspace,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    let encoding = buffer.read(cx).encoding();
+    let (tx, rx) = oneshot::channel();
+    workspace.toggle_modal(window, cx, |window, cx| {
+        let delegate = EncodingSelectorDelegate::new(Some(encoding), tx);
+        Picker::uniform_list(delegate, window, cx)
+    });
+    cx.spawn(async move |workspace, cx| {
+        let encoding = rx.await.unwrap();
+        workspace
+            .update(cx, |workspace, cx| {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.set_encoding(encoding, cx);
+                });
+                workspace
+                    .project()
+                    .update(cx, |project, cx| project.save_buffer(buffer, cx))
+            })
+            .ok();
+    })
+    .detach();
+}
 
-    impl EventEmitter<DismissEvent> for EncodingSaveOrReopenSelector {}
+pub enum SaveOrReopen {
+    Save,
+    Reopen,
+}
 
-    pub struct EncodingSaveOrReopenDelegate {
-        selector: WeakEntity<EncodingSaveOrReopenSelector>,
-        current_selection: usize,
-        matches: Vec<StringMatch>,
-        pub actions: Vec<StringMatchCandidate>,
-        workspace: WeakEntity<Workspace>,
-    }
+pub struct EncodingSaveOrReopenDelegate {
+    current_selection: usize,
+    actions: Vec<SaveOrReopen>,
+    workspace: WeakEntity<Workspace>,
+    buffer: Entity<Buffer>,
+}
 
-    impl EncodingSaveOrReopenDelegate {
-        pub fn new(
-            selector: WeakEntity<EncodingSaveOrReopenSelector>,
-            workspace: WeakEntity<Workspace>,
-        ) -> Self {
-            Self {
-                selector,
-                current_selection: 0,
-                matches: Vec::new(),
-                actions: vec![
-                    StringMatchCandidate::new(0, "Save with encoding"),
-                    StringMatchCandidate::new(1, "Reopen with encoding"),
-                ],
-                workspace,
-            }
+impl EncodingSaveOrReopenDelegate {
+    pub fn new(buffer: Entity<Buffer>, workspace: WeakEntity<Workspace>) -> Self {
+        Self {
+            current_selection: 0,
+            actions: vec![SaveOrReopen::Save, SaveOrReopen::Reopen],
+            workspace,
+            buffer,
         }
+    }
+}
 
-        pub fn get_actions(&self) -> (&str, &str) {
-            (&self.actions[0].string, &self.actions[1].string)
-        }
+impl PickerDelegate for EncodingSaveOrReopenDelegate {
+    type ListItem = ListItem;
 
-        /// Handle the action selected by the user.
-        pub fn post_selection(
-            &self,
-            cx: &mut Context<Picker<EncodingSaveOrReopenDelegate>>,
-            window: &mut Window,
-        ) -> Option<()> {
-            if self.current_selection == 0 {
-                if let Some(workspace) = self.workspace.upgrade() {
-                    let (_, buffer, _) = workspace
-                        .read(cx)
-                        .active_item(cx)?
-                        .act_as::<Editor>(cx)?
-                        .read(cx)
-                        .active_excerpt(cx)?;
-
-                    let weak_workspace = workspace.read(cx).weak_handle();
-
-                    if let Some(file) = buffer.read(cx).file() {
-                        let path = file.as_local()?.abs_path(cx);
-
-                        workspace.update(cx, |workspace, cx| {
-                            workspace.toggle_modal(window, cx, |window, cx| {
-                                let selector = EncodingSelector::new(
-                                    window,
-                                    cx,
-                                    Action::Save,
-                                    Some(buffer.downgrade()),
-                                    weak_workspace,
-                                    Some(path),
-                                );
-                                selector
-                            })
-                        });
-                    }
-                }
-            } else if self.current_selection == 1 {
-                if let Some(workspace) = self.workspace.upgrade() {
-                    let (_, buffer, _) = workspace
-                        .read(cx)
-                        .active_item(cx)?
-                        .act_as::<Editor>(cx)?
-                        .read(cx)
-                        .active_excerpt(cx)?;
-
-                    let weak_workspace = workspace.read(cx).weak_handle();
-
-                    if let Some(file) = buffer.read(cx).file() {
-                        let path = file.as_local()?.abs_path(cx);
-
-                        workspace.update(cx, |workspace, cx| {
-                            workspace.toggle_modal(window, cx, |window, cx| {
-                                let selector = EncodingSelector::new(
-                                    window,
-                                    cx,
-                                    Action::Reopen,
-                                    Some(buffer.downgrade()),
-                                    weak_workspace,
-                                    Some(path),
-                                );
-                                selector
-                            });
-                        });
-                    }
-                }
-            }
+    fn match_count(&self) -> usize {
+        self.actions.len()
+    }
 
-            Some(())
-        }
+    fn selected_index(&self) -> usize {
+        self.current_selection
     }
 
-    impl PickerDelegate for EncodingSaveOrReopenDelegate {
-        type ListItem = ListItem;
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) {
+        self.current_selection = ix;
+    }
 
-        fn match_count(&self) -> usize {
-            self.matches.len()
-        }
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
+        "Select an action...".into()
+    }
 
-        fn selected_index(&self) -> usize {
-            self.current_selection
-        }
+    fn update_matches(
+        &mut self,
+        _query: String,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        return Task::ready(());
+    }
 
-        fn set_selected_index(
-            &mut self,
-            ix: usize,
-            _window: &mut Window,
-            cx: &mut Context<Picker<Self>>,
-        ) {
-            self.current_selection = ix;
-            self.selector
-                .update(cx, |selector, _cx| {
-                    selector.current_selection = ix;
+    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        self.dismissed(window, cx);
+        cx.defer_in(window, |this, window, cx| {
+            let this = &this.delegate;
+            this.workspace
+                .update(cx, |workspace, cx| {
+                    match this.actions[this.current_selection] {
+                        SaveOrReopen::Reopen => {
+                            reopen_with_encoding(this.buffer.clone(), workspace, window, cx);
+                        }
+                        SaveOrReopen::Save => {
+                            save_with_encoding(this.buffer.clone(), workspace, window, cx);
+                        }
+                    }
                 })
-                .log_err();
-        }
-
-        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
-            "Select an action...".into()
-        }
+                .ok();
+        })
+    }
 
-        fn update_matches(
-            &mut self,
-            query: String,
-            window: &mut Window,
-            cx: &mut Context<Picker<Self>>,
-        ) -> gpui::Task<()> {
-            let executor = cx.background_executor().clone();
-            let actions = self.actions.clone();
-
-            cx.spawn_in(window, async move |this, cx| {
-                let matches = if query.is_empty() {
-                    actions
-                        .into_iter()
-                        .enumerate()
-                        .map(|(index, value)| StringMatch {
-                            candidate_id: index,
-                            score: 0.0,
-                            positions: vec![],
-                            string: value.string,
-                        })
-                        .collect::<Vec<StringMatch>>()
-                } else {
-                    fuzzy::match_strings(
-                        &actions,
-                        &query,
-                        false,
-                        false,
-                        2,
-                        &AtomicBool::new(false),
-                        executor,
-                    )
-                    .await
-                };
+    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        cx.emit(DismissEvent)
+    }
 
-                this.update(cx, |picker, cx| {
-                    let delegate = &mut picker.delegate;
-                    delegate.matches = matches;
-                    delegate.current_selection = delegate
-                        .current_selection
-                        .min(delegate.matches.len().saturating_sub(1));
-                    delegate
-                        .selector
-                        .update(cx, |selector, _cx| {
-                            selector.current_selection = delegate.current_selection
-                        })
-                        .log_err();
-                    cx.notify();
+    fn render_match(
+        &self,
+        ix: usize,
+        _: bool,
+        _: &mut Window,
+        _: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        Some(
+            ListItem::new(ix)
+                .child(match self.actions[ix] {
+                    SaveOrReopen::Save => Label::new("Save with encoding"),
+                    SaveOrReopen::Reopen => Label::new("Reopen with encoding"),
                 })
-                .log_err();
-            })
-        }
-
-        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-            self.dismissed(window, cx);
-            if self.selector.is_upgradable() {
-                self.post_selection(cx, window);
-            }
-        }
+                .spacing(ui::ListItemSpacing::Sparse),
+        )
+    }
+}
 
-        fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-            self.selector
-                .update(cx, |_, cx| cx.emit(DismissEvent))
-                .log_err();
-        }
+pub struct EncodingSelectorDelegate {
+    current_selection: usize,
+    encodings: Vec<StringMatchCandidate>,
+    matches: Vec<StringMatch>,
+    tx: Option<oneshot::Sender<Encoding>>,
+}
 
-        fn render_match(
-            &self,
-            ix: usize,
-            _: bool,
-            _: &mut Window,
-            _: &mut Context<Picker<Self>>,
-        ) -> Option<Self::ListItem> {
-            Some(
-                ListItem::new(ix)
-                    .child(HighlightedLabel::new(
-                        &self.matches[ix].string,
-                        self.matches[ix].positions.clone(),
-                    ))
-                    .spacing(ui::ListItemSpacing::Sparse),
-            )
+impl EncodingSelectorDelegate {
+    pub fn new(
+        encoding: Option<Encoding>,
+        tx: oneshot::Sender<Encoding>,
+    ) -> EncodingSelectorDelegate {
+        let encodings = vec![
+            StringMatchCandidate::new(0, "UTF-8"),
+            StringMatchCandidate::new(1, "UTF-16 LE"),
+            StringMatchCandidate::new(2, "UTF-16 BE"),
+            StringMatchCandidate::new(3, "Windows-1252"),
+            StringMatchCandidate::new(4, "Windows-1251"),
+            StringMatchCandidate::new(5, "Windows-1250"),
+            StringMatchCandidate::new(6, "ISO 8859-2"),
+            StringMatchCandidate::new(7, "ISO 8859-3"),
+            StringMatchCandidate::new(8, "ISO 8859-4"),
+            StringMatchCandidate::new(9, "ISO 8859-5"),
+            StringMatchCandidate::new(10, "ISO 8859-6"),
+            StringMatchCandidate::new(11, "ISO 8859-7"),
+            StringMatchCandidate::new(12, "ISO 8859-8"),
+            StringMatchCandidate::new(13, "ISO 8859-13"),
+            StringMatchCandidate::new(14, "ISO 8859-15"),
+            StringMatchCandidate::new(15, "KOI8-R"),
+            StringMatchCandidate::new(16, "KOI8-U"),
+            StringMatchCandidate::new(17, "MacRoman"),
+            StringMatchCandidate::new(18, "Mac Cyrillic"),
+            StringMatchCandidate::new(19, "Windows-874"),
+            StringMatchCandidate::new(20, "Windows-1253"),
+            StringMatchCandidate::new(21, "Windows-1254"),
+            StringMatchCandidate::new(22, "Windows-1255"),
+            StringMatchCandidate::new(23, "Windows-1256"),
+            StringMatchCandidate::new(24, "Windows-1257"),
+            StringMatchCandidate::new(25, "Windows-1258"),
+            StringMatchCandidate::new(26, "Windows-949"),
+            StringMatchCandidate::new(27, "EUC-JP"),
+            StringMatchCandidate::new(28, "ISO 2022-JP"),
+            StringMatchCandidate::new(29, "GBK"),
+            StringMatchCandidate::new(30, "GB18030"),
+            StringMatchCandidate::new(31, "Big5"),
+        ];
+        let current_selection = if let Some(encoding) = encoding {
+            encodings
+                .iter()
+                .position(|e| encoding.name() == e.string)
+                .unwrap_or_default()
+        } else {
+            0
+        };
+
+        EncodingSelectorDelegate {
+            current_selection,
+            encodings,
+            matches: Vec::new(),
+            tx: Some(tx),
         }
     }
 }
 
-/// This module contains the encoding selector for choosing an encoding to save or reopen a file with.
-pub mod encoding {
-    use editor::Editor;
-    use std::{path::PathBuf, sync::atomic::AtomicBool};
-
-    use fuzzy::{StringMatch, StringMatchCandidate};
-    use gpui::{
-        AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, http_client::anyhow,
-    };
-    use language::Buffer;
-    use picker::{Picker, PickerDelegate};
-    use ui::{
-        Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled,
-        Window, rems, v_flex,
-    };
-    use util::ResultExt;
-    use workspace::{CloseActiveItem, ModalView, OpenOptions, Workspace};
-
-    use crate::encoding_from_name;
-
-    /// A modal view that allows the user to select an encoding from a list of encodings.
-    pub struct EncodingSelector {
-        picker: Entity<Picker<EncodingSelectorDelegate>>,
-        workspace: WeakEntity<Workspace>,
-        path: Option<PathBuf>,
-    }
+impl PickerDelegate for EncodingSelectorDelegate {
+    type ListItem = ListItem;
 
-    pub struct EncodingSelectorDelegate {
-        current_selection: usize,
-        encodings: Vec<StringMatchCandidate>,
-        matches: Vec<StringMatch>,
-        selector: WeakEntity<EncodingSelector>,
-        buffer: Option<WeakEntity<Buffer>>,
-        action: Action,
+    fn match_count(&self) -> usize {
+        self.matches.len()
     }
 
-    impl EncodingSelectorDelegate {
-        pub fn new(
-            selector: WeakEntity<EncodingSelector>,
-            buffer: Option<WeakEntity<Buffer>>,
-            action: Action,
-        ) -> EncodingSelectorDelegate {
-            EncodingSelectorDelegate {
-                current_selection: 0,
-                encodings: vec![
-                    StringMatchCandidate::new(0, "UTF-8"),
-                    StringMatchCandidate::new(1, "UTF-16 LE"),
-                    StringMatchCandidate::new(2, "UTF-16 BE"),
-                    StringMatchCandidate::new(3, "Windows-1252"),
-                    StringMatchCandidate::new(4, "Windows-1251"),
-                    StringMatchCandidate::new(5, "Windows-1250"),
-                    StringMatchCandidate::new(6, "ISO 8859-2"),
-                    StringMatchCandidate::new(7, "ISO 8859-3"),
-                    StringMatchCandidate::new(8, "ISO 8859-4"),
-                    StringMatchCandidate::new(9, "ISO 8859-5"),
-                    StringMatchCandidate::new(10, "ISO 8859-6"),
-                    StringMatchCandidate::new(11, "ISO 8859-7"),
-                    StringMatchCandidate::new(12, "ISO 8859-8"),
-                    StringMatchCandidate::new(13, "ISO 8859-13"),
-                    StringMatchCandidate::new(14, "ISO 8859-15"),
-                    StringMatchCandidate::new(15, "KOI8-R"),
-                    StringMatchCandidate::new(16, "KOI8-U"),
-                    StringMatchCandidate::new(17, "MacRoman"),
-                    StringMatchCandidate::new(18, "Mac Cyrillic"),
-                    StringMatchCandidate::new(19, "Windows-874"),
-                    StringMatchCandidate::new(20, "Windows-1253"),
-                    StringMatchCandidate::new(21, "Windows-1254"),
-                    StringMatchCandidate::new(22, "Windows-1255"),
-                    StringMatchCandidate::new(23, "Windows-1256"),
-                    StringMatchCandidate::new(24, "Windows-1257"),
-                    StringMatchCandidate::new(25, "Windows-1258"),
-                    StringMatchCandidate::new(26, "Windows-949"),
-                    StringMatchCandidate::new(27, "EUC-JP"),
-                    StringMatchCandidate::new(28, "ISO 2022-JP"),
-                    StringMatchCandidate::new(29, "GBK"),
-                    StringMatchCandidate::new(30, "GB18030"),
-                    StringMatchCandidate::new(31, "Big5"),
-                ],
-                matches: Vec::new(),
-                selector,
-                buffer: buffer,
-                action,
-            }
-        }
+    fn selected_index(&self) -> usize {
+        self.current_selection
     }
 
-    impl PickerDelegate for EncodingSelectorDelegate {
-        type ListItem = ListItem;
-
-        fn match_count(&self) -> usize {
-            self.matches.len()
-        }
-
-        fn selected_index(&self) -> usize {
-            self.current_selection
-        }
-
-        fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) {
-            self.current_selection = ix;
-        }
+    fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) {
+        self.current_selection = ix;
+    }
 
-        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
-            "Select an encoding...".into()
-        }
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
+        "Select an encoding...".into()
+    }
 
-        fn update_matches(
-            &mut self,
-            query: String,
-            window: &mut Window,
-            cx: &mut Context<Picker<Self>>,
-        ) -> gpui::Task<()> {
-            let executor = cx.background_executor().clone();
-            let encodings = self.encodings.clone();
-
-            cx.spawn_in(window, async move |picker, cx| {
-                let matches: Vec<StringMatch>;
-
-                if query.is_empty() {
-                    matches = encodings
-                        .into_iter()
-                        .enumerate()
-                        .map(|(index, value)| StringMatch {
-                            candidate_id: index,
-                            score: 0.0,
-                            positions: Vec::new(),
-                            string: value.string,
-                        })
-                        .collect();
-                } else {
-                    matches = fuzzy::match_strings(
-                        &encodings,
-                        &query,
-                        true,
-                        false,
-                        30,
-                        &AtomicBool::new(false),
-                        executor,
-                    )
-                    .await
-                }
-                picker
-                    .update(cx, |picker, cx| {
-                        let delegate = &mut picker.delegate;
-                        delegate.matches = matches;
-                        delegate.current_selection = delegate
-                            .current_selection
-                            .min(delegate.matches.len().saturating_sub(1));
-                        cx.notify();
+    fn update_matches(
+        &mut self,
+        query: String,
+        window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        let executor = cx.background_executor().clone();
+        let encodings = self.encodings.clone();
+
+        cx.spawn_in(window, async move |picker, cx| {
+            let matches: Vec<StringMatch>;
+
+            if query.is_empty() {
+                matches = encodings
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, value)| StringMatch {
+                        candidate_id: index,
+                        score: 0.0,
+                        positions: Vec::new(),
+                        string: value.string,
                     })
-                    .log_err();
-            })
-        }
-
-        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-            let workspace = self
-                .selector
-                .upgrade()
-                .unwrap()
-                .read(cx)
-                .workspace
-                .upgrade()
-                .unwrap();
-
-            let weak_workspace = workspace.read(cx).weak_handle();
-
-            let current_selection = self.matches[self.current_selection].string.clone();
-
-            if let Some(buffer) = &self.buffer
-                && let Some(buffer) = buffer.upgrade()
-            {
-                let path = self
-                    .selector
-                    .upgrade()
-                    .unwrap()
-                    .read(cx)
-                    .path
-                    .clone()
-                    .unwrap();
-
-                let reload = buffer.update(cx, |buffer, cx| buffer.reload(cx));
-
-                buffer.update(cx, |buffer, _| {
-                    buffer.update_encoding(encoding_from_name(&current_selection).into())
-                });
-
-                self.dismissed(window, cx);
-
-                if self.action == Action::Reopen {
-                    buffer.update(cx, |_, cx| {
-                        cx.spawn_in(window, async move |_, cx| {
-                            if let Err(_) | Ok(None) = reload.await {
-                                let workspace = weak_workspace.upgrade().unwrap();
-
-                                workspace
-                                    .update_in(cx, |workspace, window, cx| {
-                                        workspace
-                                            .encoding_options
-                                            .encoding
-                                            .set(encoding_from_name(&current_selection));
-
-                                        *workspace.encoding_options.force.get_mut() = false;
-
-                                        *workspace.encoding_options.detect_utf16.get_mut() = true;
-
-                                        workspace
-                                            .active_pane()
-                                            .update(cx, |pane, cx| {
-                                                pane.close_active_item(
-                                                    &CloseActiveItem::default(),
-                                                    window,
-                                                    cx,
-                                                )
-                                            })
-                                            .detach();
-
-                                        workspace
-                                            .open_abs_path(path, OpenOptions::default(), window, cx)
-                                            .detach()
-                                    })
-                                    .log_err();
-                            }
-                        })
-                        .detach()
-                    });
-                } else if self.action == Action::Save {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace
-                            .save_active_item(workspace::SaveIntent::Save, window, cx)
-                            .detach();
-                    });
-                }
+                    .collect();
             } else {
-                if let Some(path) = self.selector.upgrade().unwrap().read(cx).path.clone() {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace.active_pane().update(cx, |pane, cx| {
-                            pane.close_active_item(&CloseActiveItem::default(), window, cx)
-                                .detach();
-                        });
-                    });
-
-                    let encoding =
-                        encoding_from_name(self.matches[self.current_selection].string.as_str());
-
-                    let open_task = workspace.update(cx, |workspace, cx| {
-                        workspace.encoding_options.encoding.set(encoding);
-
-                        workspace.open_abs_path(path, OpenOptions::default(), window, cx)
-                    });
-
-                    cx.spawn(async move |_, cx| {
-                        if let Ok(_) = {
-                            let result = open_task.await;
-                            workspace
-                                .update(cx, |workspace, _| {
-                                    *workspace.encoding_options.force.get_mut() = false;
-                                })
-                                .unwrap();
-
-                            result
-                        } && let Ok(Ok((_, buffer, _))) =
-                            workspace.read_with(cx, |workspace, cx| {
-                                if let Some(active_item) = workspace.active_item(cx)
-                                    && let Some(editor) = active_item.act_as::<Editor>(cx)
-                                {
-                                    Ok(editor.read(cx).active_excerpt(cx).unwrap())
-                                } else {
-                                    Err(anyhow!("error"))
-                                }
-                            })
-                        {
-                            buffer
-                                .update(cx, |buffer, _| buffer.update_encoding(encoding.into()))
-                                .log_err();
-                        }
-                    })
-                    .detach();
-                }
+                matches = fuzzy::match_strings(
+                    &encodings,
+                    &query,
+                    true,
+                    false,
+                    30,
+                    &AtomicBool::new(false),
+                    executor,
+                )
+                .await
             }
-        }
-
-        fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-            self.selector
-                .update(cx, |_, cx| cx.emit(DismissEvent))
+            picker
+                .update(cx, |picker, cx| {
+                    let delegate = &mut picker.delegate;
+                    delegate.matches = matches;
+                    delegate.current_selection = delegate
+                        .current_selection
+                        .min(delegate.matches.len().saturating_sub(1));
+                    cx.notify();
+                })
                 .log_err();
-        }
-
-        fn render_match(
-            &self,
-            ix: usize,
-            _: bool,
-            _: &mut Window,
-            _: &mut Context<Picker<Self>>,
-        ) -> Option<Self::ListItem> {
-            Some(
-                ListItem::new(ix)
-                    .child(HighlightedLabel::new(
-                        &self.matches[ix].string,
-                        self.matches[ix].positions.clone(),
-                    ))
-                    .spacing(ListItemSpacing::Sparse),
-            )
-        }
+        })
     }
 
-    /// The action to perform after selecting an encoding.
-    #[derive(PartialEq, Clone)]
-    pub enum Action {
-        Save,
-        Reopen,
-    }
-
-    impl EncodingSelector {
-        pub fn new(
-            window: &mut Window,
-            cx: &mut Context<EncodingSelector>,
-            action: Action,
-            buffer: Option<WeakEntity<Buffer>>,
-            workspace: WeakEntity<Workspace>,
-            path: Option<PathBuf>,
-        ) -> EncodingSelector {
-            let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action);
-            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
-            EncodingSelector {
-                picker,
-                workspace,
-                path,
-            }
+    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        let current_selection = self.matches[self.current_selection].string.clone();
+        let encoding = Encoding::from_name(&current_selection);
+        if let Some(tx) = self.tx.take() {
+            tx.send(encoding).log_err();
         }
+        self.dismissed(window, cx);
     }
 
-    impl EventEmitter<DismissEvent> for EncodingSelector {}
-
-    impl Focusable for EncodingSelector {
-        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
-            self.picker.focus_handle(cx)
-        }
+    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+        cx.emit(DismissEvent);
     }
 
-    impl ModalView for EncodingSelector {}
-
-    impl Render for EncodingSelector {
-        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
-            v_flex().w(rems(34.0)).child(self.picker.clone())
-        }
+    fn render_match(
+        &self,
+        ix: usize,
+        _: bool,
+        _: &mut Window,
+        _: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        Some(
+            ListItem::new(ix)
+                .child(HighlightedLabel::new(
+                    &self.matches[ix].string,
+                    self.matches[ix].positions.clone(),
+                ))
+                .spacing(ListItemSpacing::Sparse),
+        )
     }
 }

crates/fs/src/fs.rs πŸ”—

@@ -58,7 +58,7 @@ use smol::io::AsyncReadExt;
 #[cfg(any(test, feature = "test-support"))]
 use std::ffi::OsStr;
 
-use encodings::{Encoding, EncodingOptions, from_utf8, to_utf8};
+use encodings::{Encoding, EncodingOptions};
 #[cfg(any(test, feature = "test-support"))]
 pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK};
 
@@ -121,9 +121,9 @@ pub trait Fs: Send + Sync {
         &self,
         path: &Path,
         options: &EncodingOptions,
-        buffer_encoding: Option<Arc<Encoding>>,
-    ) -> Result<String> {
-        Ok(to_utf8(self.load_bytes(path).await?, options, buffer_encoding).await?)
+    ) -> Result<(Encoding, String)> {
+        let bytes = self.load_bytes(path).await?;
+        options.process(bytes)
     }
 
     async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
@@ -689,21 +689,12 @@ impl Fs for RealFs {
         let file = smol::fs::File::create(path).await?;
         let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
 
-        // BOM for UTF-16 is written at the start of the file here because
-        // if BOM is written in the `encode` function of `fs::encodings`, it would be written
-        // twice. Hence, it is written only here.
-        if encoding.get() == encodings::UTF_16BE {
-            // Write BOM for UTF-16BE
-            writer.write_all(&[0xFE, 0xFF]).await?;
-        } else if encoding.get() == encodings::UTF_16LE {
-            // Write BOM for UTF-16LE
-            writer.write_all(&[0xFF, 0xFE]).await?;
+        if let Some(bom) = encoding.bom() {
+            writer.write_all(bom).await?;
         }
 
         for chunk in chunks(text, line_ending) {
-            writer
-                .write_all(&from_utf8(chunk.to_string(), Encoding::new(encoding.get())).await?)
-                .await?
+            writer.write_all(&encoding.encode_chunk(chunk)?).await?
         }
 
         writer.flush().await?;
@@ -2424,15 +2415,18 @@ impl Fs for FakeFs {
         line_ending: LineEnding,
         encoding: Encoding,
     ) -> Result<()> {
-        use encodings::from_utf8;
-
         self.simulate_random_delay().await;
         let path = normalize_path(path);
         let content = chunks(text, line_ending).collect::<String>();
         if let Some(path) = path.parent() {
             self.create_dir(path).await?;
         }
-        self.write_file_internal(path, from_utf8(content, encoding).await?, false)?;
+        let mut bytes = Vec::new();
+        if let Some(bom) = encoding.bom() {
+            bytes.extend_from_slice(bom);
+        }
+        bytes.extend_from_slice(&encoding.encode_chunk(&content)?);
+        self.write_file_internal(path, bytes, false)?;
         Ok(())
     }
 

crates/language/src/buffer.rs πŸ”—

@@ -127,7 +127,7 @@ pub struct Buffer {
     has_unsaved_edits: Cell<(clock::Global, bool)>,
     change_bits: Vec<rc::Weak<Cell<bool>>>,
     _subscriptions: Vec<gpui::Subscription>,
-    pub encoding: Arc<Encoding>,
+    encoding: Encoding,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -332,6 +332,8 @@ pub enum BufferEvent {
     DiagnosticsUpdated,
     /// The buffer gained or lost editing capabilities.
     CapabilityChanged,
+    /// The buffer's encoding was changed.
+    EncodingChanged,
 }
 
 /// The file associated with a buffer.
@@ -418,12 +420,7 @@ pub trait LocalFile: File {
     fn abs_path(&self, cx: &App) -> PathBuf;
 
     /// Loads the file contents from disk and returns them as a UTF-8 encoded string.
-    fn load(
-        &self,
-        cx: &App,
-        options: &EncodingOptions,
-        buffer_encoding: Option<Arc<Encoding>>,
-    ) -> Task<Result<String>>;
+    fn load(&self, cx: &App, options: EncodingOptions) -> Task<Result<(Encoding, String)>>;
 
     /// Loads the file's contents from disk.
     fn load_bytes(&self, cx: &App) -> Task<Result<Vec<u8>>>;
@@ -1029,7 +1026,7 @@ impl Buffer {
             has_conflict: false,
             change_bits: Default::default(),
             _subscriptions: Vec::new(),
-            encoding: Arc::new(Encoding::new(encodings::UTF_8)),
+            encoding: Encoding::default(),
         }
     }
 
@@ -1365,11 +1362,6 @@ impl Buffer {
     /// Reloads the contents of the buffer from disk.
     pub fn reload(&mut self, cx: &Context<Self>) -> oneshot::Receiver<Option<Transaction>> {
         let (tx, rx) = futures::channel::oneshot::channel();
-        let encoding = (*self.encoding).clone();
-
-        let buffer_encoding = self.encoding.clone();
-        let options = EncodingOptions::default();
-        options.encoding.set(encoding.get());
 
         let prev_version = self.text.version();
         self.reload_task = Some(cx.spawn(async move |this, cx| {
@@ -1377,14 +1369,20 @@ impl Buffer {
                 let file = this.file.as_ref()?.as_local()?;
 
                 Some((file.disk_state().mtime(), {
-                    file.load(cx, &options, Some(buffer_encoding))
+                    file.load(
+                        cx,
+                        EncodingOptions {
+                            expected: this.encoding,
+                            auto_detect: false,
+                        },
+                    )
                 }))
             })?
             else {
                 return Ok(());
             };
 
-            let new_text = new_text.await?;
+            let (new_encoding, new_text) = new_text.await?;
             let diff = this
                 .update(cx, |this, cx| this.diff(new_text.clone(), cx))?
                 .await;
@@ -1394,6 +1392,9 @@ impl Buffer {
                     this.apply_diff(diff, cx);
                     tx.send(this.finalize_last_transaction().cloned()).ok();
                     this.has_conflict = false;
+                    if new_encoding != this.encoding {
+                        this.set_encoding(new_encoding, cx);
+                    }
                     this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
                 } else {
                     if !diff.edits.is_empty()
@@ -2935,9 +2936,14 @@ impl Buffer {
         !self.has_edits_since(&self.preview_version)
     }
 
+    pub fn encoding(&self) -> Encoding {
+        self.encoding
+    }
+
     /// Update the buffer
-    pub fn update_encoding(&mut self, encoding: Encoding) {
-        self.encoding.set(encoding.get());
+    pub fn set_encoding(&mut self, encoding: Encoding, cx: &mut Context<Self>) {
+        self.encoding = encoding;
+        cx.emit(BufferEvent::EncodingChanged);
     }
 }
 
@@ -5260,12 +5266,7 @@ impl LocalFile for TestFile {
             .join(self.path.as_std_path())
     }
 
-    fn load(
-        &self,
-        _cx: &App,
-        _options: &EncodingOptions,
-        _buffer_encoding: Option<Arc<Encoding>>,
-    ) -> Task<Result<String>> {
+    fn load(&self, _cx: &App, _options: EncodingOptions) -> Task<Result<(Encoding, String)>> {
         unimplemented!()
     }
 

crates/languages/src/json.rs πŸ”—

@@ -57,7 +57,7 @@ impl ContextProvider for JsonTaskProvider {
             let contents = file
                 .worktree
                 .update(cx, |this, cx| {
-                    this.load_file(&file.path, &Default::default(), None, cx)
+                    this.load_file(&file.path, &Default::default(), cx)
                 })
                 .ok()?
                 .await

crates/multi_buffer/src/multi_buffer.rs πŸ”—

@@ -1730,7 +1730,9 @@ impl MultiBuffer {
                 self.capability = buffer.read(cx).capability();
                 return;
             }
-            BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return,
+            BufferEvent::Operation { .. }
+            | BufferEvent::ReloadNeeded
+            | BufferEvent::EncodingChanged => return,
         });
     }
 

crates/project/src/buffer_store.rs πŸ”—

@@ -389,7 +389,7 @@ impl LocalBufferStore {
         let version = buffer.version();
         let buffer_id = buffer.remote_id();
         let file = buffer.file().cloned();
-        let encoding = buffer.encoding.clone();
+        let encoding = buffer.encoding().clone();
 
         if file
             .as_ref()
@@ -399,7 +399,7 @@ impl LocalBufferStore {
         }
 
         let save = worktree.update(cx, |worktree, cx| {
-            worktree.write_file(path.clone(), text, line_ending, (*encoding).clone(), cx)
+            worktree.write_file(path.clone(), text, line_ending, encoding, cx)
         });
 
         cx.spawn(async move |this, cx| {
@@ -528,7 +528,6 @@ impl LocalBufferStore {
                     path: entry.path.clone(),
                     worktree: worktree.clone(),
                     is_private: entry.is_private,
-                    encoding: None,
                 }
             } else {
                 File {
@@ -538,7 +537,6 @@ impl LocalBufferStore {
                     path: old_file.path.clone(),
                     worktree: worktree.clone(),
                     is_private: old_file.is_private,
-                    encoding: None,
                 }
             };
 
@@ -633,20 +631,19 @@ impl LocalBufferStore {
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<Entity<Buffer>>> {
         let options = options.clone();
-        let encoding = options.encoding.clone();
 
         let load_buffer = worktree.update(cx, |worktree, cx| {
             let reservation = cx.reserve_entity();
             let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
 
-            let load_file_task = worktree.load_file(path.as_ref(), &options, None, cx);
+            let load_file_task = worktree.load_file(path.as_ref(), &options, cx);
 
             cx.spawn(async move |_, cx| {
                 let loaded_file = load_file_task.await?;
                 let background_executor = cx.background_executor().clone();
 
-                let buffer = cx.insert_entity(reservation, |_| {
-                    Buffer::build(
+                let buffer = cx.insert_entity(reservation, |cx| {
+                    let mut buffer = Buffer::build(
                         text::Buffer::new(
                             ReplicaId::LOCAL,
                             buffer_id,
@@ -655,7 +652,9 @@ impl LocalBufferStore {
                         ),
                         Some(loaded_file.file),
                         Capability::ReadWrite,
-                    )
+                    );
+                    buffer.set_encoding(loaded_file.encoding, cx);
+                    buffer
                 })?;
 
                 Ok(buffer)
@@ -682,7 +681,6 @@ impl LocalBufferStore {
                             entry_id: None,
                             is_local: true,
                             is_private: false,
-                            encoding: Some(encoding.clone()),
                         })),
                         Capability::ReadWrite,
                     )
@@ -710,10 +708,6 @@ impl LocalBufferStore {
                 anyhow::Ok(())
             })??;
 
-            buffer.update(cx, |buffer, _| {
-                buffer.update_encoding(encoding.get().into())
-            })?;
-
             Ok(buffer)
         })
     }

crates/project/src/image_store.rs πŸ”—

@@ -605,7 +605,6 @@ impl LocalImageStore {
                     path: entry.path.clone(),
                     worktree: worktree.clone(),
                     is_private: entry.is_private,
-                    encoding: None,
                 }
             } else {
                 worktree::File {
@@ -615,7 +614,6 @@ impl LocalImageStore {
                     path: old_file.path.clone(),
                     worktree: worktree.clone(),
                     is_private: old_file.is_private,
-                    encoding: None,
                 }
             };
 

crates/project/src/invalid_item_view.rs πŸ”—

@@ -1,122 +0,0 @@
-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, div, h_flex, v_flex,
-};
-use zed_actions::workspace::OpenWithSystem;
-
-use crate::Item;
-
-/// A view to display when a certain buffer fails to open.
-#[derive(Debug)]
-pub struct InvalidItemView {
-    /// Which path was attempted to open.
-    pub abs_path: Arc<Path>,
-    /// An error message, happened when opening the buffer.
-    pub error: SharedString,
-    is_local: bool,
-    focus_handle: FocusHandle,
-}
-
-impl InvalidItemView {
-    pub fn new(
-        abs_path: &Path,
-        is_local: bool,
-        e: &anyhow::Error,
-        _: &mut Window,
-        cx: &mut App,
-    ) -> Self {
-        Self {
-            is_local,
-            abs_path: Arc::from(abs_path),
-            error: format!("{}", e.root_cause()).into(),
-            focus_handle: cx.focus_handle(),
-        }
-    }
-}
-
-impl Item for InvalidItemView {
-    type Event = ();
-
-    fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString {
-        // Ensure we always render at least the filename.
-        detail += 1;
-
-        let path = self.abs_path.as_ref();
-
-        let mut prefix = path;
-        while detail > 0 {
-            if let Some(parent) = prefix.parent() {
-                prefix = parent;
-                detail -= 1;
-            } else {
-                break;
-            }
-        }
-
-        let path = if detail > 0 {
-            path
-        } else {
-            path.strip_prefix(prefix).unwrap_or(path)
-        };
-
-        SharedString::new(path.to_string_lossy())
-    }
-}
-
-impl EventEmitter<()> for InvalidItemView {}
-
-impl Focusable for InvalidItemView {
-    fn focus_handle(&self, _: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Render for InvalidItemView {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
-        let abs_path = self.abs_path.clone();
-
-        v_flex()
-            .size_full()
-            .track_focus(&self.focus_handle(cx))
-            .flex_none()
-            .justify_center()
-            .overflow_hidden()
-            .key_context("InvalidBuffer")
-            .child(
-                h_flex().size_full().justify_center().items_center().child(
-                    v_flex()
-                        .gap_2()
-                        .max_w_96()
-                        .child(h_flex().justify_center().child("Could not open file"))
-                        .child(
-                            h_flex().justify_center().child(
-                                div()
-                                    .whitespace_normal()
-                                    .text_center()
-                                    .child(Label::new(self.error.clone()).size(LabelSize::Small)),
-                            ),
-                        )
-                        .when(self.is_local, |contents| {
-                            contents.child(
-                                h_flex().justify_center().child(
-                                    Button::new("open-with-system", "Open in Default App")
-                                        .on_click(move |_, _, cx| {
-                                            cx.open_with_system(&abs_path);
-                                        })
-                                        .style(ButtonStyle::Outlined)
-                                        .key_binding(KeyBinding::for_action(
-                                            &OpenWithSystem,
-                                            window,
-                                            cx,
-                                        )),
-                                ),
-                            )
-                        }),
-                ),
-            )
-    }
-}

crates/project/src/project.rs πŸ”—

@@ -27,7 +27,7 @@ mod environment;
 use buffer_diff::BufferDiff;
 use context_server_store::ContextServerStore;
 
-use encodings::{Encoding, EncodingOptions};
+use encodings::Encoding;
 pub use environment::ProjectEnvironmentEvent;
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
@@ -218,7 +218,6 @@ pub struct Project {
     settings_observer: Entity<SettingsObserver>,
     toolchain_store: Option<Entity<ToolchainStore>>,
     agent_location: Option<AgentLocation>,
-    pub encoding_options: EncodingOptions,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -1229,7 +1228,6 @@ impl Project {
                 toolchain_store: Some(toolchain_store),
 
                 agent_location: None,
-                encoding_options: EncodingOptions::default(),
             }
         })
     }
@@ -1415,7 +1413,6 @@ impl Project {
 
                 toolchain_store: Some(toolchain_store),
                 agent_location: None,
-                encoding_options: EncodingOptions::default(),
             };
 
             // remote server -> local machine handlers
@@ -1669,7 +1666,6 @@ impl Project {
                 remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())),
                 toolchain_store: None,
                 agent_location: None,
-                encoding_options: EncodingOptions::default(),
             };
 
             project.set_role(role, cx);
@@ -2720,7 +2716,7 @@ impl Project {
         }
 
         self.buffer_store.update(cx, |buffer_store, cx| {
-            buffer_store.open_buffer(path.into(), &self.encoding_options, cx)
+            buffer_store.open_buffer(path.into(), &Default::default(), cx)
         })
     }
 
@@ -5403,7 +5399,7 @@ impl Project {
         cx.spawn(async move |cx| {
             let file = worktree
                 .update(cx, |worktree, cx| {
-                    worktree.load_file(&rel_path, &Default::default(), None, cx)
+                    worktree.load_file(&rel_path, &Default::default(), cx)
                 })?
                 .await
                 .context("Failed to load settings file")?;

crates/project/src/project_tests.rs πŸ”—

@@ -12,7 +12,7 @@ use buffer_diff::{
     BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
     DiffHunkStatusKind, assert_hunks,
 };
-use encodings::{Encoding, UTF_8};
+use encodings::Encoding;
 use fs::FakeFs;
 use futures::{StreamExt, future};
 use git::{
@@ -3878,12 +3878,7 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         worktree
             .update(cx, |worktree, cx| {
-                worktree.load_file(
-                    rel_path("dir1/dir2/dir3/test.txt"),
-                    &Default::default(),
-                    None,
-                    cx,
-                )
+                worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), &Default::default(), cx)
             })
             .await
             .unwrap()
@@ -3930,12 +3925,7 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         worktree
             .update(cx, |worktree, cx| {
-                worktree.load_file(
-                    rel_path("dir1/dir2/test.txt"),
-                    &Default::default(),
-                    None,
-                    cx,
-                )
+                worktree.load_file(rel_path("dir1/dir2/test.txt"), &Default::default(), cx)
             })
             .await
             .unwrap()
@@ -4085,15 +4075,13 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
     // the next file change occurs.
     cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
 
-    let encoding = Encoding::default();
-
     // Change the buffer's file on disk, and then wait for the file change
     // to be detected by the worktree, so that the buffer starts reloading.
     fs.save(
         path!("/dir/file1").as_ref(),
         &Rope::from_str("the first contents", cx.background_executor()),
         Default::default(),
-        encoding.clone(),
+        Default::default(),
     )
     .await
     .unwrap();
@@ -4105,7 +4093,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
         path!("/dir/file1").as_ref(),
         &Rope::from_str("the second contents", cx.background_executor()),
         Default::default(),
-        encoding,
+        Default::default(),
     )
     .await
     .unwrap();
@@ -4144,15 +4132,13 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
     // the next file change occurs.
     cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
 
-    let encoding = Encoding::new(UTF_8);
-
     // Change the buffer's file on disk, and then wait for the file change
     // to be detected by the worktree, so that the buffer starts reloading.
     fs.save(
         path!("/dir/file1").as_ref(),
         &Rope::from_str("the first contents", cx.background_executor()),
         Default::default(),
-        encoding,
+        Default::default(),
     )
     .await
     .unwrap();
@@ -4828,13 +4814,11 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
     let (new_contents, new_offsets) =
         marked_text_offsets("oneˇ\nthree ˇFOURˇ five\nsixtyˇ seven\n");
 
-    let encoding = Encoding::new(UTF_8);
-
     fs.save(
         path!("/dir/the-file").as_ref(),
         &Rope::from_str(new_contents.as_str(), cx.background_executor()),
         LineEnding::Unix,
-        encoding,
+        Default::default(),
     )
     .await
     .unwrap();
@@ -4862,14 +4846,12 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
         assert!(!buffer.has_conflict());
     });
 
-    let encoding = Encoding::new(UTF_8);
-
     // Change the file on disk again, adding blank lines to the beginning.
     fs.save(
         path!("/dir/the-file").as_ref(),
         &Rope::from_str("\n\n\nAAAA\naaa\nBB\nbbbbb\n", cx.background_executor()),
         LineEnding::Unix,
-        encoding,
+        Default::default(),
     )
     .await
     .unwrap();
@@ -4916,15 +4898,13 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
         assert_eq!(buffer.line_ending(), LineEnding::Windows);
     });
 
-    let encoding = Encoding::new(UTF_8);
-
     // Change a file's line endings on disk from unix to windows. The buffer's
     // state updates correctly.
     fs.save(
         path!("/dir/file1").as_ref(),
         &Rope::from_str("aaa\nb\nc\n", cx.background_executor()),
         LineEnding::Windows,
-        encoding,
+        Default::default(),
     )
     .await
     .unwrap();
@@ -9016,7 +8996,6 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
         tree.load_file(
             rel_path("project/target/debug/important_text.txt"),
             &Default::default(),
-            None,
             cx,
         )
     })
@@ -9179,12 +9158,7 @@ async fn test_odd_events_for_ignored_dirs(
 
     let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
     tree.update(cx, |tree, cx| {
-        tree.load_file(
-            rel_path("target/debug/foo.txt"),
-            &Default::default(),
-            None,
-            cx,
-        )
+        tree.load_file(rel_path("target/debug/foo.txt"), &Default::default(), cx)
     })
     .await
     .unwrap();

crates/workspace/src/invalid_item_view.rs πŸ”—

@@ -1,5 +1,4 @@
 use std::{path::Path, sync::Arc};
-use ui::TintColor;
 
 use gpui::{EventEmitter, FocusHandle, Focusable, div};
 use ui::{
@@ -79,7 +78,6 @@ impl Render for InvalidItemView {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
         let abs_path = self.abs_path.clone();
         let path0 = self.abs_path.clone();
-        let path1 = self.abs_path.clone();
 
         v_flex()
             .size_full()
@@ -118,46 +116,25 @@ impl Render for InvalidItemView {
                                     ),
                                 )
                                 .child(
-                                    h_flex()
-                                        .justify_center()
-                                        .child(
-                                            Button::new(
-                                                "open-with-encoding",
-                                                "Open With a Different Encoding",
-                                            )
-                                            .style(ButtonStyle::Outlined)
-                                            .on_click(
-                                                move |_, window, cx| {
-                                                    window.dispatch_action(
-                                                        Box::new(
-                                                            zed_actions::encodings_ui::Toggle(
-                                                                path0.clone(),
-                                                            ),
-                                                        ),
-                                                        cx,
-                                                    )
-                                                },
-                                            ),
+                                    h_flex().justify_center().child(
+                                        Button::new(
+                                            "open-with-encoding",
+                                            "Try a Different Encoding",
                                         )
-                                        .child(
-                                            Button::new(
-                                                "accept-risk-and-open",
-                                                "Accept the Risk and Open",
-                                            )
-                                            .style(ButtonStyle::Tinted(TintColor::Warning))
-                                            .on_click(
-                                                move |_, window, cx| {
-                                                    window.dispatch_action(
-                                                        Box::new(
-                                                            zed_actions::encodings_ui::ForceOpen(
-                                                                path1.clone(),
-                                                            ),
+                                        .style(ButtonStyle::Outlined)
+                                        .on_click(
+                                            move |_, window, cx| {
+                                                window.dispatch_action(
+                                                    Box::new(
+                                                        zed_actions::encodings_ui::OpenWithEncoding(
+                                                            path0.clone(),
                                                         ),
-                                                        cx,
-                                                    );
-                                                },
-                                            ),
+                                                    ),
+                                                    cx,
+                                                )
+                                            },
                                         ),
+                                    ),
                                 )
                         }),
                 ),

crates/workspace/src/workspace.rs πŸ”—

@@ -19,7 +19,6 @@ mod workspace_settings;
 
 pub use crate::notifications::NotificationFrame;
 pub use dock::Panel;
-use encodings::Encoding;
 
 pub use path_list::PathList;
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
@@ -32,7 +31,6 @@ use client::{
 };
 use collections::{HashMap, HashSet, hash_map};
 use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
-use encodings::EncodingOptions;
 
 use futures::{
     Future, FutureExt, StreamExt,
@@ -625,7 +623,6 @@ type BuildProjectItemForPathFn =
     fn(
         &Entity<Project>,
         &ProjectPath,
-        Option<Encoding>,
         &mut Window,
         &mut App,
     ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
@@ -647,11 +644,8 @@ impl ProjectItemRegistry {
             },
         );
         self.build_project_item_for_path_fns
-            .push(|project, project_path, encoding, window, cx| {
+            .push(|project, project_path, window, cx| {
                 let project_path = project_path.clone();
-                let encoding = encoding.unwrap_or_default();
-
-                project.update(cx, |project, _| project.encoding_options.encoding.set(encoding.get()));
 
                 let is_file = project
                     .read(cx)
@@ -721,17 +715,14 @@ impl ProjectItemRegistry {
         &self,
         project: &Entity<Project>,
         path: &ProjectPath,
-        encoding: Option<Encoding>,
         window: &mut Window,
         cx: &mut App,
     ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
-        let Some(open_project_item) =
-            self.build_project_item_for_path_fns
-                .iter()
-                .rev()
-                .find_map(|open_project_item| {
-                    open_project_item(project, path, encoding.clone(), window, cx)
-                })
+        let Some(open_project_item) = self
+            .build_project_item_for_path_fns
+            .iter()
+            .rev()
+            .find_map(|open_project_item| open_project_item(project, path, window, cx))
         else {
             return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
         };
@@ -1191,7 +1182,6 @@ pub struct Workspace {
     session_id: Option<String>,
     scheduled_tasks: Vec<Task<()>>,
     last_open_dock_positions: Vec<DockPosition>,
-    pub encoding_options: EncodingOptions,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -1534,7 +1524,6 @@ impl Workspace {
             session_id: Some(session_id),
             scheduled_tasks: Vec::new(),
             last_open_dock_positions: Vec::new(),
-            encoding_options: Default::default(),
         }
     }
 
@@ -1945,10 +1934,6 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Task<Result<()>> {
-        // This is done so that we would get an error when we try to open the file with wrong encoding,
-        // and not silently use the previously set encoding.
-        self.encoding_options.reset();
-
         let to_load = if let Some(pane) = pane.upgrade() {
             pane.update(cx, |pane, cx| {
                 window.focus(&pane.focus_handle(cx));
@@ -3578,25 +3563,8 @@ impl Workspace {
         window: &mut Window,
         cx: &mut App,
     ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
-        let project = self.project();
-
-        project.update(cx, |project, _| {
-            project.encoding_options.force.store(
-                self.encoding_options
-                    .force
-                    .load(std::sync::atomic::Ordering::Acquire),
-                std::sync::atomic::Ordering::Release,
-            );
-        });
-
         let registry = cx.default_global::<ProjectItemRegistry>().clone();
-        registry.open_path(
-            project,
-            &path,
-            Some((*self.encoding_options.encoding).clone()),
-            window,
-            cx,
-        )
+        registry.open_path(&self.project, &path, window, cx)
     }
 
     pub fn find_project_item<T>(
@@ -7623,8 +7591,6 @@ pub fn create_and_open_local_file(
             fs.save(
                 path,
                 &default_content(cx),
-
-
                 Default::default(),
                 Default::default(),
             )

crates/worktree/src/worktree.rs πŸ”—

@@ -100,6 +100,7 @@ pub enum CreatedEntry {
 
 pub struct LoadedFile {
     pub file: Arc<File>,
+    pub encoding: Encoding,
     pub text: String,
 }
 
@@ -708,11 +709,10 @@ impl Worktree {
         &self,
         path: &RelPath,
         options: &EncodingOptions,
-        buffer_encoding: Option<Arc<Encoding>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<LoadedFile>> {
         match self {
-            Worktree::Local(this) => this.load_file(path, options, buffer_encoding, cx),
+            Worktree::Local(this) => this.load_file(path, options, cx),
             Worktree::Remote(_) => {
                 Task::ready(Err(anyhow!("remote worktrees can't yet load files")))
             }
@@ -741,7 +741,7 @@ impl Worktree {
         cx: &Context<Worktree>,
     ) -> Task<Result<Arc<File>>> {
         match self {
-            Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding),
+            Worktree::Local(this) => this.write_file(path, text, line_ending, encoding, cx),
             Worktree::Remote(_) => {
                 Task::ready(Err(anyhow!("remote worktree can't yet write files")))
             }
@@ -1311,7 +1311,6 @@ impl LocalWorktree {
                         },
                         is_local: true,
                         is_private,
-                        encoding: None,
                     })
                 }
             };
@@ -1324,7 +1323,6 @@ impl LocalWorktree {
         &self,
         path: &RelPath,
         options: &EncodingOptions,
-        buffer_encoding: Option<Arc<Encoding>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<LoadedFile>> {
         let path = Arc::from(path);
@@ -1333,7 +1331,6 @@ impl LocalWorktree {
         let entry = self.refresh_entry(path.clone(), None, cx);
         let is_private = self.is_path_private(path.as_ref());
         let options = options.clone();
-        let encoding = options.encoding.clone();
 
         let this = cx.weak_entity();
         cx.background_spawn(async move {
@@ -1351,9 +1348,7 @@ impl LocalWorktree {
                     anyhow::bail!("File is too large to load");
                 }
             }
-            let text = fs
-                .load_with_encoding(&abs_path, &options, buffer_encoding.clone())
-                .await?;
+            let (encoding, text) = fs.load_with_encoding(&abs_path, &options).await?;
 
             let worktree = this.upgrade().context("worktree was dropped")?;
             let file = match entry.await? {
@@ -1377,12 +1372,15 @@ impl LocalWorktree {
                         },
                         is_local: true,
                         is_private,
-                        encoding: Some(encoding),
                     })
                 }
             };
 
-            Ok(LoadedFile { file, text })
+            Ok(LoadedFile {
+                file,
+                encoding,
+                text,
+            })
         })
     }
 
@@ -1465,8 +1463,8 @@ impl LocalWorktree {
         path: Arc<RelPath>,
         text: Rope,
         line_ending: LineEnding,
-        cx: &Context<Worktree>,
         encoding: Encoding,
+        cx: &Context<Worktree>,
     ) -> Task<Result<Arc<File>>> {
         let fs = self.fs.clone();
         let is_private = self.is_path_private(&path);
@@ -1512,7 +1510,6 @@ impl LocalWorktree {
                     entry_id: None,
                     is_local: true,
                     is_private,
-                    encoding: Some(Arc::new(encoding)),
                 }))
             }
         })
@@ -3066,7 +3063,7 @@ impl fmt::Debug for Snapshot {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct File {
     pub worktree: Entity<Worktree>,
     pub path: Arc<RelPath>,
@@ -3074,35 +3071,8 @@ pub struct File {
     pub entry_id: Option<ProjectEntryId>,
     pub is_local: bool,
     pub is_private: bool,
-    pub encoding: Option<Arc<Encoding>>,
 }
 
-impl PartialEq for File {
-    fn eq(&self, other: &Self) -> bool {
-        if self.worktree == other.worktree
-            && self.path == other.path
-            && self.disk_state == other.disk_state
-            && self.entry_id == other.entry_id
-            && self.is_local == other.is_local
-            && self.is_private == other.is_private
-            && (if let Some(encoding) = &self.encoding
-                && let Some(other_encoding) = &other.encoding
-            {
-                if encoding.get() != other_encoding.get() {
-                    false
-                } else {
-                    true
-                }
-            } else {
-                true
-            })
-        {
-            true
-        } else {
-            false
-        }
-    }
-}
 impl language::File for File {
     fn as_local(&self) -> Option<&dyn language::LocalFile> {
         if self.is_local { Some(self) } else { None }
@@ -3149,13 +3119,6 @@ impl language::File for File {
     fn path_style(&self, cx: &App) -> PathStyle {
         self.worktree.read(cx).path_style()
     }
-    fn encoding(&self) -> Option<Arc<Encoding>> {
-        if let Some(encoding) = &self.encoding {
-            Some(encoding.clone())
-        } else {
-            None
-        }
-    }
 }
 
 impl language::LocalFile for File {
@@ -3163,30 +3126,11 @@ impl language::LocalFile for File {
         self.worktree.read(cx).absolutize(&self.path)
     }
 
-    fn load(
-        &self,
-        cx: &App,
-        options: &EncodingOptions,
-        buffer_encoding: Option<Arc<Encoding>>,
-    ) -> Task<Result<String>> {
+    fn load(&self, cx: &App, encoding: EncodingOptions) -> Task<Result<(Encoding, String)>> {
         let worktree = self.worktree.read(cx).as_local().unwrap();
         let abs_path = worktree.absolutize(&self.path);
         let fs = worktree.fs.clone();
-        let options = EncodingOptions {
-            encoding: options.encoding.clone(),
-            force: std::sync::atomic::AtomicBool::new(
-                options.force.load(std::sync::atomic::Ordering::Acquire),
-            ),
-            detect_utf16: std::sync::atomic::AtomicBool::new(
-                options
-                    .detect_utf16
-                    .load(std::sync::atomic::Ordering::Acquire),
-            ),
-        };
-        cx.background_spawn(async move {
-            fs.load_with_encoding(&abs_path, &options, buffer_encoding)
-                .await
-        })
+        cx.background_spawn(async move { fs.load_with_encoding(&abs_path, &encoding).await })
     }
 
     fn load_bytes(&self, cx: &App) -> Task<Result<Vec<u8>>> {
@@ -3210,7 +3154,6 @@ impl File {
             entry_id: Some(entry.id),
             is_local: true,
             is_private: entry.is_private,
-            encoding: None,
         })
     }
 
@@ -3241,7 +3184,6 @@ impl File {
             entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
             is_local: false,
             is_private: false,
-            encoding: None,
         })
     }
 

crates/worktree/src/worktree_tests.rs πŸ”—

@@ -470,7 +470,6 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
             tree.load_file(
                 rel_path("one/node_modules/b/b1.js"),
                 &Default::default(),
-                None,
                 cx,
             )
         })
@@ -515,7 +514,6 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
             tree.load_file(
                 rel_path("one/node_modules/a/a2.js"),
                 &Default::default(),
-                None,
                 cx,
             )
         })
@@ -1781,8 +1779,8 @@ fn randomly_mutate_worktree(
                     entry.path.clone(),
                     Rope::default(),
                     Default::default(),
-                    cx,
                     Default::default(),
+                    cx,
                 );
                 cx.background_spawn(async move {
                     task.await?;

crates/zed/src/zed.rs πŸ”—

@@ -443,9 +443,8 @@ pub fn initialize_workspace(
             }
         });
 
-        let encoding_indicator = cx.new(|_cx| {
-            encodings_ui::EncodingIndicator::new(None, workspace.weak_handle(), None, None)
-        });
+        let encoding_indicator =
+            cx.new(|_cx| encodings_ui::EncodingIndicator::new(workspace.weak_handle()));
 
         let cursor_position =
             cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));

crates/zed_actions/src/lib.rs πŸ”—

@@ -307,7 +307,7 @@ pub mod encodings_ui {
     use serde::Deserialize;
 
     #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)]
-    pub struct Toggle(pub Arc<std::path::Path>);
+    pub struct OpenWithEncoding(pub Arc<std::path::Path>);
 
     #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)]
     pub struct ForceOpen(pub Arc<std::path::Path>);

crates/zeta/src/zeta.rs πŸ”—

@@ -1986,7 +1986,7 @@ mod tests {
                     .worktree_for_root_name("closed_source_worktree", cx)
                     .unwrap();
                 worktree2.update(cx, |worktree2, cx| {
-                    worktree2.load_file(rel_path("main.rs"), &Default::default(), None, cx)
+                    worktree2.load_file(rel_path("main.rs"), &Default::default(), cx)
                 })
             })
             .await