lib.rs

  1//! A crate for handling file encodings in the text editor.
  2
  3use crate::selectors::encoding::Action;
  4use editor::Editor;
  5use encoding_rs::Encoding;
  6use gpui::{ClickEvent, Entity, Subscription, WeakEntity};
  7use language::Buffer;
  8use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div};
  9use ui::{Clickable, ParentElement};
 10use util::ResultExt;
 11use workspace::{
 12    CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace,
 13    with_active_or_new_workspace,
 14};
 15use zed_actions::encodings_ui::{ForceOpen, Toggle};
 16
 17use crate::selectors::encoding::EncodingSelector;
 18use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector;
 19
 20/// A status bar item that shows the current file encoding and allows changing it.
 21pub struct EncodingIndicator {
 22    pub encoding: Option<&'static Encoding>,
 23    pub workspace: WeakEntity<Workspace>,
 24
 25    /// Subscription to observe changes in the active editor
 26    observe_editor: Option<Subscription>,
 27
 28    /// Subscription to observe changes in the `encoding` field of the `Buffer` struct
 29    observe_buffer_encoding: Option<Subscription>,
 30
 31    /// Whether to show the indicator or not, based on whether an editor is active
 32    show: bool,
 33
 34    /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when
 35    /// the current buffer is associated with a file.
 36    show_save_or_reopen_selector: bool,
 37}
 38
 39pub mod selectors;
 40
 41impl Render for EncodingIndicator {
 42    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
 43        let status_element = div();
 44        let show_save_or_reopen_selector = self.show_save_or_reopen_selector;
 45
 46        if !self.show {
 47            return status_element;
 48        }
 49
 50        status_element.child(
 51            Button::new(
 52                "encoding",
 53                encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)),
 54            )
 55            .label_size(LabelSize::Small)
 56            .tooltip(Tooltip::text("Select Encoding"))
 57            .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| {
 58                if let Some(workspace) = indicator.workspace.upgrade() {
 59                    workspace.update(cx, move |workspace, cx| {
 60                        // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file,
 61                        if show_save_or_reopen_selector {
 62                            EncodingSaveOrReopenSelector::toggle(workspace, window, cx)
 63                        }
 64                        // otherwise, open the `EncodingSelector` directly.
 65                        else {
 66                            let (_, buffer, _) = workspace
 67                                .active_item(cx)
 68                                .unwrap()
 69                                .act_as::<Editor>(cx)
 70                                .unwrap()
 71                                .read(cx)
 72                                .active_excerpt(cx)
 73                                .unwrap();
 74
 75                            let weak_workspace = workspace.weak_handle();
 76
 77                            if let Some(path) = buffer.read(cx).file() {
 78                                let path = path.clone().path().to_rel_path_buf();
 79                                workspace.toggle_modal(window, cx, |window, cx| {
 80                                    let selector = EncodingSelector::new(
 81                                        window,
 82                                        cx,
 83                                        Action::Save,
 84                                        Some(buffer.downgrade()),
 85                                        weak_workspace,
 86                                        Some(path.as_std_path().to_path_buf()),
 87                                    );
 88                                    selector
 89                                });
 90                            }
 91                        }
 92                    })
 93                }
 94            })),
 95        )
 96    }
 97}
 98
 99impl EncodingIndicator {
100    pub fn new(
101        encoding: Option<&'static Encoding>,
102        workspace: WeakEntity<Workspace>,
103        observe_editor: Option<Subscription>,
104        observe_buffer_encoding: Option<Subscription>,
105    ) -> EncodingIndicator {
106        EncodingIndicator {
107            encoding,
108            workspace,
109            observe_editor,
110            show: false,
111            observe_buffer_encoding,
112            show_save_or_reopen_selector: false,
113        }
114    }
115
116    /// Update the encoding when the active editor is switched.
117    pub fn update_when_editor_is_switched(
118        &mut self,
119        editor: Entity<Editor>,
120        _: &mut Window,
121        cx: &mut Context<EncodingIndicator>,
122    ) {
123        let editor = editor.read(cx);
124        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
125            let encoding = buffer.read(cx).encoding.clone();
126            self.encoding = Some(encoding.get());
127
128            if let Some(_) = buffer.read(cx).file() {
129                self.show_save_or_reopen_selector = true;
130            } else {
131                self.show_save_or_reopen_selector = false;
132            }
133        }
134
135        cx.notify();
136    }
137
138    /// Update the encoding when the `encoding` field of the `Buffer` struct changes.
139    pub fn update_when_buffer_encoding_changes(
140        &mut self,
141        buffer: Entity<Buffer>,
142        _: &mut Window,
143        cx: &mut Context<EncodingIndicator>,
144    ) {
145        let encoding = buffer.read(cx).encoding.clone();
146        self.encoding = Some(encoding.get());
147        cx.notify();
148    }
149}
150
151impl StatusItemView for EncodingIndicator {
152    fn set_active_pane_item(
153        &mut self,
154        active_pane_item: Option<&dyn ItemHandle>,
155        window: &mut Window,
156        cx: &mut Context<Self>,
157    ) {
158        match active_pane_item.and_then(|item| item.downcast::<Editor>()) {
159            Some(editor) => {
160                self.observe_editor =
161                    Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched));
162                if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) {
163                    self.observe_buffer_encoding = Some(cx.observe_in(
164                        buffer,
165                        window,
166                        Self::update_when_buffer_encoding_changes,
167                    ));
168                }
169                self.update_when_editor_is_switched(editor, window, cx);
170                self.show = true;
171            }
172            None => {
173                self.encoding = None;
174                self.observe_editor = None;
175                self.show = false;
176            }
177        }
178    }
179}
180
181/// Get a human-readable name for the given encoding.
182pub fn encoding_name(encoding: &'static Encoding) -> String {
183    let name = encoding.name();
184
185    match name {
186        "UTF-8" => "UTF-8",
187        "UTF-16LE" => "UTF-16 LE",
188        "UTF-16BE" => "UTF-16 BE",
189        "windows-1252" => "Windows-1252",
190        "windows-1251" => "Windows-1251",
191        "windows-1250" => "Windows-1250",
192        "ISO-8859-2" => "ISO 8859-2",
193        "ISO-8859-3" => "ISO 8859-3",
194        "ISO-8859-4" => "ISO 8859-4",
195        "ISO-8859-5" => "ISO 8859-5",
196        "ISO-8859-6" => "ISO 8859-6",
197        "ISO-8859-7" => "ISO 8859-7",
198        "ISO-8859-8" => "ISO 8859-8",
199        "ISO-8859-13" => "ISO 8859-13",
200        "ISO-8859-15" => "ISO 8859-15",
201        "KOI8-R" => "KOI8-R",
202        "KOI8-U" => "KOI8-U",
203        "macintosh" => "MacRoman",
204        "x-mac-cyrillic" => "Mac Cyrillic",
205        "windows-874" => "Windows-874",
206        "windows-1253" => "Windows-1253",
207        "windows-1254" => "Windows-1254",
208        "windows-1255" => "Windows-1255",
209        "windows-1256" => "Windows-1256",
210        "windows-1257" => "Windows-1257",
211        "windows-1258" => "Windows-1258",
212        "EUC-KR" => "Windows-949",
213        "EUC-JP" => "EUC-JP",
214        "ISO-2022-JP" => "ISO 2022-JP",
215        "GBK" => "GBK",
216        "gb18030" => "GB18030",
217        "Big5" => "Big5",
218        _ => name,
219    }
220    .to_string()
221}
222
223/// Get an encoding from its index in the predefined list.
224/// If the index is out of range, UTF-8 is returned as a default.
225pub fn encoding_from_index(index: usize) -> &'static Encoding {
226    match index {
227        0 => encoding_rs::UTF_8,
228        1 => encoding_rs::UTF_16LE,
229        2 => encoding_rs::UTF_16BE,
230        3 => encoding_rs::WINDOWS_1252,
231        4 => encoding_rs::WINDOWS_1251,
232        5 => encoding_rs::WINDOWS_1250,
233        6 => encoding_rs::ISO_8859_2,
234        7 => encoding_rs::ISO_8859_3,
235        8 => encoding_rs::ISO_8859_4,
236        9 => encoding_rs::ISO_8859_5,
237        10 => encoding_rs::ISO_8859_6,
238        11 => encoding_rs::ISO_8859_7,
239        12 => encoding_rs::ISO_8859_8,
240        13 => encoding_rs::ISO_8859_13,
241        14 => encoding_rs::ISO_8859_15,
242        15 => encoding_rs::KOI8_R,
243        16 => encoding_rs::KOI8_U,
244        17 => encoding_rs::MACINTOSH,
245        18 => encoding_rs::X_MAC_CYRILLIC,
246        19 => encoding_rs::WINDOWS_874,
247        20 => encoding_rs::WINDOWS_1253,
248        21 => encoding_rs::WINDOWS_1254,
249        22 => encoding_rs::WINDOWS_1255,
250        23 => encoding_rs::WINDOWS_1256,
251        24 => encoding_rs::WINDOWS_1257,
252        25 => encoding_rs::WINDOWS_1258,
253        26 => encoding_rs::EUC_KR,
254        27 => encoding_rs::EUC_JP,
255        28 => encoding_rs::ISO_2022_JP,
256        29 => encoding_rs::GBK,
257        30 => encoding_rs::GB18030,
258        31 => encoding_rs::BIG5,
259        _ => encoding_rs::UTF_8,
260    }
261}
262
263/// Get an encoding from its name.
264pub fn encoding_from_name(name: &str) -> &'static Encoding {
265    match name {
266        "UTF-8" => encoding_rs::UTF_8,
267        "UTF-16 LE" => encoding_rs::UTF_16LE,
268        "UTF-16 BE" => encoding_rs::UTF_16BE,
269        "Windows-1252" => encoding_rs::WINDOWS_1252,
270        "Windows-1251" => encoding_rs::WINDOWS_1251,
271        "Windows-1250" => encoding_rs::WINDOWS_1250,
272        "ISO 8859-2" => encoding_rs::ISO_8859_2,
273        "ISO 8859-3" => encoding_rs::ISO_8859_3,
274        "ISO 8859-4" => encoding_rs::ISO_8859_4,
275        "ISO 8859-5" => encoding_rs::ISO_8859_5,
276        "ISO 8859-6" => encoding_rs::ISO_8859_6,
277        "ISO 8859-7" => encoding_rs::ISO_8859_7,
278        "ISO 8859-8" => encoding_rs::ISO_8859_8,
279        "ISO 8859-13" => encoding_rs::ISO_8859_13,
280        "ISO 8859-15" => encoding_rs::ISO_8859_15,
281        "KOI8-R" => encoding_rs::KOI8_R,
282        "KOI8-U" => encoding_rs::KOI8_U,
283        "MacRoman" => encoding_rs::MACINTOSH,
284        "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC,
285        "Windows-874" => encoding_rs::WINDOWS_874,
286        "Windows-1253" => encoding_rs::WINDOWS_1253,
287        "Windows-1254" => encoding_rs::WINDOWS_1254,
288        "Windows-1255" => encoding_rs::WINDOWS_1255,
289        "Windows-1256" => encoding_rs::WINDOWS_1256,
290        "Windows-1257" => encoding_rs::WINDOWS_1257,
291        "Windows-1258" => encoding_rs::WINDOWS_1258,
292        "Windows-949" => encoding_rs::EUC_KR,
293        "EUC-JP" => encoding_rs::EUC_JP,
294        "ISO 2022-JP" => encoding_rs::ISO_2022_JP,
295        "GBK" => encoding_rs::GBK,
296        "GB18030" => encoding_rs::GB18030,
297        "Big5" => encoding_rs::BIG5,
298        _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names
299    }
300}
301
302pub fn init(cx: &mut App) {
303    cx.on_action(|action: &Toggle, cx: &mut App| {
304        let Toggle(path) = action.clone();
305        let path = path.to_path_buf();
306
307        with_active_or_new_workspace(cx, |workspace, window, cx| {
308            let weak_workspace = workspace.weak_handle();
309            workspace.toggle_modal(window, cx, |window, cx| {
310                EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path))
311            });
312        });
313    });
314
315    cx.on_action(|action: &ForceOpen, cx: &mut App| {
316        let ForceOpen(path) = action.clone();
317        let path = path.to_path_buf();
318
319        with_active_or_new_workspace(cx, |workspace, window, cx| {
320            workspace.active_pane().update(cx, |pane, cx| {
321                pane.close_active_item(&CloseActiveItem::default(), window, cx)
322                    .detach();
323            });
324
325            workspace
326                .encoding_options
327                .force
328                .store(true, std::sync::atomic::Ordering::Release);
329
330            let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx);
331            let weak_workspace = workspace.weak_handle();
332
333            cx.spawn(async move |_, cx| {
334                let workspace = weak_workspace.upgrade().unwrap();
335                open_task.await.log_err();
336                workspace
337                    .update(cx, |workspace: &mut Workspace, _| {
338                        *workspace.encoding_options.force.get_mut() = false;
339                    })
340                    .log_err();
341            })
342            .detach();
343        });
344    });
345}