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                            workspace.toggle_modal(window, cx, |window, cx| {
 78                                let selector = EncodingSelector::new(
 79                                    window,
 80                                    cx,
 81                                    Action::Save,
 82                                    Some(buffer.downgrade()),
 83                                    weak_workspace,
 84                                    None,
 85                                );
 86                                selector
 87                            })
 88                        }
 89                    })
 90                }
 91            })),
 92        )
 93    }
 94}
 95
 96impl EncodingIndicator {
 97    pub fn new(
 98        encoding: Option<&'static Encoding>,
 99        workspace: WeakEntity<Workspace>,
100        observe_editor: Option<Subscription>,
101        observe_buffer_encoding: Option<Subscription>,
102    ) -> EncodingIndicator {
103        EncodingIndicator {
104            encoding,
105            workspace,
106            observe_editor,
107            show: false,
108            observe_buffer_encoding,
109            show_save_or_reopen_selector: false,
110        }
111    }
112
113    /// Update the encoding when the active editor is switched.
114    pub fn update_when_editor_is_switched(
115        &mut self,
116        editor: Entity<Editor>,
117        _: &mut Window,
118        cx: &mut Context<EncodingIndicator>,
119    ) {
120        let editor = editor.read(cx);
121        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
122            let encoding = buffer.read(cx).encoding.clone();
123            self.encoding = Some(encoding.get());
124
125            if let Some(_) = buffer.read(cx).file() {
126                self.show_save_or_reopen_selector = true;
127            } else {
128                self.show_save_or_reopen_selector = false;
129            }
130        }
131
132        cx.notify();
133    }
134
135    /// Update the encoding when the `encoding` field of the `Buffer` struct changes.
136    pub fn update_when_buffer_encoding_changes(
137        &mut self,
138        buffer: Entity<Buffer>,
139        _: &mut Window,
140        cx: &mut Context<EncodingIndicator>,
141    ) {
142        let encoding = buffer.read(cx).encoding.clone();
143        self.encoding = Some(encoding.get());
144        cx.notify();
145    }
146}
147
148impl StatusItemView for EncodingIndicator {
149    fn set_active_pane_item(
150        &mut self,
151        active_pane_item: Option<&dyn ItemHandle>,
152        window: &mut Window,
153        cx: &mut Context<Self>,
154    ) {
155        match active_pane_item.and_then(|item| item.downcast::<Editor>()) {
156            Some(editor) => {
157                self.observe_editor =
158                    Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched));
159                if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) {
160                    self.observe_buffer_encoding = Some(cx.observe_in(
161                        buffer,
162                        window,
163                        Self::update_when_buffer_encoding_changes,
164                    ));
165                }
166                self.update_when_editor_is_switched(editor, window, cx);
167                self.show = true;
168            }
169            None => {
170                self.encoding = None;
171                self.observe_editor = None;
172                self.show = false;
173            }
174        }
175    }
176}
177
178/// Get a human-readable name for the given encoding.
179pub fn encoding_name(encoding: &'static Encoding) -> String {
180    let name = encoding.name();
181
182    match name {
183        "UTF-8" => "UTF-8",
184        "UTF-16LE" => "UTF-16 LE",
185        "UTF-16BE" => "UTF-16 BE",
186        "windows-1252" => "Windows-1252",
187        "windows-1251" => "Windows-1251",
188        "windows-1250" => "Windows-1250",
189        "ISO-8859-2" => "ISO 8859-2",
190        "ISO-8859-3" => "ISO 8859-3",
191        "ISO-8859-4" => "ISO 8859-4",
192        "ISO-8859-5" => "ISO 8859-5",
193        "ISO-8859-6" => "ISO 8859-6",
194        "ISO-8859-7" => "ISO 8859-7",
195        "ISO-8859-8" => "ISO 8859-8",
196        "ISO-8859-13" => "ISO 8859-13",
197        "ISO-8859-15" => "ISO 8859-15",
198        "KOI8-R" => "KOI8-R",
199        "KOI8-U" => "KOI8-U",
200        "macintosh" => "MacRoman",
201        "x-mac-cyrillic" => "Mac Cyrillic",
202        "windows-874" => "Windows-874",
203        "windows-1253" => "Windows-1253",
204        "windows-1254" => "Windows-1254",
205        "windows-1255" => "Windows-1255",
206        "windows-1256" => "Windows-1256",
207        "windows-1257" => "Windows-1257",
208        "windows-1258" => "Windows-1258",
209        "EUC-KR" => "Windows-949",
210        "EUC-JP" => "EUC-JP",
211        "ISO-2022-JP" => "ISO 2022-JP",
212        "GBK" => "GBK",
213        "gb18030" => "GB18030",
214        "Big5" => "Big5",
215        _ => name,
216    }
217    .to_string()
218}
219
220/// Get an encoding from its index in the predefined list.
221/// If the index is out of range, UTF-8 is returned as a default.
222pub fn encoding_from_index(index: usize) -> &'static Encoding {
223    match index {
224        0 => encoding_rs::UTF_8,
225        1 => encoding_rs::UTF_16LE,
226        2 => encoding_rs::UTF_16BE,
227        3 => encoding_rs::WINDOWS_1252,
228        4 => encoding_rs::WINDOWS_1251,
229        5 => encoding_rs::WINDOWS_1250,
230        6 => encoding_rs::ISO_8859_2,
231        7 => encoding_rs::ISO_8859_3,
232        8 => encoding_rs::ISO_8859_4,
233        9 => encoding_rs::ISO_8859_5,
234        10 => encoding_rs::ISO_8859_6,
235        11 => encoding_rs::ISO_8859_7,
236        12 => encoding_rs::ISO_8859_8,
237        13 => encoding_rs::ISO_8859_13,
238        14 => encoding_rs::ISO_8859_15,
239        15 => encoding_rs::KOI8_R,
240        16 => encoding_rs::KOI8_U,
241        17 => encoding_rs::MACINTOSH,
242        18 => encoding_rs::X_MAC_CYRILLIC,
243        19 => encoding_rs::WINDOWS_874,
244        20 => encoding_rs::WINDOWS_1253,
245        21 => encoding_rs::WINDOWS_1254,
246        22 => encoding_rs::WINDOWS_1255,
247        23 => encoding_rs::WINDOWS_1256,
248        24 => encoding_rs::WINDOWS_1257,
249        25 => encoding_rs::WINDOWS_1258,
250        26 => encoding_rs::EUC_KR,
251        27 => encoding_rs::EUC_JP,
252        28 => encoding_rs::ISO_2022_JP,
253        29 => encoding_rs::GBK,
254        30 => encoding_rs::GB18030,
255        31 => encoding_rs::BIG5,
256        _ => encoding_rs::UTF_8,
257    }
258}
259
260/// Get an encoding from its name.
261pub fn encoding_from_name(name: &str) -> &'static Encoding {
262    match name {
263        "UTF-8" => encoding_rs::UTF_8,
264        "UTF-16 LE" => encoding_rs::UTF_16LE,
265        "UTF-16 BE" => encoding_rs::UTF_16BE,
266        "Windows-1252" => encoding_rs::WINDOWS_1252,
267        "Windows-1251" => encoding_rs::WINDOWS_1251,
268        "Windows-1250" => encoding_rs::WINDOWS_1250,
269        "ISO 8859-2" => encoding_rs::ISO_8859_2,
270        "ISO 8859-3" => encoding_rs::ISO_8859_3,
271        "ISO 8859-4" => encoding_rs::ISO_8859_4,
272        "ISO 8859-5" => encoding_rs::ISO_8859_5,
273        "ISO 8859-6" => encoding_rs::ISO_8859_6,
274        "ISO 8859-7" => encoding_rs::ISO_8859_7,
275        "ISO 8859-8" => encoding_rs::ISO_8859_8,
276        "ISO 8859-13" => encoding_rs::ISO_8859_13,
277        "ISO 8859-15" => encoding_rs::ISO_8859_15,
278        "KOI8-R" => encoding_rs::KOI8_R,
279        "KOI8-U" => encoding_rs::KOI8_U,
280        "MacRoman" => encoding_rs::MACINTOSH,
281        "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC,
282        "Windows-874" => encoding_rs::WINDOWS_874,
283        "Windows-1253" => encoding_rs::WINDOWS_1253,
284        "Windows-1254" => encoding_rs::WINDOWS_1254,
285        "Windows-1255" => encoding_rs::WINDOWS_1255,
286        "Windows-1256" => encoding_rs::WINDOWS_1256,
287        "Windows-1257" => encoding_rs::WINDOWS_1257,
288        "Windows-1258" => encoding_rs::WINDOWS_1258,
289        "Windows-949" => encoding_rs::EUC_KR,
290        "EUC-JP" => encoding_rs::EUC_JP,
291        "ISO 2022-JP" => encoding_rs::ISO_2022_JP,
292        "GBK" => encoding_rs::GBK,
293        "GB18030" => encoding_rs::GB18030,
294        "Big5" => encoding_rs::BIG5,
295        _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names
296    }
297}
298
299pub fn init(cx: &mut App) {
300    cx.on_action(|action: &Toggle, cx: &mut App| {
301        let Toggle(path) = action.clone();
302        let path = path.to_path_buf();
303
304        with_active_or_new_workspace(cx, |workspace, window, cx| {
305            let weak_workspace = workspace.weak_handle();
306            workspace.toggle_modal(window, cx, |window, cx| {
307                EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path))
308            });
309        });
310    });
311
312    cx.on_action(|action: &ForceOpen, cx: &mut App| {
313        let ForceOpen(path) = action.clone();
314        let path = path.to_path_buf();
315
316        with_active_or_new_workspace(cx, |workspace, window, cx| {
317            workspace.active_pane().update(cx, |pane, cx| {
318                pane.close_active_item(&CloseActiveItem::default(), window, cx)
319                    .detach();
320            });
321
322            {
323                let force = workspace.encoding_options.force.get_mut();
324
325                *force = true;
326            }
327
328            let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx);
329            let weak_workspace = workspace.weak_handle();
330
331            cx.spawn(async move |_, cx| {
332                let workspace = weak_workspace.upgrade().unwrap();
333                open_task.await.log_err();
334                workspace
335                    .update(cx, |workspace: &mut Workspace, _| {
336                        *workspace.encoding_options.force.get_mut() = false;
337                    })
338                    .log_err();
339            })
340            .detach();
341        });
342    });
343}