refactor: `encoding` in `EncodingIndicator` is now an optional trait object

R Aadarsh created

feat: Add all supported encodings, and open the encoding selector when an action(save or reopen) is chosen.

Change summary

..gitignore.swp                   |   0 
Cargo.lock                        |   2 
crates/encodings/Cargo.toml       |   1 
crates/encodings/src/lib.rs       |  63 +++++++---
crates/encodings/src/selectors.rs | 206 +++++++++++++++++++++++++++-----
crates/language/src/buffer.rs     |   2 
crates/zed/Cargo.toml             |   1 
crates/zed/src/zed.rs             |   6 
8 files changed, 221 insertions(+), 60 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5589,6 +5589,7 @@ name = "encodings"
 version = "0.1.0"
 dependencies = [
  "editor",
+ "encoding",
  "fuzzy",
  "gpui",
  "picker",
@@ -21255,6 +21256,7 @@ dependencies = [
  "diagnostics",
  "edit_prediction_button",
  "editor",
+ "encoding",
  "encodings",
  "env_logger 0.11.8",
  "extension",

crates/encodings/Cargo.toml 🔗

@@ -12,6 +12,7 @@ picker.workspace = true
 util.workspace = true
 fuzzy.workspace = true
 editor.workspace = true
+encoding = "0.2.33"
 
 [lints]
 workspace = true

crates/encodings/src/lib.rs 🔗

@@ -1,28 +1,16 @@
 use editor::Editor;
-use gpui::{ClickEvent, Entity, WeakEntity};
+use encoding::Encoding;
+use gpui::{ClickEvent, Entity, Subscription, WeakEntity};
 use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div};
 use ui::{Clickable, ParentElement};
 use workspace::{ItemHandle, StatusItemView, Workspace};
 
 use crate::selectors::save_or_reopen::{EncodingSaveOrReopenSelector, get_current_encoding};
 
-pub enum Encoding {
-    Utf8,
-    Iso8859_1,
-}
-
-impl Encoding {
-    pub fn as_str(&self) -> &str {
-        match &self {
-            Encoding::Utf8 => "UTF-8",
-            Encoding::Iso8859_1 => "ISO 8859-1",
-        }
-    }
-}
-
 pub struct EncodingIndicator {
-    pub encoding: Encoding,
+    pub encoding: Option<&'static dyn Encoding>,
     pub workspace: WeakEntity<Workspace>,
+    observe: Option<Subscription>,
 }
 
 pub mod selectors;
@@ -49,14 +37,51 @@ impl Render for EncodingIndicator {
 
 impl EncodingIndicator {
     pub fn get_current_encoding(&self, cx: &mut Context<Self>, editor: WeakEntity<Editor>) {}
+
+    pub fn new(
+        encoding: Option<&'static dyn encoding::Encoding>,
+        workspace: WeakEntity<Workspace>,
+        observe: Option<Subscription>,
+    ) -> EncodingIndicator {
+        EncodingIndicator {
+            encoding,
+            workspace,
+            observe,
+        }
+    }
+
+    pub fn update(
+        &mut self,
+        editor: Entity<Editor>,
+        _: &mut Window,
+        cx: &mut Context<EncodingIndicator>,
+    ) {
+        let editor = editor.read(cx);
+        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
+            let encoding = buffer.read(cx).encoding;
+            self.encoding = Some(encoding);
+        }
+
+        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>,
+        active_pane_item: Option<&dyn ItemHandle>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
     ) {
+        match active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+            Some(editor) => {
+                self.observe = Some(cx.observe_in(&editor, window, Self::update));
+                self.update(editor, window, cx);
+            }
+            None => {
+                self.encoding = None;
+                self.observe = None;
+            }
+        }
     }
 }

crates/encodings/src/selectors.rs 🔗

