Add a status indicator to indicate the current file's encoding. When clicked a modal view opens that lets user choose to either reopen or save a file with a particular encoding. The actual implementations are incomplete

R Aadarsh created

Change summary

Cargo.lock                        |  14 ++
Cargo.toml                        |   3 
crates/encodings/Cargo.toml       |  17 ++
crates/encodings/src/lib.rs       | 210 +++++++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs |   1 
crates/zed/Cargo.toml             |   1 
crates/zed/src/zed.rs             |   3 
7 files changed, 249 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -5520,6 +5520,19 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "encodings"
+version = "0.1.0"
+dependencies = [
+ "fuzzy",
+ "gpui",
+ "language",
+ "picker",
+ "ui",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "endi"
 version = "1.1.0"
@@ -21176,6 +21189,7 @@ dependencies = [
  "diagnostics",
  "edit_prediction_button",
  "editor",
+ "encodings",
  "env_logger 0.11.8",
  "extension",
  "extension_host",

Cargo.toml 🔗

@@ -59,6 +59,7 @@ members = [
     "crates/zeta2_tools",
     "crates/editor",
     "crates/eval",
+    "crates/encodings",
     "crates/explorer_command_injector",
     "crates/extension",
     "crates/extension_api",
@@ -221,6 +222,7 @@ members = [
 
     "tooling/perf",
     "tooling/xtask",
+    "crates/encodings",
 ]
 default-members = ["crates/zed"]
 
@@ -315,6 +317,7 @@ edit_prediction = { path = "crates/edit_prediction" }
 edit_prediction_button = { path = "crates/edit_prediction_button" }
 edit_prediction_context = { path = "crates/edit_prediction_context" }
 zeta2_tools = { path = "crates/zeta2_tools" }
+encodings = {path = "crates/encodings"}
 inspector_ui = { path = "crates/inspector_ui" }
 install_cli = { path = "crates/install_cli" }
 journal = { path = "crates/journal" }

crates/encodings/Cargo.toml 🔗

@@ -0,0 +1,17 @@
+[package]
+name = "encodings"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+
+[dependencies]
+ui.workspace = true
+workspace.workspace = true
+gpui.workspace = true
+picker.workspace = true
+language.workspace = true
+util.workspace = true
+fuzzy.workspace = true
+
+[lints]
+workspace = true

crates/encodings/src/lib.rs 🔗

@@ -0,0 +1,210 @@
+use std::sync::Weak;
+use std::sync::atomic::AtomicBool;
+
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{AppContext, ClickEvent, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
+use language::Buffer;
+use picker::{Picker, PickerDelegate};
+use ui::{
+    Button, ButtonCommon, Context, Label, LabelSize, ListItem, Render, Styled, Tooltip, Window,
+    div, rems, v_flex,
+};
+use ui::{Clickable, ParentElement};
+use util::ResultExt;
+use workspace::{ItemHandle, ModalView, StatusItemView, Workspace};
+
+pub enum Encoding {
+    Utf8(WeakEntity<Workspace>),
+}
+
+impl Encoding {
+    pub fn as_str(&self) -> &str {
+        match &self {
+            Encoding::Utf8(_) => "UTF-8",
+        }
+    }
+}
+
+impl EncodingSaveOrReopenSelector {
+    pub fn new(window: &mut Window, cx: &mut Context<EncodingSaveOrReopenSelector>) -> Self {
+        let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade());
+
+        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+
+        Self { picker }
+    }
+
+    pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
+        workspace.toggle_modal(window, cx, |window, cx| {
+            EncodingSaveOrReopenSelector::new(window, cx)
+        });
+    }
+}
+
+pub struct EncodingSaveOrReopenSelector {
+    picker: Entity<Picker<EncodingSaveOrReopenDelegate>>,
+}
+
+impl Focusable for EncodingSaveOrReopenSelector {
+    fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
+        self.picker.focus_handle(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())
+    }
+}
+
+impl ModalView for EncodingSaveOrReopenSelector {}
+
+impl EventEmitter<DismissEvent> for EncodingSaveOrReopenSelector {}
+
+pub struct EncodingSaveOrReopenDelegate {
+    encoding_selector: WeakEntity<EncodingSaveOrReopenSelector>,
+    current_selection: usize,
+    matches: Vec<StringMatch>,
+    pub actions: Vec<StringMatchCandidate>,
+}
+
+impl EncodingSaveOrReopenDelegate {
+    pub fn new(selector: WeakEntity<EncodingSaveOrReopenSelector>) -> Self {
+        Self {
+            encoding_selector: selector,
+            current_selection: 0,
+            matches: Vec::new(),
+            actions: vec![
+                StringMatchCandidate::new(0, "Save with encoding"),
+                StringMatchCandidate::new(1, "Reopen with encoding"),
+            ],
+        }
+    }
+
+    pub fn get_actions(&self) -> (&str, &str) {
+        (&self.actions[0].string, &self.actions[1].string)
+    }
+}
+
+impl PickerDelegate for EncodingSaveOrReopenDelegate {
+    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,
+        _window: &mut Window,
+        _cx: &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 action...".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 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
+            };
+
+            this.update(cx, |picker, cx| {
+                let delegate = &mut picker.delegate;
+                delegate.current_selection = matches.len().saturating_sub(1);
+                delegate.matches = matches;
+                cx.notify();
+            })
+            .log_err();
+        })
+    }
+
+    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {}
+
+    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        self.encoding_selector
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string)))
+    }
+}
+
+fn get_current_encoding() -> &'static str {
+    "UTF-8"
+}
+
+impl Render for Encoding {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+        let encoding_indicator = div();
+
+        encoding_indicator.child(
+            Button::new("encoding", get_current_encoding())
+                .label_size(LabelSize::Small)
+                .tooltip(Tooltip::text("Select Encoding"))
+                .on_click(cx.listener(|encoding, _: &ClickEvent, window, cx| {
+                    if let Some(workspace) = match encoding {
+                        Encoding::Utf8(workspace) => workspace.upgrade(),
+                    } {
+                        workspace.update(cx, |workspace, cx| {
+                            EncodingSaveOrReopenSelector::toggle(workspace, window, cx)
+                        })
+                    } else {
+                    }
+                })),
+        )
+    }
+}
+
+impl StatusItemView for Encoding {
+    fn set_active_pane_item(
+        &mut self,
+        _active_pane_item: Option<&dyn ItemHandle>,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) {
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -132,6 +132,7 @@ use crate::persistence::{
 };
 use crate::{item::ItemBufferKind, notifications::NotificationId};
 
+
 pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
 
 static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {

crates/zed/Cargo.toml 🔗

@@ -52,6 +52,7 @@ debugger_ui.workspace = true
 diagnostics.workspace = true
 editor.workspace = true
 zeta2_tools.workspace = true
+encodings.workspace = true
 env_logger.workspace = true
 extension.workspace = true
 extension_host.workspace = true

crates/zed/src/zed.rs 🔗

@@ -443,6 +443,8 @@ pub fn initialize_workspace(
             }
         });
 
+        let encoding_indicator = cx.new(|_cx| encodings::Encoding::Utf8(workspace.weak_handle()));
+
         let cursor_position =
             cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
         let line_ending_indicator =
@@ -458,6 +460,7 @@ pub fn initialize_workspace(
             status_bar.add_right_item(line_ending_indicator, window, cx);
             status_bar.add_right_item(vim_mode_indicator, window, cx);
             status_bar.add_right_item(cursor_position, window, cx);
+            status_bar.add_right_item(encoding_indicator, window, cx);
             status_bar.add_right_item(image_info, window, cx);
         });