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