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