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