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