encoding_selector.rs

  1mod active_buffer_encoding;
  2pub use active_buffer_encoding::ActiveBufferEncoding;
  3
  4use editor::Editor;
  5use encoding_rs::Encoding;
  6use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  7use gpui::{
  8    App, AppContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  9    InteractiveElement, ParentElement, Render, Styled, Task, WeakEntity, Window, actions,
 10};
 11use language::Buffer;
 12use picker::{Picker, PickerDelegate};
 13use std::sync::Arc;
 14use ui::{HighlightedLabel, ListItem, ListItemSpacing, Toggleable, v_flex};
 15use util::ResultExt;
 16use workspace::{ModalView, Toast, Workspace, notifications::NotificationId};
 17
 18actions!(
 19    encoding_selector,
 20    [
 21        /// Toggles the encoding selector modal.
 22        Toggle
 23    ]
 24);
 25
 26pub fn init(cx: &mut App) {
 27    cx.observe_new(EncodingSelector::register).detach();
 28}
 29
 30pub struct EncodingSelector {
 31    picker: Entity<Picker<EncodingSelectorDelegate>>,
 32}
 33
 34impl EncodingSelector {
 35    fn register(
 36        workspace: &mut Workspace,
 37        _window: Option<&mut Window>,
 38        _: &mut Context<Workspace>,
 39    ) {
 40        workspace.register_action(move |workspace, _: &Toggle, window, cx| {
 41            Self::toggle(workspace, window, cx);
 42        });
 43    }
 44
 45    pub fn toggle(
 46        workspace: &mut Workspace,
 47        window: &mut Window,
 48        cx: &mut Context<Workspace>,
 49    ) -> Option<()> {
 50        let (_, buffer, _) = workspace
 51            .active_item(cx)?
 52            .act_as::<Editor>(cx)?
 53            .read(cx)
 54            .active_excerpt(cx)?;
 55
 56        let buffer_handle = buffer.read(cx);
 57        let project = workspace.project().read(cx);
 58
 59        if buffer_handle.is_dirty() {
 60            workspace.show_toast(
 61                Toast::new(
 62                    NotificationId::unique::<EncodingSelector>(),
 63                    "Save file to change encoding",
 64                ),
 65                cx,
 66            );
 67            return Some(());
 68        }
 69        if project.is_shared() {
 70            workspace.show_toast(
 71                Toast::new(
 72                    NotificationId::unique::<EncodingSelector>(),
 73                    "Cannot change encoding during collaboration",
 74                ),
 75                cx,
 76            );
 77            return Some(());
 78        }
 79        if project.is_via_remote_server() {
 80            workspace.show_toast(
 81                Toast::new(
 82                    NotificationId::unique::<EncodingSelector>(),
 83                    "Cannot change encoding of remote server file",
 84                ),
 85                cx,
 86            );
 87            return Some(());
 88        }
 89
 90        workspace.toggle_modal(window, cx, move |window, cx| {
 91            EncodingSelector::new(buffer, window, cx)
 92        });
 93        Some(())
 94    }
 95
 96    fn new(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 97        let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer);
 98        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 99        Self { picker }
100    }
101}
102
103impl Render for EncodingSelector {
104    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
105        v_flex()
106            .key_context("EncodingSelector")
107            .w(gpui::rems(34.))
108            .child(self.picker.clone())
109    }
110}
111
112impl Focusable for EncodingSelector {
113    fn focus_handle(&self, cx: &App) -> FocusHandle {
114        self.picker.focus_handle(cx)
115    }
116}
117
118impl EventEmitter<DismissEvent> for EncodingSelector {}
119impl ModalView for EncodingSelector {}
120
121pub struct EncodingSelectorDelegate {
122    encoding_selector: WeakEntity<EncodingSelector>,
123    buffer: Entity<Buffer>,
124    encodings: Vec<&'static Encoding>,
125    match_candidates: Arc<Vec<StringMatchCandidate>>,
126    matches: Vec<StringMatch>,
127    selected_index: usize,
128}
129
130impl EncodingSelectorDelegate {
131    fn new(encoding_selector: WeakEntity<EncodingSelector>, buffer: Entity<Buffer>) -> Self {
132        let encodings = available_encodings();
133        let match_candidates = encodings
134            .iter()
135            .enumerate()
136            .map(|(id, enc)| StringMatchCandidate::new(id, enc.name()))
137            .collect::<Vec<_>>();
138        Self {
139            encoding_selector,
140            buffer,
141            encodings,
142            match_candidates: Arc::new(match_candidates),
143            matches: vec![],
144            selected_index: 0,
145        }
146    }
147
148    fn render_data_for_match(&self, mat: &StringMatch, cx: &App) -> String {
149        let candidate_encoding = self.encodings[mat.candidate_id];
150        let current_encoding = self.buffer.read(cx).encoding();
151
152        if candidate_encoding.name() == current_encoding.name() {
153            format!("{} (current)", candidate_encoding.name())
154        } else {
155            candidate_encoding.name().to_string()
156        }
157    }
158}
159
160fn available_encodings() -> Vec<&'static Encoding> {
161    let mut encodings = vec![
162        // Unicode
163        encoding_rs::UTF_8,
164        encoding_rs::UTF_16LE,
165        encoding_rs::UTF_16BE,
166        // Japanese
167        encoding_rs::SHIFT_JIS,
168        encoding_rs::EUC_JP,
169        encoding_rs::ISO_2022_JP,
170        // Chinese
171        encoding_rs::GBK,
172        encoding_rs::GB18030,
173        encoding_rs::BIG5,
174        // Korean
175        encoding_rs::EUC_KR,
176        // Windows / Single Byte Series
177        encoding_rs::WINDOWS_1252, // Western (ISO-8859-1 unified)
178        encoding_rs::WINDOWS_1250, // Central European
179        encoding_rs::WINDOWS_1251, // Cyrillic
180        encoding_rs::WINDOWS_1253, // Greek
181        encoding_rs::WINDOWS_1254, // Turkish (ISO-8859-9 unified)
182        encoding_rs::WINDOWS_1255, // Hebrew
183        encoding_rs::WINDOWS_1256, // Arabic
184        encoding_rs::WINDOWS_1257, // Baltic
185        encoding_rs::WINDOWS_1258, // Vietnamese
186        encoding_rs::WINDOWS_874,  // Thai
187        // ISO-8859 Series (others)
188        encoding_rs::ISO_8859_2,
189        encoding_rs::ISO_8859_3,
190        encoding_rs::ISO_8859_4,
191        encoding_rs::ISO_8859_5,
192        encoding_rs::ISO_8859_6,
193        encoding_rs::ISO_8859_7,
194        encoding_rs::ISO_8859_8,
195        encoding_rs::ISO_8859_8_I, // Logical Hebrew
196        encoding_rs::ISO_8859_10,
197        encoding_rs::ISO_8859_13,
198        encoding_rs::ISO_8859_14,
199        encoding_rs::ISO_8859_15,
200        encoding_rs::ISO_8859_16,
201        // Cyrillic / Legacy Misc
202        encoding_rs::KOI8_R,
203        encoding_rs::KOI8_U,
204        encoding_rs::IBM866,
205        encoding_rs::MACINTOSH,
206        encoding_rs::X_MAC_CYRILLIC,
207        // NOTE: The following encodings are intentionally excluded from the list:
208        //
209        // 1. encoding_rs::REPLACEMENT
210        //    Used internally for decoding errors. Not suitable for user selection.
211        //
212        // 2. encoding_rs::X_USER_DEFINED
213        //    Used for binary data emulation (legacy web behavior). Not for general text editing.
214    ];
215
216    encodings.sort_by_key(|enc| enc.name());
217
218    encodings
219}
220
221impl PickerDelegate for EncodingSelectorDelegate {
222    type ListItem = ListItem;
223
224    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
225        "Reopen with encoding...".into()
226    }
227
228    fn match_count(&self) -> usize {
229        self.matches.len()
230    }
231
232    fn selected_index(&self) -> usize {
233        self.selected_index
234    }
235
236    fn set_selected_index(
237        &mut self,
238        ix: usize,
239        _window: &mut Window,
240        _: &mut Context<Picker<Self>>,
241    ) {
242        self.selected_index = ix;
243    }
244
245    fn update_matches(
246        &mut self,
247        query: String,
248        window: &mut Window,
249        cx: &mut Context<Picker<Self>>,
250    ) -> Task<()> {
251        let background = cx.background_executor().clone();
252        let candidates = self.match_candidates.clone();
253
254        cx.spawn_in(window, async move |this, cx| {
255            let matches = if query.is_empty() {
256                candidates
257                    .iter()
258                    .enumerate()
259                    .map(|(index, candidate)| StringMatch {
260                        candidate_id: index,
261                        string: candidate.string.clone(),
262                        positions: Vec::new(),
263                        score: 0.0,
264                    })
265                    .collect()
266            } else {
267                match_strings(
268                    &candidates,
269                    &query,
270                    false,
271                    true,
272                    100,
273                    &Default::default(),
274                    background,
275                )
276                .await
277            };
278
279            this.update(cx, |this, cx| {
280                let delegate = &mut this.delegate;
281                delegate.matches = matches;
282                delegate.selected_index = delegate
283                    .selected_index
284                    .min(delegate.matches.len().saturating_sub(1));
285                cx.notify();
286            })
287            .log_err();
288        })
289    }
290
291    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
292        if let Some(mat) = self.matches.get(self.selected_index) {
293            let selected_encoding = self.encodings[mat.candidate_id];
294
295            self.buffer.update(cx, |buffer, cx| {
296                let _ = buffer.reload_with_encoding(selected_encoding, cx);
297            });
298        }
299        self.dismissed(window, cx);
300    }
301
302    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
303        self.encoding_selector
304            .update(cx, |_, cx| cx.emit(DismissEvent))
305            .log_err();
306    }
307
308    fn render_match(
309        &self,
310        ix: usize,
311        selected: bool,
312        _: &mut Window,
313        cx: &mut Context<Picker<Self>>,
314    ) -> Option<Self::ListItem> {
315        let mat = &self.matches.get(ix)?;
316
317        let label = self.render_data_for_match(mat, cx);
318
319        Some(
320            ListItem::new(ix)
321                .inset(true)
322                .spacing(ListItemSpacing::Sparse)
323                .toggle_state(selected)
324                .child(HighlightedLabel::new(label, mat.positions.clone())),
325        )
326    }
327}