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