@@ -3,31 +3,49 @@ pub mod save_or_reopen {
     use gpui::{AppContext, ParentElement};
     use picker::Picker;
     use picker::PickerDelegate;
+    use std::cell::RefCell;
+    use std::ops::{Deref, DerefMut};
+    use std::rc::Rc;
+    use std::sync::Arc;
     use std::sync::atomic::AtomicBool;
     use util::ResultExt;
 
     use fuzzy::{StringMatch, StringMatchCandidate};
     use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
 
-    use ui::{Context, Label, ListItem, Render, Window, rems, v_flex};
+    use ui::{Context, HighlightedLabel, Label, ListItem, Render, Window, rems, v_flex};
     use workspace::{ModalView, Workspace};
 
+    use crate::selectors::encoding::{Action, EncodingSelector, EncodingSelectorDelegate};
+
     pub struct EncodingSaveOrReopenSelector {
         picker: Entity<Picker<EncodingSaveOrReopenDelegate>>,
+        pub current_selection: usize,
+        workspace: WeakEntity<Workspace>,
     }
 
     impl EncodingSaveOrReopenSelector {
-        pub fn new(window: &mut Window, cx: &mut Context<EncodingSaveOrReopenSelector>) -> Self {
-            let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade());
+        pub fn new(
+            window: &mut Window,
+            cx: &mut Context<EncodingSaveOrReopenSelector>,
+            workspace: WeakEntity<Workspace>,
+        ) -> Self {
+            let delegate =
+                EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace.clone());
 
             let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 
-            Self { picker }
+            Self {
+                picker,
+                current_selection: 0,
+                workspace,
+            }
         }
 
         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)
+                EncodingSaveOrReopenSelector::new(window, cx, weak_workspace)
             });
         }
     }
@@ -53,28 +71,57 @@ pub mod save_or_reopen {
     impl EventEmitter<DismissEvent> for EncodingSaveOrReopenSelector {}
 
     pub struct EncodingSaveOrReopenDelegate {
-        encoding_selector: WeakEntity<EncodingSaveOrReopenSelector>,
+        selector: WeakEntity<EncodingSaveOrReopenSelector>,
         current_selection: usize,
         matches: Vec<StringMatch>,
         pub actions: Vec<StringMatchCandidate>,
+        workspace: WeakEntity<Workspace>,
     }
 
     impl EncodingSaveOrReopenDelegate {
-        pub fn new(selector: WeakEntity<EncodingSaveOrReopenSelector>) -> Self {
+        pub fn new(
+            selector: WeakEntity<EncodingSaveOrReopenSelector>,
+            workspace: WeakEntity<Workspace>,
+        ) -> Self {
             Self {
-                encoding_selector: selector,
+                selector,
                 current_selection: 0,
                 matches: Vec::new(),
                 actions: vec![
                     StringMatchCandidate::new(0, "Save with encoding"),
                     StringMatchCandidate::new(1, "Reopen with encoding"),
                 ],
+                workspace,
             }
         }
 
         pub fn get_actions(&self) -> (&str, &str) {
             (&self.actions[0].string, &self.actions[1].string)
         }
+
+        pub fn post_selection(
+            &self,
+            cx: &mut Context<Picker<EncodingSaveOrReopenDelegate>>,
+            window: &mut Window,
+        ) {
+            if self.current_selection == 0 {
+                if let Some(workspace) = self.workspace.upgrade() {
+                    workspace.update(cx, |workspace, cx| {
+                        workspace.toggle_modal(window, cx, |window, cx| {
+                            EncodingSelector::new(window, cx, Action::Save)
+                        })
+                    });
+                }
+            } else if self.current_selection == 1 {
+                if let Some(workspace) = self.workspace.upgrade() {
+                    workspace.update(cx, |workspace, cx| {
+                        workspace.toggle_modal(window, cx, |window, cx| {
+                            EncodingSelector::new(window, cx, Action::Reopen)
+                        })
+                    });
+                }
+            }
+        }
     }
 
     impl PickerDelegate for EncodingSaveOrReopenDelegate {
@@ -92,9 +139,14 @@ pub mod save_or_reopen {
             &mut self,
             ix: usize,
             _window: &mut Window,
-            _cx: &mut Context<Picker<Self>>,
+            cx: &mut Context<Picker<Self>>,
         ) {
             self.current_selection = ix;
+            self.selector
+                .update(cx, |selector, cx| {
+                    selector.current_selection = ix;
+                })
+                .log_err();
         }
 
         fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
@@ -137,24 +189,31 @@ pub mod save_or_reopen {
 
                 this.update(cx, |picker, cx| {
                     let delegate = &mut picker.delegate;
-                    delegate.current_selection = matches.len().saturating_sub(1);
                     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();
                 })
                 .log_err();
             })
         }
 
-        fn confirm(
-            &mut self,
-            secondary: bool,
-            window: &mut Window,
-            cx: &mut Context<Picker<Self>>,
-        ) {
+        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);
+            }
         }
 
         fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-            self.encoding_selector
