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 async move |this, cx| {
171 let term = project
172 .read_with(cx, |this, _| {
173 Project::toolchain_term(this.languages().clone(), language_name.clone())
174 })
175 .ok()?
176 .await?;
177 let relative_path = this
178 .read_with(cx, |this, _| this.delegate.relative_path.clone())
179 .ok()?;
180
181 let (available_toolchains, relative_path) = project
182 .update(cx, |this, cx| {
183 this.available_toolchains(
184 ProjectPath {
185 worktree_id,
186 path: relative_path.clone(),
187 },
188 language_name,
189 cx,
190 )
191 })
192 .ok()?
193 .await?;
194 let pretty_path = {
195 let path = relative_path.to_string_lossy();
196 if path.is_empty() {
197 Cow::Borrowed("worktree root")
198 } else {
199 Cow::Owned(format!("`{}`", path))
200 }
201 };
202 let placeholder_text =
203 format!("Select a {} for {pretty_path}…", term.to_lowercase(),).into();
204 let _ = this.update_in(cx, move |this, window, cx| {
205 this.delegate.relative_path = relative_path;
206 this.delegate.placeholder_text = placeholder_text;
207 this.refresh_placeholder(window, cx);
208 });
209
210 let _ = this.update_in(cx, move |this, window, cx| {
211 this.delegate.candidates = available_toolchains;
212
213 if let Some(active_toolchain) = active_toolchain
214 && let Some(position) = this
215 .delegate
216 .candidates
217 .toolchains
218 .iter()
219 .position(|toolchain| *toolchain == active_toolchain)
220 {
221 this.delegate.set_selected_index(position, window, cx);
222 }
223 this.update_matches(this.query(cx), window, cx);
224 });
225
226 Some(())
227 }
228 });
229 let placeholder_text = "Select a toolchain…".to_string().into();
230 Self {
231 toolchain_selector,
232 candidates: Default::default(),
233 matches: vec![],
234 selected_index: 0,
235 workspace,
236 worktree_id,
237 worktree_abs_path_root,
238 placeholder_text,
239 relative_path,
240 _fetch_candidates_task,
241 }
242 }
243 fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString {
244 Path::new(&path.as_ref())
245 .strip_prefix(&worktree_root)
246 .ok()
247 .map(|suffix| Path::new(".").join(suffix))
248 .and_then(|path| path.to_str().map(String::from).map(SharedString::from))
249 .unwrap_or(path)
250 }
251}
252
253impl PickerDelegate for ToolchainSelectorDelegate {
254 type ListItem = ListItem;
255
256 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
257 self.placeholder_text.clone()
258 }
259
260 fn match_count(&self) -> usize {
261 self.matches.len()
262 }
263
264 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
265 if let Some(string_match) = self.matches.get(self.selected_index) {
266 let toolchain = self.candidates.toolchains[string_match.candidate_id].clone();
267 if let Some(workspace_id) = self
268 .workspace
269 .read_with(cx, |this, _| this.database_id())
270 .ok()
271 .flatten()
272 {
273 let workspace = self.workspace.clone();
274 let worktree_id = self.worktree_id;
275 let path = self.relative_path.clone();
276 let relative_path = self.relative_path.to_string_lossy().into_owned();
277 cx.spawn_in(window, async move |_, cx| {
278 workspace::WORKSPACE_DB
279 .set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
280 .await
281 .log_err();
282 workspace
283 .update(cx, |this, cx| {
284 this.project().update(cx, |this, cx| {
285 this.activate_toolchain(
286 ProjectPath { worktree_id, path },
287 toolchain,
288 cx,
289 )
290 })
291 })
292 .ok()?
293 .await;
294 Some(())
295 })
296 .detach();
297 }
298 }
299 self.dismissed(window, cx);
300 }
301
302 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
303 self.toolchain_selector
304 .update(cx, |_, cx| cx.emit(DismissEvent))
305 .log_err();
306 }
307
308 fn selected_index(&self) -> usize {
309 self.selected_index
310 }
311
312 fn set_selected_index(
313 &mut self,
314 ix: usize,
315 _window: &mut Window,
316 _: &mut Context<Picker<Self>>,
317 ) {
318 self.selected_index = ix;
319 }
320
321 fn update_matches(
322 &mut self,
323 query: String,
324 window: &mut Window,
325 cx: &mut Context<Picker<Self>>,
326 ) -> gpui::Task<()> {
327 let background = cx.background_executor().clone();
328 let candidates = self.candidates.clone();
329 let worktree_root_path = self.worktree_abs_path_root.clone();
330 cx.spawn_in(window, async move |this, cx| {
331 let matches = if query.is_empty() {
332 candidates
333 .toolchains
334 .into_iter()
335 .enumerate()
336 .map(|(index, candidate)| {
337 let path = Self::relativize_path(candidate.path, &worktree_root_path);
338 let string = format!("{}{}", candidate.name, path);
339 StringMatch {
340 candidate_id: index,
341 string,
342 positions: Vec::new(),
343 score: 0.0,
344 }
345 })
346 .collect()
347 } else {
348 let candidates = candidates
349 .toolchains
350 .into_iter()
351 .enumerate()
352 .map(|(candidate_id, toolchain)| {
353 let path = Self::relativize_path(toolchain.path, &worktree_root_path);
354 let string = format!("{}{}", toolchain.name, path);
355 StringMatchCandidate::new(candidate_id, &string)
356 })
357 .collect::<Vec<_>>();
358 match_strings(
359 &candidates,
360 &query,
361 false,
362 true,
363 100,
364 &Default::default(),
365 background,
366 )
367 .await
368 };
369
370 this.update(cx, |this, cx| {
371 let delegate = &mut this.delegate;
372 delegate.matches = matches;
373 delegate.selected_index = delegate
374 .selected_index
375 .min(delegate.matches.len().saturating_sub(1));
376 cx.notify();
377 })
378 .log_err();
379 })
380 }
381
382 fn render_match(
383 &self,
384 ix: usize,
385 selected: bool,
386 _window: &mut Window,
387 _: &mut Context<Picker<Self>>,
388 ) -> Option<Self::ListItem> {
389 let mat = &self.matches[ix];
390 let toolchain = &self.candidates.toolchains[mat.candidate_id];
391
392 let label = toolchain.name.clone();
393 let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root);
394 let (name_highlights, mut path_highlights) = mat
395 .positions
396 .iter()
397 .cloned()
398 .partition::<Vec<_>, _>(|index| *index < label.len());
399 path_highlights.iter_mut().for_each(|index| {
400 *index -= label.len();
401 });
402 Some(
403 ListItem::new(ix)
404 .inset(true)
405 .spacing(ListItemSpacing::Sparse)
406 .toggle_state(selected)
407 .child(HighlightedLabel::new(label, name_highlights))
408 .child(
409 HighlightedLabel::new(path, path_highlights)
410 .size(LabelSize::Small)
411 .color(Color::Muted),
412 ),
413 )
414 }
415}