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