+            self.selector
                 .update(cx, |_, cx| cx.emit(DismissEvent))
                 .log_err();
         }
@@ -162,11 +221,18 @@ pub mod save_or_reopen {
         fn render_match(
             &self,
             ix: usize,
-            selected: bool,
-            window: &mut Window,
-            cx: &mut Context<Picker<Self>>,
+            _: bool,
+            _: &mut Window,
+            _: &mut Context<Picker<Self>>,
         ) -> Option<Self::ListItem> {
-            Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string)))
+            Some(
+                ListItem::new(ix)
+                    .child(HighlightedLabel::new(
+                        &self.matches[ix].string,
+                        self.matches[ix].positions.clone(),
+                    ))
+                    .spacing(ui::ListItemSpacing::Sparse),
+            )
         }
     }
 
@@ -176,19 +242,28 @@ pub mod save_or_reopen {
 }
 
 pub mod encoding {
-    use std::sync::atomic::AtomicBool;
+    use std::{
+        ops::DerefMut,
+        rc::{Rc, Weak},
+        sync::{Arc, atomic::AtomicBool},
+    };
 
     use fuzzy::{StringMatch, StringMatchCandidate};
     use gpui::{
-        AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity,
+        AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, Length,
+        WeakEntity, actions,
     };
     use picker::{Picker, PickerDelegate};
-    use ui::{Context, Label, ListItem, ParentElement, Render, Styled, Window, rems, v_flex};
+    use ui::{
+        Context, DefiniteLength, HighlightedLabel, Label, ListItem, ListItemSpacing, ParentElement,
+        Render, Styled, Window, rems, v_flex,
+    };
     use util::{ResultExt, TryFutureExt};
     use workspace::{ModalView, Workspace};
 
     pub struct EncodingSelector {
-        pub picker: Entity<Picker<EncodingSelectorDelegate>>,
+        picker: Entity<Picker<EncodingSelectorDelegate>>,
+        action: Action,
     }
 
     pub struct EncodingSelectorDelegate {
@@ -204,7 +279,44 @@ pub mod encoding {
                 current_selection: 0,
                 encodings: vec![
                     StringMatchCandidate::new(0, "UTF-8"),
-                    StringMatchCandidate::new(1, "ISO 8859-1"),
+                    StringMatchCandidate::new(1, "UTF-16 LE"),
+                    StringMatchCandidate::new(2, "UTF-16 BE"),
+                    StringMatchCandidate::new(3, "IBM866"),
+                    StringMatchCandidate::new(4, "ISO 8859-1"),
+                    StringMatchCandidate::new(5, "ISO 8859-2"),
+                    StringMatchCandidate::new(6, "ISO 8859-3"),
+                    StringMatchCandidate::new(7, "ISO 8859-4"),
+                    StringMatchCandidate::new(8, "ISO 8859-5"),
+                    StringMatchCandidate::new(9, "ISO 8859-6"),
+                    StringMatchCandidate::new(10, "ISO 8859-7"),
+                    StringMatchCandidate::new(11, "ISO 8859-8"),
+                    StringMatchCandidate::new(12, "ISO 8859-10"),
+                    StringMatchCandidate::new(13, "ISO 8859-13"),
+                    StringMatchCandidate::new(14, "ISO 8859-14"),
+                    StringMatchCandidate::new(15, "ISO 8859-15"),
+                    StringMatchCandidate::new(16, "ISO 8859-16"),
+                    StringMatchCandidate::new(17, "KOI8-R"),
+                    StringMatchCandidate::new(18, "KOI8-U"),
+                    StringMatchCandidate::new(19, "MacRoman"),
+                    StringMatchCandidate::new(20, "Mac Cyrillic"),
+                    StringMatchCandidate::new(21, "Windows-874"),
+                    StringMatchCandidate::new(22, "Windows-1250"),
+                    StringMatchCandidate::new(23, "Windows-1251"),
+                    StringMatchCandidate::new(24, "Windows-1252"),
+                    StringMatchCandidate::new(25, "Windows-1253"),
+                    StringMatchCandidate::new(26, "Windows-1254"),
+                    StringMatchCandidate::new(27, "Windows-1255"),
+                    StringMatchCandidate::new(28, "Windows-1256"),
+                    StringMatchCandidate::new(29, "Windows-1257"),
+                    StringMatchCandidate::new(30, "Windows-1258"),
+                    StringMatchCandidate::new(31, "EUC-KR"),
+                    StringMatchCandidate::new(32, "EUC-JP"),
+                    StringMatchCandidate::new(33, "Shift_JIS"),
+                    StringMatchCandidate::new(34, "ISO 2022-JP"),
+                    StringMatchCandidate::new(35, "GBK"),
+                    StringMatchCandidate::new(36, "GB18030"),
+                    StringMatchCandidate::new(37, "Big5"),
+                    StringMatchCandidate::new(38, "HZ-GB-2312"),
                 ],
                 matches: Vec::new(),
                 selector,
@@ -244,7 +356,6 @@ pub mod encoding {
         ) -> gpui::Task<()> {
             let executor = cx.background_executor().clone();
             let encodings = self.encodings.clone();
-            let current_selection = self.current_selection;
 
             cx.spawn_in(window, async move |picker, cx| {
                 let matches: Vec<StringMatch>;
@@ -264,14 +375,25 @@ pub mod encoding {
                     matches = fuzzy::match_strings(
                         &encodings,
                         &query,
+                        true,
                         false,
-                        false,
-                        0,
+                        38,
                         &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();
+                    })
+                    .log_err();
             })
         }
 
@@ -296,20 +418,32 @@ pub mod encoding {
             window: &mut Window,
             cx: &mut Context<Picker<Self>>,
         ) -> Option<Self::ListItem> {
-            Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string)))
+            Some(
+                ListItem::new(ix)
+                    .child(HighlightedLabel::new(
+                        &self.matches[ix].string,
+                        self.matches[ix].positions.clone(),
+                    ))
+                    .spacing(ListItemSpacing::Sparse),
+            )
         }
     }
 
