wsl_picker.rs

  1use std::{path::PathBuf, sync::Arc};
  2
  3use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Subscription, Task};
  4use picker::Picker;
  5use remote::{RemoteConnectionOptions, WslConnectionOptions};
  6use ui::{
  7    App, Context, HighlightedLabel, Icon, IconName, InteractiveElement, ListItem, ParentElement,
  8    Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex,
  9};
 10use util::ResultExt as _;
 11use workspace::{ModalView, MultiWorkspace};
 12
 13use crate::open_remote_project;
 14
 15#[derive(Clone, Debug)]
 16pub struct WslDistroSelected {
 17    pub secondary: bool,
 18    pub distro: String,
 19}
 20
 21#[derive(Clone, Debug)]
 22pub struct WslPickerDismissed;
 23
 24pub(crate) struct WslPickerDelegate {
 25    selected_index: usize,
 26    distro_list: Option<Vec<String>>,
 27    matches: Vec<fuzzy_nucleo::StringMatch>,
 28}
 29
 30impl WslPickerDelegate {
 31    pub fn new() -> Self {
 32        WslPickerDelegate {
 33            selected_index: 0,
 34            distro_list: None,
 35            matches: Vec::new(),
 36        }
 37    }
 38
 39    pub fn selected_distro(&self) -> Option<String> {
 40        self.matches
 41            .get(self.selected_index)
 42            .map(|m| m.string.to_string())
 43    }
 44}
 45
 46impl WslPickerDelegate {
 47    fn fetch_distros() -> anyhow::Result<Vec<String>> {
 48        use anyhow::Context;
 49        use windows_registry::CURRENT_USER;
 50
 51        let lxss_key = CURRENT_USER
 52            .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss")
 53            .context("failed to get lxss wsl key")?;
 54
 55        let distros = lxss_key
 56            .keys()
 57            .context("failed to get wsl distros")?
 58            .filter_map(|key| {
 59                lxss_key
 60                    .open(&key)
 61                    .context("failed to open subkey for distro")
 62                    .log_err()
 63            })
 64            .filter_map(|distro| distro.get_string("DistributionName").ok())
 65            .collect::<Vec<_>>();
 66
 67        Ok(distros)
 68    }
 69}
 70
 71impl EventEmitter<WslDistroSelected> for Picker<WslPickerDelegate> {}
 72
 73impl EventEmitter<WslPickerDismissed> for Picker<WslPickerDelegate> {}
 74
 75impl picker::PickerDelegate for WslPickerDelegate {
 76    type ListItem = ListItem;
 77
 78    fn match_count(&self) -> usize {
 79        self.matches.len()
 80    }
 81
 82    fn selected_index(&self) -> usize {
 83        self.selected_index
 84    }
 85
 86    fn set_selected_index(
 87        &mut self,
 88        ix: usize,
 89        _window: &mut Window,
 90        cx: &mut Context<Picker<Self>>,
 91    ) {
 92        self.selected_index = ix;
 93        cx.notify();
 94    }
 95
 96    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 97        Arc::from("Enter WSL distro name")
 98    }
 99
