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}