selectors.rs

  1/// This module contains the encoding selectors for saving or reopening files with a different encoding.
  2/// It provides a modal view that allows the user to choose between saving with a different encoding
  3/// or reopening with a different encoding, and then selecting the desired encoding from a list.
  4pub mod save_or_reopen {
  5    use editor::Editor;
  6    use gpui::Styled;
  7    use gpui::{AppContext, ParentElement};
  8    use picker::Picker;
  9    use picker::PickerDelegate;
 10    use std::sync::atomic::AtomicBool;
 11    use util::ResultExt;
 12
 13    use fuzzy::{StringMatch, StringMatchCandidate};
 14    use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
 15
 16    use ui::{Context, HighlightedLabel, ListItem, Render, Window, rems, v_flex};
 17    use workspace::{ModalView, Workspace};
 18
 19    use crate::selectors::encoding::{Action, EncodingSelector};
 20
 21    /// A modal view that allows the user to select between saving with a different encoding or
 22    /// reopening with a different encoding.
 23    pub struct EncodingSaveOrReopenSelector {
 24        picker: Entity<Picker<EncodingSaveOrReopenDelegate>>,
 25        pub current_selection: usize,
 26    }
 27
 28    impl EncodingSaveOrReopenSelector {
 29        pub fn new(
 30            window: &mut Window,
 31            cx: &mut Context<EncodingSaveOrReopenSelector>,
 32            workspace: WeakEntity<Workspace>,
 33        ) -> Self {
 34            let delegate =
 35                EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace.clone());
 36
 37            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 38
 39            Self {
 40                picker,
 41                current_selection: 0,
 42            }
 43        }
 44
 45        /// Toggle the modal view for selecting between saving with a different encoding or
 46        /// reopening with a different encoding.
 47        pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
 48            let weak_workspace = workspace.weak_handle();
 49            workspace.toggle_modal(window, cx, |window, cx| {
 50                EncodingSaveOrReopenSelector::new(window, cx, weak_workspace)
 51            });
 52        }
 53    }
 54
 55    impl Focusable for EncodingSaveOrReopenSelector {
 56        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
 57            self.picker.focus_handle(cx)
 58        }
 59    }
 60
 61    impl Render for EncodingSaveOrReopenSelector {
 62        fn render(
 63            &mut self,
 64            _window: &mut Window,
 65            _cx: &mut Context<Self>,
 66        ) -> impl ui::IntoElement {
 67            v_flex().w(rems(34.0)).child(self.picker.clone())
 68        }
 69    }
 70
 71    impl ModalView for EncodingSaveOrReopenSelector {}
 72
 73    impl EventEmitter<DismissEvent> for EncodingSaveOrReopenSelector {}
 74
 75    pub struct EncodingSaveOrReopenDelegate {
 76        selector: WeakEntity<EncodingSaveOrReopenSelector>,
 77        current_selection: usize,
 78        matches: Vec<StringMatch>,
 79        pub actions: Vec<StringMatchCandidate>,
 80        workspace: WeakEntity<Workspace>,
 81    }
 82
 83    impl EncodingSaveOrReopenDelegate {
 84        pub fn new(
 85            selector: WeakEntity<EncodingSaveOrReopenSelector>,
 86            workspace: WeakEntity<Workspace>,
 87        ) -> Self {
 88            Self {
 89                selector,
 90                current_selection: 0,
 91                matches: Vec::new(),
 92                actions: vec![
 93                    StringMatchCandidate::new(0, "Save with encoding"),
 94                    StringMatchCandidate::new(1, "Reopen with encoding"),
 95                ],
 96                workspace,
 97            }
 98        }
 99
100        pub fn get_actions(&self) -> (&str, &str) {
101            (&self.actions[0].string, &self.actions[1].string)
102        }
103
104        /// Handle the action selected by the user.
105        pub fn post_selection(
106            &self,
107            cx: &mut Context<Picker<EncodingSaveOrReopenDelegate>>,
108            window: &mut Window,
109        ) -> Option<()> {
110            if self.current_selection == 0 {
111                if let Some(workspace) = self.workspace.upgrade() {
112                    let (_, buffer, _) = workspace
113                        .read(cx)
114                        .active_item(cx)?
115                        .act_as::<Editor>(cx)?
116                        .read(cx)
117                        .active_excerpt(cx)?;
118
119                    let weak_workspace = workspace.read(cx).weak_handle();
120
121                    workspace.update(cx, |workspace, cx| {
122                        workspace.toggle_modal(window, cx, |window, cx| {
123                            EncodingSelector::new(
124                                window,
125                                cx,
126                                Action::Save,
127                                buffer.downgrade(),
128                                weak_workspace,
129                            )
130                        })
131                    });
132                }
133            } else if self.current_selection == 1 {
134                if let Some(workspace) = self.workspace.upgrade() {
135                    let (_, buffer, _) = workspace
136                        .read(cx)
137                        .active_item(cx)?
138                        .act_as::<Editor>(cx)?
139                        .read(cx)
140                        .active_excerpt(cx)?;
141
142                    let weak_workspace = workspace.read(cx).weak_handle();
143
144                    workspace.update(cx, |workspace, cx| {
145                        workspace.toggle_modal(window, cx, |window, cx| {
146                            EncodingSelector::new(
147                                window,
148                                cx,
149                                Action::Reopen,
150                                buffer.downgrade(),
151                                weak_workspace,
152                            )
153                        })
154                    });
155                }
156            }
157
158            Some(())
159        }
160    }
161
162    impl PickerDelegate for EncodingSaveOrReopenDelegate {
163        type ListItem = ListItem;
164
165        fn match_count(&self) -> usize {
166            self.matches.len()
167        }
168
169        fn selected_index(&self) -> usize {
170            self.current_selection
171        }
172
173        fn set_selected_index(
174            &mut self,
175            ix: usize,
176            _window: &mut Window,
177            cx: &mut Context<Picker<Self>>,
178        ) {
179            self.current_selection = ix;
180            self.selector
181                .update(cx, |selector, _cx| {
182                    selector.current_selection = ix;
183                })
184                .log_err();
185        }
186
187        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
188            "Select an action...".into()
189        }
190
191        fn update_matches(
192            &mut self,
193            query: String,
194            window: &mut Window,
195            cx: &mut Context<Picker<Self>>,
196        ) -> gpui::Task<()> {
197            let executor = cx.background_executor().clone();
198            let actions = self.actions.clone();
199
200            cx.spawn_in(window, async move |this, cx| {
201                let matches = if query.is_empty() {
202                    actions
203                        .into_iter()
204                        .enumerate()
205                        .map(|(index, value)| StringMatch {
206                            candidate_id: index,
207                            score: 0.0,
208                            positions: vec![],
209                            string: value.string,
210                        })
211                        .collect::<Vec<StringMatch>>()
212                } else {
213                    fuzzy::match_strings(
214                        &actions,
215                        &query,
216                        false,
217                        false,
218                        2,
219                        &AtomicBool::new(false),
220                        executor,
221                    )
222                    .await
223                };
224
225                this.update(cx, |picker, cx| {
226                    let delegate = &mut picker.delegate;
227                    delegate.matches = matches;
228                    delegate.current_selection = delegate
229                        .current_selection
230                        .min(delegate.matches.len().saturating_sub(1));
231                    delegate
232                        .selector
233                        .update(cx, |selector, _cx| {
234                            selector.current_selection = delegate.current_selection
235                        })
236                        .log_err();
237                    cx.notify();
238                })
239                .log_err();
240            })
241        }
242
243        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
244            self.dismissed(window, cx);
245            if self.selector.is_upgradable() {
246                self.post_selection(cx, window);
247            }
248        }
249
250        fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
251            self.selector
252                .update(cx, |_, cx| cx.emit(DismissEvent))
253                .log_err();
254        }
255
256        fn render_match(
257            &self,
258            ix: usize,
259            _: bool,
260            _: &mut Window,
261            _: &mut Context<Picker<Self>>,
262        ) -> Option<Self::ListItem> {
263            Some(
264                ListItem::new(ix)
265                    .child(HighlightedLabel::new(
266                        &self.matches[ix].string,
267                        self.matches[ix].positions.clone(),
268                    ))
269                    .spacing(ui::ListItemSpacing::Sparse),
270            )
271        }
272    }
273
274    pub fn get_current_encoding() -> &'static str {
275        "UTF-8"
276    }
277}
278
279/// This module contains the encoding selector for choosing an encoding to save or reopen a file with.
280pub mod encoding {
281    use std::sync::atomic::AtomicBool;
282
283    use fuzzy::{StringMatch, StringMatchCandidate};
284    use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
285    use language::Buffer;
286    use picker::{Picker, PickerDelegate};
287    use ui::{
288        Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled,
289        Window, rems, v_flex,
290    };
291    use util::{ResultExt, TryFutureExt};
292    use workspace::{ModalView, Workspace};
293
294    use crate::encoding_from_name;
295
296    /// A modal view that allows the user to select an encoding from a list of encodings.
297    pub struct EncodingSelector {
298        picker: Entity<Picker<EncodingSelectorDelegate>>,
299        workspace: WeakEntity<Workspace>,
300    }
301
302    pub struct EncodingSelectorDelegate {
303        current_selection: usize,
304        encodings: Vec<StringMatchCandidate>,
305        matches: Vec<StringMatch>,
306        selector: WeakEntity<EncodingSelector>,
307        buffer: WeakEntity<Buffer>,
308        action: Action,
309    }
310
311    impl EncodingSelectorDelegate {
312        pub fn new(
313            selector: WeakEntity<EncodingSelector>,
314            buffer: WeakEntity<Buffer>,
315            action: Action,
316        ) -> EncodingSelectorDelegate {
317            EncodingSelectorDelegate {
318                current_selection: 0,
319                encodings: vec![
320                    StringMatchCandidate::new(0, "UTF-8"),
321                    StringMatchCandidate::new(1, "UTF-16 LE"),
322                    StringMatchCandidate::new(2, "UTF-16 BE"),
323                    StringMatchCandidate::new(3, "IBM866"),
324                    StringMatchCandidate::new(4, "ISO 8859-1"),
325                    StringMatchCandidate::new(5, "ISO 8859-2"),
326                    StringMatchCandidate::new(6, "ISO 8859-3"),
327                    StringMatchCandidate::new(7, "ISO 8859-4"),
328                    StringMatchCandidate::new(8, "ISO 8859-5"),
329                    StringMatchCandidate::new(9, "ISO 8859-6"),
330                    StringMatchCandidate::new(10, "ISO 8859-7"),
331                    StringMatchCandidate::new(11, "ISO 8859-8"),
332                    StringMatchCandidate::new(12, "ISO 8859-10"),
333                    StringMatchCandidate::new(13, "ISO 8859-13"),
334                    StringMatchCandidate::new(14, "ISO 8859-14"),
335                    StringMatchCandidate::new(15, "ISO 8859-15"),
336                    StringMatchCandidate::new(16, "ISO 8859-16"),
337                    StringMatchCandidate::new(17, "KOI8-R"),
338                    StringMatchCandidate::new(18, "KOI8-U"),
339                    StringMatchCandidate::new(19, "MacRoman"),
340                    StringMatchCandidate::new(20, "Mac Cyrillic"),
341                    StringMatchCandidate::new(21, "Windows-874"),
342                    StringMatchCandidate::new(22, "Windows-1250"),
343                    StringMatchCandidate::new(23, "Windows-1251"),
344                    StringMatchCandidate::new(24, "Windows-1252"),
345                    StringMatchCandidate::new(25, "Windows-1253"),
346                    StringMatchCandidate::new(26, "Windows-1254"),
347                    StringMatchCandidate::new(27, "Windows-1255"),
348                    StringMatchCandidate::new(28, "Windows-1256"),
349                    StringMatchCandidate::new(29, "Windows-1257"),
350                    StringMatchCandidate::new(30, "Windows-1258"),
351                    StringMatchCandidate::new(31, "Windows-949"),
352                    StringMatchCandidate::new(32, "EUC-JP"),
353                    StringMatchCandidate::new(33, "ISO 2022-JP"),
354                    StringMatchCandidate::new(34, "GBK"),
355                    StringMatchCandidate::new(35, "GB18030"),
356                    StringMatchCandidate::new(36, "Big5"),
357                    StringMatchCandidate::new(37, "HZ-GB-2312"),
358                ],
359                matches: Vec::new(),
360                selector,
361                buffer,
362                action,
363            }
364        }
365    }
366
367    impl PickerDelegate for EncodingSelectorDelegate {
368        type ListItem = ListItem;
369
370        fn match_count(&self) -> usize {
371            self.matches.len()
372        }
373
374        fn selected_index(&self) -> usize {
375            self.current_selection
376        }
377
378        fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) {
379            self.current_selection = ix;
380        }
381
382        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
383            "Select an encoding...".into()
384        }
385
386        fn update_matches(
387            &mut self,
388            query: String,
389            window: &mut Window,
390            cx: &mut Context<Picker<Self>>,
391        ) -> gpui::Task<()> {
392            let executor = cx.background_executor().clone();
393            let encodings = self.encodings.clone();
394
395            cx.spawn_in(window, async move |picker, cx| {
396                let matches: Vec<StringMatch>;
397
398                if query.is_empty() {
399                    matches = encodings
400                        .into_iter()
401                        .enumerate()
402                        .map(|(index, value)| StringMatch {
403                            candidate_id: index,
404                            score: 0.0,
405                            positions: Vec::new(),
406                            string: value.string,
407                        })
408                        .collect();
409                } else {
410                    matches = fuzzy::match_strings(
411                        &encodings,
412                        &query,
413                        true,
414                        false,
415                        38,
416                        &AtomicBool::new(false),
417                        executor,
418                    )
419                    .await
420                }
421
422                picker
423                    .update(cx, |picker, cx| {
424                        let delegate = &mut picker.delegate;
425                        delegate.matches = matches;
426                        delegate.current_selection = delegate
427                            .current_selection
428                            .min(delegate.matches.len().saturating_sub(1));
429                        cx.notify();
430                    })
431                    .log_err();
432            })
433        }
434
435        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
436            if let Some(buffer) = self.buffer.upgrade() {
437                buffer.update(cx, |buffer, cx| {
438                    buffer.encoding =
439                        encoding_from_name(self.matches[self.current_selection].string.as_str());
440                    if self.action == Action::Reopen {
441                        let executor = cx.background_executor().clone();
442                        executor.spawn(buffer.reload(cx)).detach();
443                    } else if self.action == Action::Save {
444                        let executor = cx.background_executor().clone();
445
446                        let workspace = self
447                            .selector
448                            .upgrade()
449                            .unwrap()
450                            .read(cx)
451                            .workspace
452                            .upgrade()
453                            .unwrap();
454
455                        executor
456                            .spawn(workspace.update(cx, |workspace, cx| {
457                                workspace
458                                    .save_active_item(workspace::SaveIntent::Save, window, cx)
459                                    .log_err()
460                            }))
461                            .detach();
462                    }
463                });
464            }
465            self.dismissed(window, cx);
466        }
467
468        fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
469            self.selector
470                .update(cx, |_, cx| cx.emit(DismissEvent))
471                .log_err();
472        }
473
474        fn render_match(
475            &self,
476            ix: usize,
477            _: bool,
478            _: &mut Window,
479            _: &mut Context<Picker<Self>>,
480        ) -> Option<Self::ListItem> {
481            Some(
482                ListItem::new(ix)
483                    .child(HighlightedLabel::new(
484                        &self.matches[ix].string,
485                        self.matches[ix].positions.clone(),
486                    ))
487                    .spacing(ListItemSpacing::Sparse),
488            )
489        }
490    }
491
492    /// The action to perform after selecting an encoding.
493    #[derive(PartialEq, Clone)]
494    pub enum Action {
495        Save,
496        Reopen,
497    }
498
499    impl EncodingSelector {
500        pub fn new(
501            window: &mut Window,
502            cx: &mut Context<EncodingSelector>,
503            action: Action,
504            buffer: WeakEntity<Buffer>,
505            workspace: WeakEntity<Workspace>,
506        ) -> EncodingSelector {
507            let delegate =
508                EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action.clone());
509            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
510
511            EncodingSelector { picker, workspace }
512        }
513    }
514
515    impl EventEmitter<DismissEvent> for EncodingSelector {}
516
517    impl Focusable for EncodingSelector {
518        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
519            cx.focus_handle()
520        }
521    }
522
523    impl ModalView for EncodingSelector {}
524
525    impl Render for EncodingSelector {
526        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
527            v_flex().w(rems(34.0)).child(self.picker.clone())
528        }
529    }
530}