100    fn update_matches(
101        &mut self,
102        query: String,
103        _window: &mut Window,
104        _cx: &mut Context<Picker<Self>>,
105    ) -> Task<()> {
106        use fuzzy_nucleo::StringMatchCandidate;
107
108        let needs_fetch = self.distro_list.is_none();
109        if needs_fetch {
110            let distros = Self::fetch_distros().log_err();
111            self.distro_list = distros;
112        }
113
114        if let Some(distro_list) = &self.distro_list {
115            use ordered_float::OrderedFloat;
116
117            let candidates = distro_list
118                .iter()
119                .enumerate()
120                .map(|(id, distro)| StringMatchCandidate::new(id, distro))
121                .collect::<Vec<_>>();
122
123            let query = query.trim_start();
124            let case = fuzzy_nucleo::Case::smart_if_uppercase_in(query);
125            self.matches = fuzzy_nucleo::match_strings(
126                &candidates,
127                query,
128                case,
129                fuzzy_nucleo::LengthPenalty::On,
130                100,
131            );
132            self.matches.sort_unstable_by_key(|m| m.candidate_id);
133
134            self.selected_index = self
135                .matches
136                .iter()
137                .enumerate()
138                .rev()
139                .max_by_key(|(_, m)| OrderedFloat(m.score))
140                .map(|(index, _)| index)
141                .unwrap_or(0);
142        }
143
144        Task::ready(())
145    }
146
147    fn confirm(&mut self, secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
148        if let Some(distro) = self.matches.get(self.selected_index) {
149            cx.emit(WslDistroSelected {
150                secondary,
151                distro: distro.string.to_string(),
152            });
153        }
154    }
155
156    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
157        cx.emit(WslPickerDismissed);
158    }
159
160    fn render_match(
161        &self,
162        ix: usize,
163        selected: bool,
164        _: &mut Window,
165        _: &mut Context<Picker<Self>>,
166    ) -> Option<Self::ListItem> {
167        let matched = self.matches.get(ix)?;
168        Some(
169            ListItem::new(ix)
170                .toggle_state(selected)
171                .inset(true)
172                .spacing(ui::ListItemSpacing::Sparse)
173                .child(
174                    h_flex()
175                        .flex_grow()
176                        .gap_3()
177                        .child(Icon::new(IconName::Linux))
178                        .child(v_flex().child(HighlightedLabel::new(
179                            matched.string.clone(),
180                            matched.positions.clone(),
181                        ))),
182                ),
183        )
184    }
185}
186
187pub(crate) struct WslOpenModal {
188    paths: Vec<PathBuf>,
189    create_new_window: bool,
190    picker: Entity<Picker<WslPickerDelegate>>,
191    _subscriptions: [Subscription; 2],
192}
193
194impl WslOpenModal {
195    pub fn new(
196        paths: Vec<PathBuf>,
197        create_new_window: bool,
198        window: &mut Window,
199        cx: &mut Context<Self>,
200    ) -> Self {
201        let delegate = WslPickerDelegate::new();
202        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
203
204        let selected = cx.subscribe_in(
205            &picker,
206            window,
207            |this, _, event: &WslDistroSelected, window, cx| {
208                this.confirm(&event.distro, event.secondary, window, cx);
209            },
210        );
211
212        let dismissed = cx.subscribe_in(
213            &picker,
214            window,
215            |this, _, _: &WslPickerDismissed, window, cx| {
216                this.cancel(&menu::Cancel, window, cx);
217            },
218        );
219
220        WslOpenModal {
221            paths,
222            create_new_window,
223            picker,
224            _subscriptions: [selected, dismissed],
225        }
226    }
227
228    fn confirm(
229        &mut self,
230        distro: &str,
231        secondary: bool,
232        window: &mut Window,
233        cx: &mut Context<Self>,
234    ) {
235        let app_state = workspace::AppState::global(cx);
236
237        let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions {
238            distro_name: distro.to_string(),
239            user: None,
240        });
241
242        let replace_current_window = match self.create_new_window {
243            true => secondary,
244            false => !secondary,
245        };
246        let open_mode = if replace_current_window {
247            workspace::OpenMode::Activate
248        } else {
249            workspace::OpenMode::NewWindow
250        };
251
252        let paths = self.paths.clone();
253        let open_options = workspace::OpenOptions {
254            requesting_window: window.window_handle().downcast::<MultiWorkspace>(),
255            open_mode,
256            ..Default::default()
257        };
258
259        cx.emit(DismissEvent);
260        cx.spawn_in(window, async move |_, cx| {
261            open_remote_project(connection_options, paths, app_state, open_options, cx).await
262        })
263        .detach();
264    }
265
266    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
267        cx.emit(DismissEvent);
268    }
269}
270
271impl ModalView for WslOpenModal {}
272
273impl Focusable for WslOpenModal {
274    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
275        self.picker.focus_handle(cx)
276    }
277}
278
279impl EventEmitter<DismissEvent> for WslOpenModal {}
280
281impl Render for WslOpenModal {
282    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
283        div()
284            .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent)))
285            .on_action(cx.listener(Self::cancel))
286            .elevation_3(cx)
287            .w(rems(34.))
288            .flex_1()
289            .overflow_hidden()
290            .child(self.picker.clone())
291    }
292}