1mod active_toolchain;
2
3pub use active_toolchain::ActiveToolchain;
4use editor::Editor;
5use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
6use gpui::{
7 App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement,
8 Render, Styled, Task, WeakEntity, Window, actions,
9};
10use language::{LanguageName, Toolchain, ToolchainList};
11use picker::{Picker, PickerDelegate};
12use project::{Project, ProjectPath, WorktreeId};
13use std::{path::Path, sync::Arc};
14use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
15use util::ResultExt;
16use workspace::{ModalView, Workspace};
17
18actions!(toolchain, [Select]);
19
20pub fn init(cx: &mut App) {
21 cx.observe_new(ToolchainSelector::register).detach();
22}
23
24pub struct ToolchainSelector {
25 picker: Entity<Picker<ToolchainSelectorDelegate>>,
26}
27
28impl ToolchainSelector {
29 fn register(
30 workspace: &mut Workspace,
31 _window: Option<&mut Window>,
32 _: &mut Context<Workspace>,
33 ) {
34 workspace.register_action(move |workspace, _: &Select, window, cx| {
35 Self::toggle(workspace, window, cx);
36 });
37 }
38
39 fn toggle(
40 workspace: &mut Workspace,
41 window: &mut Window,
42 cx: &mut Context<Workspace>,
43 ) -> Option<()> {
44 let (_, buffer, _) = workspace
45 .active_item(cx)?
46 .act_as::<Editor>(cx)?
47 .read(cx)
48 .active_excerpt(cx)?;
49 let project = workspace.project().clone();
50
51 let language_name = buffer.read(cx).language()?.name();
52 let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
53 let worktree_root_path = project
54 .read(cx)
55 .worktree_for_id(worktree_id, cx)?
56 .read(cx)
57 .abs_path();
58 let workspace_id = workspace.database_id()?;
59 let weak = workspace.weak_handle();
60 cx.spawn_in(window, async move |workspace, cx| {
61 let active_toolchain = workspace::WORKSPACE_DB
62 .toolchain(workspace_id, worktree_id, language_name.clone())
63 .await
64 .ok()
65 .flatten();
66 workspace
67 .update_in(cx, |this, window, cx| {
68 this.toggle_modal(window, cx, move |window, cx| {
69 ToolchainSelector::new(
70 weak,
71 project,
72 active_toolchain,
73 worktree_id,
74 worktree_root_path,
75 language_name,
76 window,
77 cx,
78 )
79 });
80 })
81 .ok();
82 })
83 .detach();
84
85 Some(())
86 }
87
88 fn new(
89 workspace: WeakEntity<Workspace>,
90 project: Entity<Project>,
91 active_toolchain: Option<Toolchain>,
92 worktree_id: WorktreeId,
93 worktree_root: Arc<Path>,
94 language_name: LanguageName,
95 window: &mut Window,
96 cx: &mut Context<Self>,
97 ) -> Self {
98 let toolchain_selector = cx.entity().downgrade();
99 let picker = cx.new(|cx| {
100 let delegate = ToolchainSelectorDelegate::new(
101 active_toolchain,
102 toolchain_selector,
103 workspace,
104 worktree_id,
105 worktree_root,
106 project,
107 language_name,
108 window,
109 cx,
110 );
111 Picker::uniform_list(delegate, window, cx)
112 });
113 Self { picker }
114 }
115}
116
117impl Render for ToolchainSelector {
118 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
119 v_flex().w(rems(34.)).child(self.picker.clone())
120 }
121}
122
123impl Focusable for ToolchainSelector {
124 fn focus_handle(&self, cx: &App) -> FocusHandle {
125 self.picker.focus_handle(cx)
126 }
127}
128
129impl EventEmitter<DismissEvent> for ToolchainSelector {}
130impl ModalView for ToolchainSelector {}
131
132pub struct ToolchainSelectorDelegate {
133 toolchain_selector: WeakEntity<ToolchainSelector>,
134 candidates: ToolchainList,
135 matches: Vec<StringMatch>,
136 selected_index: usize,
137 workspace: WeakEntity<Workspace>,
138 worktree_id: WorktreeId,
139 worktree_abs_path_root: Arc<Path>,
140 placeholder_text: Arc<str>,
141 _fetch_candidates_task: Task<Option<()>>,
142}
143
144impl ToolchainSelectorDelegate {
145 fn new(
146 active_toolchain: Option<Toolchain>,
147 toolchain_selector: WeakEntity<ToolchainSelector>,
148 workspace: WeakEntity<Workspace>,
149 worktree_id: WorktreeId,
150 worktree_abs_path_root: Arc<Path>,
151 project: Entity<Project>,
152 language_name: LanguageName,
153 window: &mut Window,
154 cx: &mut Context<Picker<Self>>,
155 ) -> Self {
156 let _fetch_candidates_task = cx.spawn_in(window, {
157 let project = project.clone();
158 async move |this, cx| {
159 let term = project
160 .update(cx, |this, _| {
161 Project::toolchain_term(this.languages().clone(), language_name.clone())
162 })
163 .ok()?
164 .await?;
165 let placeholder_text = format!("Select a {}…", term.to_lowercase()).into();
166 let _ = this.update_in(cx, move |this, window, cx| {
167 this.delegate.placeholder_text = placeholder_text;
168 this.refresh_placeholder(window, cx);
169 });
170 let available_toolchains = project
171 .update(cx, |this, cx| {
172 this.available_toolchains(
173 ProjectPath {
174 worktree_id,
175 path: Arc::from("".as_ref()),
176 },
177 language_name,
178 cx,
179 )
180 })
181 .ok()?
182 .await?;
183
184 let _ = this.update_in(cx, move |this, window, cx| {
185 this.delegate.candidates = available_toolchains;
186
187 if let Some(active_toolchain) = active_toolchain {
188 if let Some(position) = this
189 .delegate
190 .candidates
191 .toolchains
192 .iter()
193 .position(|toolchain| *toolchain == active_toolchain)
194 {
195 this.delegate.set_selected_index(position, window, cx);
196 }
197 }
198 this.update_matches(this.query(cx), window, cx);
199 });
200
201 Some(())
202 }
203 });
204 let placeholder_text = "Select a toolchain…".to_string().into();
205 Self {
206 toolchain_selector,
207 candidates: Default::default(),
208 matches: vec![],
209 selected_index: 0,
210 workspace,
211 worktree_id,
212 worktree_abs_path_root,
213 placeholder_text,
214 _fetch_candidates_task,
215 }
216 }
217 fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString {
218 Path::new(&path.as_ref())
219 .strip_prefix(&worktree_root)
220 .ok()
221 .map(|suffix| Path::new(".").join(suffix))
222 .and_then(|path| path.to_str().map(String::from).map(SharedString::from))
223 .unwrap_or(path)
224 }
225}
226
227impl PickerDelegate for ToolchainSelectorDelegate {
228 type ListItem = ListItem;
229
230 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
231 self.placeholder_text.clone()
232 }
233
234 fn match_count(&self) -> usize {
235 self.matches.len()
236 }
237
238 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
239 if let Some(string_match) = self.matches.get(self.selected_index) {
240 let toolchain = self.candidates.toolchains[string_match.candidate_id].clone();
241 if let Some(workspace_id) = self
242 .workspace
243 .update(cx, |this, _| this.database_id())
244 .ok()
245 .flatten()
246 {
247 let workspace = self.workspace.clone();
248 let worktree_id = self.worktree_id;
249 cx.spawn_in(window, async move |_, cx| {
250 workspace::WORKSPACE_DB
251 .set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
252 .await
253 .log_err();
254 workspace
255 .update(cx, |this, cx| {
256 this.project().update(cx, |this, cx| {
257 this.activate_toolchain(
258 ProjectPath {
259 worktree_id,
260 path: Arc::from("".as_ref()),
261 },
262 toolchain,
263 cx,
264 )
265 })
266 })
267 .ok()?
268 .await;
269 Some(())
270 })
271 .detach();
272 }
273 }
274 self.dismissed(window, cx);
275 }
276
277 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
278 self.toolchain_selector
279 .update(cx, |_, cx| cx.emit(DismissEvent))
280 .log_err();
281 }
282
283 fn selected_index(&self) -> usize {
284 self.selected_index
285 }
286
287 fn set_selected_index(
288 &mut self,
289 ix: usize,
290 _window: &mut Window,
291 _: &mut Context<Picker<Self>>,
292 ) {
293 self.selected_index = ix;
294 }
295
296 fn update_matches(
297 &mut self,
298 query: String,
299 window: &mut Window,
300 cx: &mut Context<Picker<Self>>,
301 ) -> gpui::Task<()> {
302 let background = cx.background_executor().clone();
303 let candidates = self.candidates.clone();
304 let worktree_root_path = self.worktree_abs_path_root.clone();
305 cx.spawn_in(window, async move |this, cx| {
306 let matches = if query.is_empty() {
307 candidates
308 .toolchains
309 .into_iter()
310 .enumerate()
311 .map(|(index, candidate)| {
312 let path = Self::relativize_path(candidate.path, &worktree_root_path);
313 let string = format!("{}{}", candidate.name, path);
314 StringMatch {
315 candidate_id: index,
316 string,
317 positions: Vec::new(),
318 score: 0.0,
319 }
320 })
321 .collect()
322 } else {
323 let candidates = candidates
324 .toolchains
325 .into_iter()
326 .enumerate()
327 .map(|(candidate_id, toolchain)| {
328 let path = Self::relativize_path(toolchain.path, &worktree_root_path);
329 let string = format!("{}{}", toolchain.name, path);
330 StringMatchCandidate::new(candidate_id, &string)
331 })
332 .collect::<Vec<_>>();
333 match_strings(
334 &candidates,
335 &query,
336 false,
337 100,
338 &Default::default(),
339 background,
340 )
341 .await
342 };
343
344 this.update(cx, |this, cx| {
345 let delegate = &mut this.delegate;
346 delegate.matches = matches;
347 delegate.selected_index = delegate
348 .selected_index
349 .min(delegate.matches.len().saturating_sub(1));
350 cx.notify();
351 })
352 .log_err();
353 })
354 }
355
356 fn render_match(
357 &self,
358 ix: usize,
359 selected: bool,
360 _window: &mut Window,
361 _: &mut Context<Picker<Self>>,
362 ) -> Option<Self::ListItem> {
363 let mat = &self.matches[ix];
364 let toolchain = &self.candidates.toolchains[mat.candidate_id];
365
366 let label = toolchain.name.clone();
367 let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root);
368 let (name_highlights, mut path_highlights) = mat
369 .positions
370 .iter()
371 .cloned()
372 .partition::<Vec<_>, _>(|index| *index < label.len());
373 path_highlights.iter_mut().for_each(|index| {
374 *index -= label.len();
375 });
376 Some(
377 ListItem::new(ix)
378 .inset(true)
379 .spacing(ListItemSpacing::Sparse)
380 .toggle_state(selected)
381 .child(HighlightedLabel::new(label, name_highlights))
382 .child(
383 HighlightedLabel::new(path, path_highlights)
384 .size(LabelSize::Small)
385 .color(Color::Muted),
386 ),
387 )
388 }
389}