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