1mod active_toolchain;
2
3pub use active_toolchain::ActiveToolchain;
4use editor::Editor;
5use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
6use gpui::{
7 actions, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
8 ParentElement, Render, Styled, Task, WeakEntity, Window,
9};
10use language::{LanguageName, Toolchain, ToolchainList};
11use picker::{Picker, PickerDelegate};
12use project::{Project, WorktreeId};
13use std::{path::Path, sync::Arc};
14use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
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(worktree_id, language_name, cx)
173 })
174 .ok()?
175 .await?;
176
177 let _ = this.update_in(cx, move |this, window, cx| {
178 this.delegate.candidates = available_toolchains;
179
180 if let Some(active_toolchain) = active_toolchain {
181 if let Some(position) = this
182 .delegate
183 .candidates
184 .toolchains
185 .iter()
186 .position(|toolchain| *toolchain == active_toolchain)
187 {
188 this.delegate.set_selected_index(position, window, cx);
189 }
190 }
191 this.update_matches(this.query(cx), window, cx);
192 });
193
194 Some(())
195 }
196 });
197 let placeholder_text = "Select a toolchain…".to_string().into();
198 Self {
199 toolchain_selector,
200 candidates: Default::default(),
201 matches: vec![],
202 selected_index: 0,
203 workspace,
204 worktree_id,
205 worktree_abs_path_root,
206 placeholder_text,
207 _fetch_candidates_task,
208 }
209 }
210 fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString {
211 Path::new(&path.as_ref())
212 .strip_prefix(&worktree_root)
213 .ok()
214 .map(|suffix| Path::new(".").join(suffix))
215 .and_then(|path| path.to_str().map(String::from).map(SharedString::from))
216 .unwrap_or(path)
217 }
218}
219
220impl PickerDelegate for ToolchainSelectorDelegate {
221 type ListItem = ListItem;
222
223 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
224 self.placeholder_text.clone()
225 }
226
227 fn match_count(&self) -> usize {
228 self.matches.len()
229 }
230
231 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
232 if let Some(string_match) = self.matches.get(self.selected_index) {
233 let toolchain = self.candidates.toolchains[string_match.candidate_id].clone();
234 if let Some(workspace_id) = self
235 .workspace
236 .update(cx, |this, _| this.database_id())
237 .ok()
238 .flatten()
239 {
240 let workspace = self.workspace.clone();
241 let worktree_id = self.worktree_id;
242 cx.spawn_in(window, async move |_, cx| {
243 workspace::WORKSPACE_DB
244 .set_toolchain(workspace_id, worktree_id, toolchain.clone())
245 .await
246 .log_err();
247 workspace
248 .update(cx, |this, cx| {
249 this.project().update(cx, |this, cx| {
250 this.activate_toolchain(worktree_id, toolchain, cx)
251 })
252 })
253 .ok()?
254 .await;
255 Some(())
256 })
257 .detach();
258 }
259 }
260 self.dismissed(window, cx);
261 }
262
263 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
264 self.toolchain_selector
265 .update(cx, |_, cx| cx.emit(DismissEvent))
266 .log_err();
267 }
268
269 fn selected_index(&self) -> usize {
270 self.selected_index
271 }
272
273 fn set_selected_index(
274 &mut self,
275 ix: usize,
276 _window: &mut Window,
277 _: &mut Context<Picker<Self>>,
278 ) {
279 self.selected_index = ix;
280 }
281
282 fn update_matches(
283 &mut self,
284 query: String,
285 window: &mut Window,
286 cx: &mut Context<Picker<Self>>,
287 ) -> gpui::Task<()> {
288 let background = cx.background_executor().clone();
289 let candidates = self.candidates.clone();
290 let worktree_root_path = self.worktree_abs_path_root.clone();
291 cx.spawn_in(window, async move |this, cx| {
292 let matches = if query.is_empty() {
293 candidates
294 .toolchains
295 .into_iter()
296 .enumerate()
297 .map(|(index, candidate)| {
298 let path = Self::relativize_path(candidate.path, &worktree_root_path);
299 let string = format!("{}{}", candidate.name, path);
300 StringMatch {
301 candidate_id: index,
302 string,
303 positions: Vec::new(),
304 score: 0.0,
305 }
306 })
307 .collect()
308 } else {
309 let candidates = candidates
310 .toolchains
311 .into_iter()
312 .enumerate()
313 .map(|(candidate_id, toolchain)| {
314 let path = Self::relativize_path(toolchain.path, &worktree_root_path);
315 let string = format!("{}{}", toolchain.name, path);
316 StringMatchCandidate::new(candidate_id, &string)
317 })
318 .collect::<Vec<_>>();
319 match_strings(
320 &candidates,
321 &query,
322 false,
323 100,
324 &Default::default(),
325 background,
326 )
327 .await
328 };
329
330 this.update(cx, |this, cx| {
331 let delegate = &mut this.delegate;
332 delegate.matches = matches;
333 delegate.selected_index = delegate
334 .selected_index
335 .min(delegate.matches.len().saturating_sub(1));
336 cx.notify();
337 })
338 .log_err();
339 })
340 }
341
342 fn render_match(
343 &self,
344 ix: usize,
345 selected: bool,
346 _window: &mut Window,
347 _: &mut Context<Picker<Self>>,
348 ) -> Option<Self::ListItem> {
349 let mat = &self.matches[ix];
350 let toolchain = &self.candidates.toolchains[mat.candidate_id];
351
352 let label = toolchain.name.clone();
353 let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root);
354 let (name_highlights, mut path_highlights) = mat
355 .positions
356 .iter()
357 .cloned()
358 .partition::<Vec<_>, _>(|index| *index < label.len());
359 path_highlights.iter_mut().for_each(|index| {
360 *index -= label.len();
361 });
362 Some(
363 ListItem::new(ix)
364 .inset(true)
365 .spacing(ListItemSpacing::Sparse)
366 .toggle_state(selected)
367 .child(HighlightedLabel::new(label, name_highlights))
368 .child(
369 HighlightedLabel::new(path, path_highlights)
370 .size(LabelSize::Small)
371 .color(Color::Muted),
372 ),
373 )
374 }
375}