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.
  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                    if let Some(file) = buffer.read(cx).file() {
121                        let path = file.as_local()?.abs_path(cx);
122
123                        workspace.update(cx, |workspace, cx| {
124                            workspace.toggle_modal(window, cx, |window, cx| {
125                                let selector = EncodingSelector::new(
126                                    window,
127                                    cx,
128                                    Action::Save,
129                                    Some(buffer.downgrade()),
130                                    weak_workspace,
131                                    Some(path),
132                                );
133                                selector
134                            })
135                        });
136                    }
137                }
138            } else if self.current_selection == 1 {
139                if let Some(workspace) = self.workspace.upgrade() {
140                    let (_, buffer, _) = workspace
141                        .read(cx)
142                        .active_item(cx)?
143                        .act_as::<Editor>(cx)?
144                        .read(cx)
145                        .active_excerpt(cx)?;
146
147                    let weak_workspace = workspace.read(cx).weak_handle();
148
149                    if let Some(file) = buffer.read(cx).file() {
150                        let path = file.as_local()?.abs_path(cx);
151
152                        workspace.update(cx, |workspace, cx| {
153                            workspace.toggle_modal(window, cx, |window, cx| {
154                                let selector = EncodingSelector::new(
155                                    window,
156                                    cx,
157                                    Action::Reopen,
158                                    Some(buffer.downgrade()),
159                                    weak_workspace,
160                                    Some(path),
161                                );
162                                selector
163                            });
164                        });
165                    }
166                }
167            }
168
169            Some(())
170        }
171    }
172
173    impl PickerDelegate for EncodingSaveOrReopenDelegate {
174        type ListItem = ListItem;
175
176        fn match_count(&self) -> usize {
177            self.matches.len()
178        }
179
180        fn selected_index(&self) -> usize {
181            self.current_selection
182        }
183
184        fn set_selected_index(
185            &mut self,
186            ix: usize,
187            _window: &mut Window,
188            cx: &mut Context<Picker<Self>>,
189        ) {
190            self.current_selection = ix;
191            self.selector
192                .update(cx, |selector, _cx| {
193                    selector.current_selection = ix;
194                })
195                .log_err();
196        }
197
198        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
199            "Select an action...".into()
200        }
201
202        fn update_matches(
203            &mut self,
204            query: String,
205            window: &mut Window,
206            cx: &mut Context<Picker<Self>>,
207        ) -> gpui::Task<()> {
208            let executor = cx.background_executor().clone();
209            let actions = self.actions.clone();
210
211            cx.spawn_in(window, async move |this, cx| {
212                let matches = if query.is_empty() {
213                    actions
214                        .into_iter()
215                        .enumerate()
216                        .map(|(index, value)| StringMatch {
217                            candidate_id: index,
218                            score: 0.0,
219                            positions: vec![],
220                            string: value.string,
221                        })
222                        .collect::<Vec<StringMatch>>()
223                } else {
224                    fuzzy::match_strings(
225                        &actions,
226                        &query,
227                        false,
228                        false,
229                        2,
230                        &AtomicBool::new(false),
231                        executor,
232                    )
233                    .await
234                };
235
236                this.update(cx, |picker, cx| {
237                    let delegate = &mut picker.delegate;
238                    delegate.matches = matches;
239                    delegate.current_selection = delegate
240                        .current_selection
241                        .min(delegate.matches.len().saturating_sub(1));
242                    delegate
243                        .selector
244                        .update(cx, |selector, _cx| {
245                            selector.current_selection = delegate.current_selection
246                        })
247                        .log_err();
248                    cx.notify();
249                })
250                .log_err();
251            })
252        }
253
254        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
255            self.dismissed(window, cx);
256            if self.selector.is_upgradable() {
257                self.post_selection(cx, window);
258            }
259        }
260
261        fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
262            self.selector
263                .update(cx, |_, cx| cx.emit(DismissEvent))
264                .log_err();
265        }
266
267        fn render_match(
268            &self,
269            ix: usize,
270            _: bool,
271            _: &mut Window,
272            _: &mut Context<Picker<Self>>,
273        ) -> Option<Self::ListItem> {
274            Some(
275                ListItem::new(ix)
276                    .child(HighlightedLabel::new(
277                        &self.matches[ix].string,
278                        self.matches[ix].positions.clone(),
279                    ))
280                    .spacing(ui::ListItemSpacing::Sparse),
281            )
282        }
283    }
284}
285
286/// This module contains the encoding selector for choosing an encoding to save or reopen a file with.
287pub mod encoding {
288    use editor::Editor;
289    use std::{path::PathBuf, sync::atomic::AtomicBool};
290
291    use fuzzy::{StringMatch, StringMatchCandidate};
292    use gpui::{
293        AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, http_client::anyhow,
294    };
295    use language::Buffer;
296    use picker::{Picker, PickerDelegate};
297    use ui::{
298        Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled,
299        Window, rems, v_flex,
300    };
301    use util::ResultExt;
302    use workspace::{CloseActiveItem, ModalView, OpenOptions, Workspace};
303
304    use crate::encoding_from_name;
305
306    /// A modal view that allows the user to select an encoding from a list of encodings.
307    pub struct EncodingSelector {
308        picker: Entity<Picker<EncodingSelectorDelegate>>,
309        workspace: WeakEntity<Workspace>,
310        path: Option<PathBuf>,
311    }
312
313    pub struct EncodingSelectorDelegate {
314        current_selection: usize,
315        encodings: Vec<StringMatchCandidate>,
316        matches: Vec<StringMatch>,
317        selector: WeakEntity<EncodingSelector>,
318        buffer: Option<WeakEntity<Buffer>>,
319        action: Action,
320    }
321
322    impl EncodingSelectorDelegate {
323        pub fn new(
324            selector: WeakEntity<EncodingSelector>,
325            buffer: Option<WeakEntity<Buffer>>,
326            action: Action,
327        ) -> EncodingSelectorDelegate {
328            EncodingSelectorDelegate {
329                current_selection: 0,
330                encodings: vec![
331                    StringMatchCandidate::new(0, "UTF-8"),
332                    StringMatchCandidate::new(1, "UTF-16 LE"),
333                    StringMatchCandidate::new(2, "UTF-16 BE"),
334                    StringMatchCandidate::new(3, "Windows-1252"),
335                    StringMatchCandidate::new(4, "Windows-1251"),
336                    StringMatchCandidate::new(5, "Windows-1250"),
337                    StringMatchCandidate::new(6, "ISO 8859-2"),
338                    StringMatchCandidate::new(7, "ISO 8859-3"),
339                    StringMatchCandidate::new(8, "ISO 8859-4"),
340                    StringMatchCandidate::new(9, "ISO 8859-5"),
341                    StringMatchCandidate::new(10, "ISO 8859-6"),
342                    StringMatchCandidate::new(11, "ISO 8859-7"),
343                    StringMatchCandidate::new(12, "ISO 8859-8"),
344                    StringMatchCandidate::new(13, "ISO 8859-13"),
345                    StringMatchCandidate::new(14, "ISO 8859-15"),
346                    StringMatchCandidate::new(15, "KOI8-R"),
347                    StringMatchCandidate::new(16, "KOI8-U"),
348                    StringMatchCandidate::new(17, "MacRoman"),
349                    StringMatchCandidate::new(18, "Mac Cyrillic"),
350                    StringMatchCandidate::new(19, "Windows-874"),
351                    StringMatchCandidate::new(20, "Windows-1253"),
352                    StringMatchCandidate::new(21, "Windows-1254"),
353                    StringMatchCandidate::new(22, "Windows-1255"),
354                    StringMatchCandidate::new(23, "Windows-1256"),
355                    StringMatchCandidate::new(24, "Windows-1257"),
356                    StringMatchCandidate::new(25, "Windows-1258"),
357                    StringMatchCandidate::new(26, "Windows-949"),
358                    StringMatchCandidate::new(27, "EUC-JP"),
359                    StringMatchCandidate::new(28, "ISO 2022-JP"),
360                    StringMatchCandidate::new(29, "GBK"),
361                    StringMatchCandidate::new(30, "GB18030"),
362                    StringMatchCandidate::new(31, "Big5"),
363                ],
364                matches: Vec::new(),
365                selector,
366                buffer: buffer,
367                action,
368            }
369        }
370    }
371
372    impl PickerDelegate for EncodingSelectorDelegate {
373        type ListItem = ListItem;
374
375        fn match_count(&self) -> usize {
376            self.matches.len()
377        }
378
379        fn selected_index(&self) -> usize {
380            self.current_selection
381        }
382
383        fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) {
384            self.current_selection = ix;
385        }
386
387        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
388            "Select an encoding...".into()
389        }
390
391        fn update_matches(
392            &mut self,
393            query: String,
394            window: &mut Window,
395            cx: &mut Context<Picker<Self>>,
396        ) -> gpui::Task<()> {
397            let executor = cx.background_executor().clone();
398            let encodings = self.encodings.clone();
399
400            cx.spawn_in(window, async move |picker, cx| {
401                let matches: Vec<StringMatch>;
402
403                if query.is_empty() {
404                    matches = encodings
405                        .into_iter()
406                        .enumerate()
407                        .map(|(index, value)| StringMatch {
408                            candidate_id: index,
409                            score: 0.0,
410                            positions: Vec::new(),
411                            string: value.string,
412                        })
413                        .collect();
414                } else {
415                    matches = fuzzy::match_strings(
416                        &encodings,
417                        &query,
418                        true,
419                        false,
420                        30,
421                        &AtomicBool::new(false),
422                        executor,
423                    )
424                    .await
425                }
426                picker
427                    .update(cx, |picker, cx| {
428                        let delegate = &mut picker.delegate;
429                        delegate.matches = matches;
430                        delegate.current_selection = delegate
431                            .current_selection
432                            .min(delegate.matches.len().saturating_sub(1));
433                        cx.notify();
434                    })
435                    .log_err();
436            })
437        }
438
439        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
440            let workspace = self
441                .selector
442                .upgrade()
443                .unwrap()
444                .read(cx)
445                .workspace
446                .upgrade()
447                .unwrap();
448
449            let weak_workspace = workspace.read(cx).weak_handle();
450
451            let current_selection = self.matches[self.current_selection].string.clone();
452
453            if let Some(buffer) = &self.buffer
454                && let Some(buffer) = buffer.upgrade()
455            {
456                let path = self
457                    .selector
458                    .upgrade()
459                    .unwrap()
460                    .read(cx)
461                    .path
462                    .clone()
463                    .unwrap();
464
465                let reload = buffer.update(cx, |buffer, cx| buffer.reload(cx));
466                // Since the encoding will be accessed in `reload`,
467                // the lock must be released before calling `reload`.
468                // By limiting the scope, we ensure that it is released
469
470                {
471                    let buffer = buffer.read(cx);
472
473                    let buffer_encoding = buffer.encoding.clone();
474                    buffer_encoding.set(encoding_from_name(&current_selection.clone()));
475                }
476
477                self.dismissed(window, cx);
478
479                if self.action == Action::Reopen {
480                    buffer.update(cx, |_, cx| {
481                        cx.spawn_in(window, async move |_, cx| {
482                            if let Err(_) | Ok(None) = reload.await {
483                                let workspace = weak_workspace.upgrade().unwrap();
484
485                                workspace
486                                    .update_in(cx, |workspace, window, cx| {
487                                        workspace
488                                            .encoding_options
489                                            .encoding
490                                            .lock()
491                                            .unwrap()
492                                            .set(encoding_from_name(&current_selection));
493
494                                        *workspace.encoding_options.force.get_mut() = false;
495
496                                        *workspace.encoding_options.detect_utf16.get_mut() = true;
497
498                                        workspace
499                                            .active_pane()
500                                            .update(cx, |pane, cx| {
501                                                pane.close_active_item(
502                                                    &CloseActiveItem::default(),
503                                                    window,
504                                                    cx,
505                                                )
506                                            })
507                                            .detach();
508
509                                        workspace
510                                            .open_abs_path(path, OpenOptions::default(), window, cx)
511                                            .detach()
512                                    })
513                                    .log_err();
514                            }
515                        })
516                        .detach()
517                    });
518                } else if self.action == Action::Save {
519                    workspace.update(cx, |workspace, cx| {
520                        workspace
521                            .save_active_item(workspace::SaveIntent::Save, window, cx)
522                            .detach();
523                    });
524                }
525            } else {
526                if let Some(path) = self.selector.upgrade().unwrap().read(cx).path.clone() {
527                    workspace.update(cx, |workspace, cx| {
528                        workspace.active_pane().update(cx, |pane, cx| {
529                            pane.close_active_item(&CloseActiveItem::default(), window, cx)
530                                .detach();
531                        });
532                    });
533
534                    let encoding =
535                        encoding_from_name(self.matches[self.current_selection].string.as_str());
536
537                    let open_task = workspace.update(cx, |workspace, cx| {
538                        workspace
539                            .encoding_options
540                            .encoding
541                            .lock()
542                            .unwrap()
543                            .set(encoding);
544
545                        workspace.open_abs_path(path, OpenOptions::default(), window, cx)
546                    });
547
548                    cx.spawn(async move |_, cx| {
549                        if let Ok(_) = {
550                            let result = open_task.await;
551                            workspace
552                                .update(cx, |workspace, _| {
553                                    *workspace.encoding_options.force.get_mut() = false;
554                                })
555                                .unwrap();
556
557                            result
558                        } && let Ok(Ok((_, buffer, _))) =
559                            workspace.read_with(cx, |workspace, cx| {
560                                if let Some(active_item) = workspace.active_item(cx)
561                                    && let Some(editor) = active_item.act_as::<Editor>(cx)
562                                {
563                                    Ok(editor.read(cx).active_excerpt(cx).unwrap())
564                                } else {
565                                    Err(anyhow!("error"))
566                                }
567                            })
568                        {
569                            buffer
570                                .read_with(cx, |buffer, _| {
571                                    buffer.encoding.set(encoding);
572                                })
573                                .log_err();
574                        }
575                    })
576                    .detach();
577                }
578            }
579        }
580
581        fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
582            self.selector
583                .update(cx, |_, cx| cx.emit(DismissEvent))
584                .log_err();
585        }
586
587        fn render_match(
588            &self,
589            ix: usize,
590            _: bool,
591            _: &mut Window,
592            _: &mut Context<Picker<Self>>,
593        ) -> Option<Self::ListItem> {
594            Some(
595                ListItem::new(ix)
596                    .child(HighlightedLabel::new(
597                        &self.matches[ix].string,
598                        self.matches[ix].positions.clone(),
599                    ))
600                    .spacing(ListItemSpacing::Sparse),
601            )
602        }
603    }
604
605    /// The action to perform after selecting an encoding.
606    #[derive(PartialEq, Clone)]
607    pub enum Action {
608        Save,
609        Reopen,
610    }
611
612    impl EncodingSelector {
613        pub fn new(
614            window: &mut Window,
615            cx: &mut Context<EncodingSelector>,
616            action: Action,
617            buffer: Option<WeakEntity<Buffer>>,
618            workspace: WeakEntity<Workspace>,
619            path: Option<PathBuf>,
620        ) -> EncodingSelector {
621            let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action);
622            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
623
624            EncodingSelector {
625                picker,
626                workspace,
627                path,
628            }
629        }
630    }
631
632    impl EventEmitter<DismissEvent> for EncodingSelector {}
633
634    impl Focusable for EncodingSelector {
635        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
636            self.picker.focus_handle(cx)
637        }
638    }
639
640    impl ModalView for EncodingSelector {}
641
642    impl Render for EncodingSelector {
643        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
644            v_flex().w(rems(34.0)).child(self.picker.clone())
645        }
646    }
647}