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                    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 editor::Editor;
281    use fs::encodings::EncodingWrapper;
282    use std::{path::PathBuf, sync::atomic::AtomicBool};
283
284    use fuzzy::{StringMatch, StringMatchCandidate};
285    use gpui::{
286        AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, http_client::anyhow,
287    };
288    use language::Buffer;
289    use picker::{Picker, PickerDelegate};
290    use ui::{
291        Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled,
292        Window, rems, v_flex,
293    };
294    use util::{ResultExt, TryFutureExt};
295    use workspace::{CloseActiveItem, ModalView, OpenOptions, Workspace};
296
297    use crate::encoding_from_name;
298
299    /// A modal view that allows the user to select an encoding from a list of encodings.
300    pub struct EncodingSelector {
301        picker: Entity<Picker<EncodingSelectorDelegate>>,
302        workspace: WeakEntity<Workspace>,
303        path: Option<PathBuf>,
304    }
305
306    pub struct EncodingSelectorDelegate {
307        current_selection: usize,
308        encodings: Vec<StringMatchCandidate>,
309        matches: Vec<StringMatch>,
310        selector: WeakEntity<EncodingSelector>,
311        buffer: Option<WeakEntity<Buffer>>,
312        action: Action,
313    }
314
315    impl EncodingSelectorDelegate {
316        pub fn new(
317            selector: WeakEntity<EncodingSelector>,
318            buffer: Option<WeakEntity<Buffer>>,
319            action: Action,
320        ) -> EncodingSelectorDelegate {
321            EncodingSelectorDelegate {
322                current_selection: 0,
323                encodings: vec![
324                    StringMatchCandidate::new(0, "UTF-8"),
325                    StringMatchCandidate::new(1, "UTF-16 LE"),
326                    StringMatchCandidate::new(2, "UTF-16 BE"),
327                    StringMatchCandidate::new(3, "Windows-1252"),
328                    StringMatchCandidate::new(4, "Windows-1251"),
329                    StringMatchCandidate::new(5, "Windows-1250"),
330                    StringMatchCandidate::new(6, "ISO 8859-2"),
331                    StringMatchCandidate::new(7, "ISO 8859-3"),
332                    StringMatchCandidate::new(8, "ISO 8859-4"),
333                    StringMatchCandidate::new(9, "ISO 8859-5"),
334                    StringMatchCandidate::new(10, "ISO 8859-6"),
335                    StringMatchCandidate::new(11, "ISO 8859-7"),
336                    StringMatchCandidate::new(12, "ISO 8859-8"),
337                    StringMatchCandidate::new(13, "ISO 8859-13"),
338                    StringMatchCandidate::new(14, "ISO 8859-15"),
339                    StringMatchCandidate::new(15, "KOI8-R"),
340                    StringMatchCandidate::new(16, "KOI8-U"),
341                    StringMatchCandidate::new(17, "MacRoman"),
342                    StringMatchCandidate::new(18, "Mac Cyrillic"),
343                    StringMatchCandidate::new(19, "Windows-874"),
344                    StringMatchCandidate::new(20, "Windows-1253"),
345                    StringMatchCandidate::new(21, "Windows-1254"),
346                    StringMatchCandidate::new(22, "Windows-1255"),
347                    StringMatchCandidate::new(23, "Windows-1256"),
348                    StringMatchCandidate::new(24, "Windows-1257"),
349                    StringMatchCandidate::new(25, "Windows-1258"),
350                    StringMatchCandidate::new(26, "Windows-949"),
351                    StringMatchCandidate::new(27, "EUC-JP"),
352                    StringMatchCandidate::new(28, "ISO 2022-JP"),
353                    StringMatchCandidate::new(29, "GBK"),
354                    StringMatchCandidate::new(30, "GB18030"),
355                    StringMatchCandidate::new(31, "Big5"),
356                ],
357                matches: Vec::new(),
358                selector,
359                buffer: buffer,
360                action,
361            }
362        }
363    }
364
365    impl PickerDelegate for EncodingSelectorDelegate {
366        type ListItem = ListItem;
367
368        fn match_count(&self) -> usize {
369            self.matches.len()
370        }
371
372        fn selected_index(&self) -> usize {
373            self.current_selection
374        }
375
376        fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) {
377            self.current_selection = ix;
378        }
379
380        fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
381            "Select an encoding...".into()
382        }
383
384        fn update_matches(
385            &mut self,
386            query: String,
387            window: &mut Window,
388            cx: &mut Context<Picker<Self>>,
389        ) -> gpui::Task<()> {
390            let executor = cx.background_executor().clone();
391            let encodings = self.encodings.clone();
392
393            cx.spawn_in(window, async move |picker, cx| {
394                let matches: Vec<StringMatch>;
395
396                if query.is_empty() {
397                    matches = encodings
398                        .into_iter()
399                        .enumerate()
400                        .map(|(index, value)| StringMatch {
401                            candidate_id: index,
402                            score: 0.0,
403                            positions: Vec::new(),
404                            string: value.string,
405                        })
406                        .collect();
407                } else {
408                    matches = fuzzy::match_strings(
409                        &encodings,
410                        &query,
411                        true,
412                        false,
413                        30,
414                        &AtomicBool::new(false),
415                        executor,
416                    )
417                    .await
418                }
419                picker
420                    .update(cx, |picker, cx| {
421                        let delegate = &mut picker.delegate;
422                        delegate.matches = matches;
423                        delegate.current_selection = delegate
424                            .current_selection
425                            .min(delegate.matches.len().saturating_sub(1));
426                        cx.notify();
427                    })
428                    .log_err();
429            })
430        }
431
432        fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
433            let workspace = self
434                .selector
435                .upgrade()
436                .unwrap()
437                .read(cx)
438                .workspace
439                .upgrade()
440                .unwrap();
441
442            if let Some(buffer) = &self.buffer
443                && let Some(buffer_entity) = buffer.upgrade()
444            {
445                let buffer = buffer_entity.read(cx);
446
447                // Since the encoding will be accessed in `reload`,
448                // the lock must be released before calling `reload`.
449                // By limiting the scope, we ensure that it is released
450                {
451                    let buffer_encoding = buffer.encoding.clone();
452                    *buffer_encoding.lock().unwrap() =
453                        encoding_from_name(self.matches[self.current_selection].string.as_str());
454                }
455
456                self.dismissed(window, cx);
457
458                if self.action == Action::Reopen {
459                    buffer_entity.update(cx, |buffer, cx| {
460                        let rec = buffer.reload(cx);
461                        cx.spawn(async move |_, _| rec.await).detach()
462                    });
463                } else if self.action == Action::Save {
464                    let task = workspace.update(cx, |workspace, cx| {
465                        workspace
466                            .save_active_item(workspace::SaveIntent::Save, window, cx)
467                            .log_err()
468                    });
469                    cx.spawn(async |_, _| task).detach();
470                }
471            } else {
472                if let Some(path) = self.selector.upgrade().unwrap().read(cx).path.clone() {
473                    workspace.update(cx, |workspace, cx| {
474                        workspace.active_pane().update(cx, |pane, cx| {
475                            pane.close_active_item(&CloseActiveItem::default(), window, cx)
476                                .detach();
477                        });
478                    });
479
480                    let encoding =
481                        encoding_from_name(self.matches[self.current_selection].string.as_str());
482
483                    let open_task = workspace.update(cx, |workspace, cx| {
484                        *workspace.encoding_options.encoding.lock().unwrap() =
485                            EncodingWrapper::new(encoding);
486
487                        workspace.open_abs_path(path, OpenOptions::default(), window, cx)
488                    });
489
490                    cx.spawn(async move |_, cx| {
491                        if let Ok(_) = {
492                            let result = open_task.await;
493                            workspace
494                                .update(cx, |workspace, _| {
495                                    *workspace.encoding_options.force.get_mut() = false;
496                                })
497                                .unwrap();
498
499                            result
500                        } && let Ok(Ok((_, buffer, _))) =
501                            workspace.read_with(cx, |workspace, cx| {
502                                if let Some(active_item) = workspace.active_item(cx)
503                                    && let Some(editor) = active_item.act_as::<Editor>(cx)
504                                {
505                                    Ok(editor.read(cx).active_excerpt(cx).unwrap())
506                                } else {
507                                    Err(anyhow!("error"))
508                                }
509                            })
510                        {
511                            buffer
512                                .read_with(cx, |buffer, _| {
513                                    *buffer.encoding.lock().unwrap() = encoding;
514                                })
515                                .log_err();
516                        }
517                    })
518                    .detach();
519                }
520            }
521        }
522
523        fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
524            self.selector
525                .update(cx, |_, cx| cx.emit(DismissEvent))
526                .log_err();
527        }
528
529        fn render_match(
530            &self,
531            ix: usize,
532            _: bool,
533            _: &mut Window,
534            _: &mut Context<Picker<Self>>,
535        ) -> Option<Self::ListItem> {
536            Some(
537                ListItem::new(ix)
538                    .child(HighlightedLabel::new(
539                        &self.matches[ix].string,
540                        self.matches[ix].positions.clone(),
541                    ))
542                    .spacing(ListItemSpacing::Sparse),
543            )
544        }
545    }
546
547    /// The action to perform after selecting an encoding.
548    #[derive(PartialEq, Clone)]
549    pub enum Action {
550        Save,
551        Reopen,
552    }
553
554    impl EncodingSelector {
555        pub fn new(
556            window: &mut Window,
557            cx: &mut Context<EncodingSelector>,
558            action: Action,
559            buffer: Option<WeakEntity<Buffer>>,
560            workspace: WeakEntity<Workspace>,
561            path: Option<PathBuf>,
562        ) -> EncodingSelector {
563            let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action);
564            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
565
566            EncodingSelector {
567                picker,
568                workspace,
569                path,
570            }
571        }
572    }
573
574    impl EventEmitter<DismissEvent> for EncodingSelector {}
575
576    impl Focusable for EncodingSelector {
577        fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
578            self.picker.focus_handle(cx)
579        }
580    }
581
582    impl ModalView for EncodingSelector {}
583
584    impl Render for EncodingSelector {
585        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
586            v_flex().w(rems(34.0)).child(self.picker.clone())
587        }
588    }
589}