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, Workspace};
 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::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.clone())
 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::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 smart_case = query.chars().any(|c| c.is_uppercase());
125            self.matches = smol::block_on(fuzzy::match_strings(
126                candidates.as_slice(),
127                query,
128                smart_case,
129                true,
130                100,
131                &Default::default(),
132                cx.background_executor().clone(),
133            ));
134            self.matches.sort_unstable_by_key(|m| m.candidate_id);
135
136            self.selected_index = self
137                .matches
138                .iter()
139                .enumerate()
140                .rev()
141                .max_by_key(|(_, m)| OrderedFloat(m.score))
142                .map(|(index, _)| index)
143                .unwrap_or(0);
144        }
145
146        Task::ready(())
147    }
148
149    fn confirm(&mut self, secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
150        if let Some(distro) = self.matches.get(self.selected_index) {
151            cx.emit(WslDistroSelected {
152                secondary,
153                distro: distro.string.clone(),
154            });
155        }
156    }
157
158    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
159        cx.emit(WslPickerDismissed);
160    }
161
162    fn render_match(
163        &self,
164        ix: usize,
165        selected: bool,
166        _: &mut Window,
167        _: &mut Context<Picker<Self>>,
168    ) -> Option<Self::ListItem> {
169        let matched = self.matches.get(ix)?;
170        Some(
171            ListItem::new(ix)
172                .toggle_state(selected)
173                .inset(true)
174                .spacing(ui::ListItemSpacing::Sparse)
175                .child(
176                    h_flex()
177                        .flex_grow()
178                        .gap_3()
179                        .child(Icon::new(IconName::Linux))
180                        .child(v_flex().child(HighlightedLabel::new(
181                            matched.string.clone(),
182                            matched.positions.clone(),
183                        ))),
184                ),
185        )
186    }
187}
188
189pub(crate) struct WslOpenModal {
190    paths: Vec<PathBuf>,
191    create_new_window: bool,
192    picker: Entity<Picker<WslPickerDelegate>>,
193    _subscriptions: [Subscription; 2],
194}
195
196impl WslOpenModal {
197    pub fn new(
198        paths: Vec<PathBuf>,
199        create_new_window: bool,
200        window: &mut Window,
201        cx: &mut Context<Self>,
202    ) -> Self {
203        let delegate = WslPickerDelegate::new();
204        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
205
206        let selected = cx.subscribe_in(
207            &picker,
208            window,
209            |this, _, event: &WslDistroSelected, window, cx| {
210                this.confirm(&event.distro, event.secondary, window, cx);
211            },
212        );
213
214        let dismissed = cx.subscribe_in(
215            &picker,
216            window,
217            |this, _, _: &WslPickerDismissed, window, cx| {
218                this.cancel(&menu::Cancel, window, cx);
219            },
220        );
221
222        WslOpenModal {
223            paths,
224            create_new_window,
225            picker,
226            _subscriptions: [selected, dismissed],
227        }
228    }
229
230    fn confirm(
231        &mut self,
232        distro: &str,
233        secondary: bool,
234        window: &mut Window,
235        cx: &mut Context<Self>,
236    ) {
237        let app_state = workspace::AppState::global(cx);
238        let Some(app_state) = app_state.upgrade() else {
239            return;
240        };
241
242        let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions {
243            distro_name: distro.to_string(),
244            user: None,
245        });
246
247        let replace_current_window = match self.create_new_window {
248            true => secondary,
249            false => !secondary,
250        };
251        let replace_window = match replace_current_window {
252            true => window.window_handle().downcast::<Workspace>(),
253            false => None,
254        };
255
256        let paths = self.paths.clone();
257        let open_options = workspace::OpenOptions {
258            replace_window,
259            ..Default::default()
260        };
261
262        cx.emit(DismissEvent);
263        cx.spawn_in(window, async move |_, cx| {
264            open_remote_project(connection_options, paths, app_state, open_options, cx).await
265        })
266        .detach();
267    }
268
269    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
270        cx.emit(DismissEvent);
271    }
272}
273
274impl ModalView for WslOpenModal {}
275
276impl Focusable for WslOpenModal {
277    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
278        self.picker.focus_handle(cx)
279    }
280}
281
282impl EventEmitter<DismissEvent> for WslOpenModal {}
283
284impl Render for WslOpenModal {
285    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
286        div()
287            .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent)))
288            .on_action(cx.listener(Self::cancel))
289            .elevation_3(cx)
290            .w(rems(34.))
291            .flex_1()
292            .overflow_hidden()
293            .child(self.picker.clone())
294    }
295}