+    pub enum Action {
+        Save,
+        Reopen,
+    }
+
     impl EncodingSelector {
-        pub fn new(window: &mut Window, cx: &mut Context<EncodingSelector>) -> EncodingSelector {
+        pub fn new(
+            window: &mut Window,
+            cx: &mut Context<EncodingSelector>,
+            action: Action,
+        ) -> EncodingSelector {
             let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade());
             let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 
-            EncodingSelector { picker: picker }
-        }
-
-        pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
-            workspace.toggle_modal(window, cx, |window, cx| EncodingSelector::new(window, cx));
+            EncodingSelector { picker, action }
         }
     }
 

crates/language/src/buffer.rs 🔗

@@ -126,7 +126,7 @@ pub struct Buffer {
     has_unsaved_edits: Cell<(clock::Global, bool)>,
     change_bits: Vec<rc::Weak<Cell<bool>>>,
     _subscriptions: Vec<gpui::Subscription>,
-    encoding: &'static dyn encoding::Encoding,
+    pub encoding: &'static dyn encoding::Encoding,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]

crates/zed/Cargo.toml 🔗

@@ -166,6 +166,7 @@ zeta.workspace = true
 zeta2.workspace = true
 zlog.workspace = true
 zlog_settings.workspace = true
+encoding = "0.2.33"
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true

crates/zed/src/zed.rs 🔗

@@ -443,10 +443,8 @@ pub fn initialize_workspace(
             }
         });
 
-        let encoding_indicator = cx.new(|_cx| encodings::EncodingIndicator {
-            encoding: encodings::Encoding::Utf8,
-            workspace: workspace_handle.downgrade(),
-        });
+        let encoding_indicator =
+            cx.new(|_cx| encodings::EncodingIndicator::new(None, workspace.weak_handle(), None));
 
         let cursor_position =
             cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));