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