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}