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                                            .set(encoding_from_name(&current_selection));
491
492                                        *workspace.encoding_options.force.get_mut() = false;
493
494                                        *workspace.encoding_options.detect_utf16.get_mut() = true;
495
496                                        workspace
497                                            .active_pane()
498                                            .update(cx, |pane, cx| {
499                                                pane.close_active_item(
500                                                    &CloseActiveItem::default(),
501                                                    window,
502                                                    cx,
503                                                )
504                                            })
505                                            .detach();
506
507                                        workspace
508                                            .open_abs_path(path, OpenOptions::default(), window, cx)
509                                            .detach()
510                                    })
511                                    .log_err();
512                            }
513                        })
514                        .detach()
515                    });
516                } else if self.action == Action::Save {
517                    workspace.update(cx, |workspace, cx| {
518                        workspace
519                            .save_active_item(workspace::SaveIntent::Save, window, cx)
520                            .detach();
521                    });
522                }
523            } else {
524                if let Some(path) = self.selector.upgrade().unwrap().read(cx).path.clone() {
525                    workspace.update(cx, |workspace, cx| {
526                        workspace.active_pane().update(cx, |pane, cx| {
527                            pane.close_active_item(&CloseActiveItem::default(), window, cx)
528                                .detach();
529                        });
530                    });
531
532                    let encoding =
533                        encoding_from_name(self.matches[self.current_selection].string.as_str());
534
535                    let open_task = workspace.update(cx, |workspace, cx| {
536                        workspace.encoding_options.encoding.set(encoding);
537
538                        workspace.open_abs_path(path, OpenOptions::default(), window, cx)
539                    });
540
541                    cx.spawn(async move |_, cx| {
542                        if let Ok(_) = {
543                            let result = open_task.await;
544                            workspace
545                                .update(cx, |workspace, _| {
546                                    *workspace.encoding_options.force.get_mut() = false;
547                                })
548                                .unwrap();
549
550                            result
551                        } && let Ok(Ok((_, buffer, _))) =
552                            workspace.read_with(cx, |workspace, cx| {
553                                if let Some(active_item) = workspace.active_item(cx)
554                                    && let Some(editor) = active_item.act_as::<Editor>(cx)
555                                {
556                                    Ok(editor.read(cx).active_excerpt(cx).unwrap())
557                                } else {
558                                    Err(anyhow!("error"))
559                                }
560                            })
561                        {
562                            buffer
563                                .read_with(cx, |buffer, _| {
564                                    buffer.encoding.set(encoding);
565                                })
566                                .log_err();
567                        }
568                    })
569                    .detach();
570                }
571            }
572        }
573
574        fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
575            self.selector
576                .update(cx, |_, cx| cx.emit(DismissEvent))
577                .log_err();
578        }
579
580        fn render_match(
581            &self,
582            ix: usize,
583            _: bool,
584            _: &mut Window,
585            _: &mut Context<Picker<Self>>,
586        ) -> Option<Self::ListItem> {
587            Some(
588                ListItem::new(ix)
589                    .child(HighlightedLabel::new(
590                        &self.matches[ix].string,
591                        self.matches[ix].positions.clone(),
592                    ))
593                    .spacing(ListItemSpacing::Sparse),
594            )
595        }
596    }
597
598    /// The action to perform after selecting an encoding.
599    #[derive(PartialEq, Clone)]
600    pub enum Action {
601        Save,
602        Reopen,
603    }
604
605    impl EncodingSelector {
606        pub fn new(
607            window: &mut Window,
608            cx: &mut Context<EncodingSelector>,
609            action: Action,
610            buffer: Option<WeakEntity<Buffer>>,
611            workspace: WeakEntity<Workspace>,
612            path: Option<PathBuf>,
613        ) -> EncodingSelector {
614            let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action);
615            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
616
617            EncodingSelector {
618                picker,
619                workspace,
620                path,
621            }
622        }
623    }
624
625    impl EventEmitter<DismissEvent> for EncodingSelector {}
626
627    impl Focusable for EncodingSelector {
628        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
629            self.picker.focus_handle(cx)
630        }
631    }
632
633    impl ModalView for EncodingSelector {}
634
635    impl Render for EncodingSelector {
636        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
637            v_flex().w(rems(34.0)).child(self.picker.clone())
638        }
639    }
640}