Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:37:02.827Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

toolchain_selector.rs

1165 lines · 45.8 KB · rust
1mod active_toolchain;
2
3pub use active_toolchain::ActiveToolchain;
4use anyhow::Context as _;
5use convert_case::Casing as _;
6use editor::Editor;
7use futures::channel::oneshot;
8use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
9use gpui::{
10    Action, Animation, AnimationExt, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
11    Focusable, KeyContext, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window,
12    actions, pulsating_between,
13};
14use language::{Language, LanguageName, Toolchain, ToolchainScope};
15use open_path_prompt::OpenPathDelegate;
16use picker::{Picker, PickerDelegate};
17use project::{DirectoryLister, Project, ProjectPath, Toolchains, WorktreeId};
18use std::{
19    borrow::Cow,
20    path::{Path, PathBuf},
21    sync::Arc,
22    time::Duration,
23};
24use ui::{
25    Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable,
26    NavigableEntry, prelude::*,
27};
28use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
29use workspace::{ModalView, Workspace};
30
31actions!(
32    toolchain,
33    [
34        /// Selects a toolchain for the current project.
35        Select,
36        /// Adds a new toolchain for the current project.
37        AddToolchain
38    ]
39);
40
41pub fn init(cx: &mut App) {
42    cx.observe_new(ToolchainSelector::register).detach();
43}
44
45pub struct ToolchainSelector {
46    state: State,
47    create_search_state: Arc<dyn Fn(&mut Window, &mut Context<Self>) -> SearchState + 'static>,
48    language: Option<Arc<Language>>,
49    project: Entity<Project>,
50    language_name: LanguageName,
51    worktree_id: WorktreeId,
52    relative_path: Arc<RelPath>,
53}
54
55#[derive(Clone)]
56struct SearchState {
57    picker: Entity<Picker<ToolchainSelectorDelegate>>,
58}
59
60struct AddToolchainState {
61    state: AddState,
62    project: Entity<Project>,
63    language_name: LanguageName,
64    root_path: ProjectPath,
65    weak: WeakEntity<ToolchainSelector>,
66    worktree_root_path: Arc<Path>,
67}
68
69struct ScopePickerState {
70    entries: [NavigableEntry; 3],
71    selected_scope: ToolchainScope,
72}
73
74#[expect(
75    dead_code,
76    reason = "These tasks have to be kept alive to run to completion"
77)]
78enum PathInputState {
79    WaitingForPath(Task<()>),
80    Resolving(Task<()>),
81}
82
83enum AddState {
84    Path {
85        picker: Entity<Picker<open_path_prompt::OpenPathDelegate>>,
86        error: Option<Arc<str>>,
87        input_state: PathInputState,
88        _subscription: Subscription,
89    },
90    Name {
91        toolchain: Toolchain,
92        editor: Entity<Editor>,
93        scope_picker: ScopePickerState,
94    },
95}
96
97impl AddToolchainState {
98    fn new(
99        project: Entity<Project>,
100        language_name: LanguageName,
101        root_path: ProjectPath,
102        window: &mut Window,
103        cx: &mut Context<ToolchainSelector>,
104    ) -> anyhow::Result<Entity<Self>> {
105        let weak = cx.weak_entity();
106        let worktree_root_path = project
107            .read(cx)
108            .worktree_for_id(root_path.worktree_id, cx)
109            .map(|worktree| worktree.read(cx).abs_path())
110            .context("Could not find worktree")?;
111        Ok(cx.new(|cx| {
112            let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx);
113            let path_style = project.read(cx).path_style(cx);
114            let picker = cx.new(|cx| {
115                let picker = Picker::uniform_list(lister, window, cx);
116                let mut worktree_root = worktree_root_path.to_string_lossy().into_owned();
117                worktree_root.push_str(path_style.primary_separator());
118                picker.set_query(&worktree_root, window, cx);
119                picker
120            });
121
122            Self {
123                state: AddState::Path {
124                    _subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
125                        cx.stop_propagation();
126                    }),
127                    picker,
128                    error: None,
129                    input_state: Self::wait_for_path(rx, window, cx),
130                },
131                project,
132                language_name,
133                root_path,
134                weak,
135                worktree_root_path,
136            }
137        }))
138    }
139
140    fn create_path_browser_delegate(
141        project: Entity<Project>,
142        cx: &mut Context<Self>,
143    ) -> (OpenPathDelegate, oneshot::Receiver<Option<Vec<PathBuf>>>) {
144        let (tx, rx) = oneshot::channel();
145        let weak = cx.weak_entity();
146        let lister = OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, cx)
147            .show_hidden()
148            .with_footer(Arc::new(move |_, cx| {
149                let error = weak
150                    .read_with(cx, |this, _| {
151                        if let AddState::Path { error, .. } = &this.state {
152                            error.clone()
153                        } else {
154                            None
155                        }
156                    })
157                    .ok()
158                    .flatten();
159                let is_loading = weak
160                    .read_with(cx, |this, _| {
161                        matches!(
162                            this.state,
163                            AddState::Path {
164                                input_state: PathInputState::Resolving(_),
165                                ..
166                            }
167                        )
168                    })
169                    .unwrap_or_default();
170                Some(
171                    v_flex()
172                        .child(Divider::horizontal())
173                        .child(
174                            h_flex()
175                                .p_1()
176                                .justify_between()
177                                .gap_2()
178                                .child(Label::new("Select Toolchain Path").color(Color::Muted).map(
179                                    |this| {
180                                        if is_loading {
181                                            this.with_animation(
182                                                "select-toolchain-label",
183                                                Animation::new(Duration::from_secs(2))
184                                                    .repeat()
185                                                    .with_easing(pulsating_between(0.4, 0.8)),
186                                                |label, delta| label.alpha(delta),
187                                            )
188                                            .into_any()
189                                        } else {
190                                            this.into_any_element()
191                                        }
192                                    },
193                                ))
194                                .when_some(error, |this, error| {
195                                    this.child(Label::new(error).color(Color::Error))
196                                }),
197                        )
198                        .into_any(),
199                )
200            }));
201
202        (lister, rx)
203    }
204    fn resolve_path(
205        path: PathBuf,
206        root_path: ProjectPath,
207        language_name: LanguageName,
208        project: Entity<Project>,
209        window: &mut Window,
210        cx: &mut Context<Self>,
211    ) -> PathInputState {
212        PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| {
213            _ = maybe!(async move {
214                let toolchain = project
215                    .update(cx, |this, cx| {
216                        this.resolve_toolchain(path.clone(), language_name, cx)
217                    })
218                    .await;
219                let Ok(toolchain) = toolchain else {
220                    // Go back to the path input state
221                    _ = this.update_in(cx, |this, window, cx| {
222                        if let AddState::Path {
223                            input_state,
224                            picker,
225                            error,
226                            ..
227                        } = &mut this.state
228                            && matches!(input_state, PathInputState::Resolving(_))
229                        {
230                            let Err(e) = toolchain else { unreachable!() };
231                            *error = Some(Arc::from(e.to_string()));
232                            let (delegate, rx) =
233                                Self::create_path_browser_delegate(this.project.clone(), cx);
234                            picker.update(cx, |picker, cx| {
235                                *picker = Picker::uniform_list(delegate, window, cx);
236                                picker.set_query(path.to_string_lossy().as_ref(), window, cx);
237                            });
238                            *input_state = Self::wait_for_path(rx, window, cx);
239                            this.focus_handle(cx).focus(window, cx);
240                        }
241                    });
242                    return Err(anyhow::anyhow!("Failed to resolve toolchain"));
243                };
244                let resolved_toolchain_path = project.read_with(cx, |this, cx| {
245                    this.find_project_path(&toolchain.path.as_ref(), cx)
246                });
247
248                // Suggest a default scope based on the applicability.
249                let scope = if let Some(project_path) = resolved_toolchain_path {
250                    if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
251                        let worktree_root_path = project
252                            .read_with(cx, |this, cx| {
253                                this.worktree_for_id(root_path.worktree_id, cx)
254                                    .map(|worktree| worktree.read(cx).abs_path())
255                            })
256                            .context("Could not find a worktree with a given worktree ID")?;
257                        ToolchainScope::Subproject(worktree_root_path, root_path.path)
258                    } else {
259                        ToolchainScope::Project
260                    }
261                } else {
262                    // This path lies outside of the project.
263                    ToolchainScope::Global
264                };
265
266                _ = this.update_in(cx, |this, window, cx| {
267                    let scope_picker = ScopePickerState {
268                        entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
269                        selected_scope: scope,
270                    };
271                    this.state = AddState::Name {
272                        editor: cx.new(|cx| {
273                            let mut editor = Editor::single_line(window, cx);
274                            editor.set_text(toolchain.name.as_ref(), window, cx);
275                            editor
276                        }),
277                        toolchain,
278                        scope_picker,
279                    };
280                    this.focus_handle(cx).focus(window, cx);
281                });
282
283                Result::<_, anyhow::Error>::Ok(())
284            })
285            .await;
286        }))
287    }
288
289    fn wait_for_path(
290        rx: oneshot::Receiver<Option<Vec<PathBuf>>>,
291        window: &mut Window,
292        cx: &mut Context<Self>,
293    ) -> PathInputState {
294        let task = cx.spawn_in(window, async move |this, cx| {
295            maybe!(async move {
296                let result = rx.await.log_err()?;
297
298                let path = result
299                    .into_iter()
300                    .flat_map(|paths| paths.into_iter())
301                    .next()?;
302                this.update_in(cx, |this, window, cx| {
303                    if let AddState::Path {
304                        input_state, error, ..
305                    } = &mut this.state
306                        && matches!(input_state, PathInputState::WaitingForPath(_))
307                    {
308                        error.take();
309                        *input_state = Self::resolve_path(
310                            path,
311                            this.root_path.clone(),
312                            this.language_name.clone(),
313                            this.project.clone(),
314                            window,
315                            cx,
316                        );
317                    }
318                })
319                .ok()?;
320                Some(())
321            })
322            .await;
323        });
324        PathInputState::WaitingForPath(task)
325    }
326
327    fn confirm_toolchain(
328        &mut self,
329        _: &menu::Confirm,
330        window: &mut Window,
331        cx: &mut Context<Self>,
332    ) {
333        let AddState::Name {
334            toolchain,
335            editor,
336            scope_picker,
337        } = &mut self.state
338        else {
339            return;
340        };
341
342        let text = editor.read(cx).text(cx);
343        if text.is_empty() {
344            return;
345        }
346
347        toolchain.name = SharedString::from(text);
348        self.project.update(cx, |this, cx| {
349            this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx);
350        });
351        _ = self.weak.update(cx, |this, cx| {
352            this.state = State::Search((this.create_search_state)(window, cx));
353            this.focus_handle(cx).focus(window, cx);
354            cx.notify();
355        });
356    }
357}
358impl Focusable for AddToolchainState {
359    fn focus_handle(&self, cx: &App) -> FocusHandle {
360        match &self.state {
361            AddState::Path { picker, .. } => picker.focus_handle(cx),
362            AddState::Name { editor, .. } => editor.focus_handle(cx),
363        }
364    }
365}
366
367impl AddToolchainState {
368    fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context<Self>) {
369        if let AddState::Name { scope_picker, .. } = &mut self.state {
370            scope_picker.selected_scope = scope;
371            cx.notify();
372        }
373    }
374}
375
376impl Focusable for State {
377    fn focus_handle(&self, cx: &App) -> FocusHandle {
378        match self {
379            State::Search(state) => state.picker.focus_handle(cx),
380            State::AddToolchain(state) => state.focus_handle(cx),
381        }
382    }
383}
384impl Render for AddToolchainState {
385    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
386        let theme = cx.theme().clone();
387        let weak = self.weak.upgrade();
388        let label = SharedString::new_static("Add");
389
390        v_flex()
391            .size_full()
392            // todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3`
393            // They get duplicated in the middle state of adding a virtual env, but then are needed for this last state
394            .bg(cx.theme().colors().elevated_surface_background)
395            .border_1()
396            .border_color(cx.theme().colors().border_variant)
397            .rounded_lg()
398            .when_some(weak, |this, weak| {
399                this.on_action(window.listener_for(
400                    &weak,
401                    |this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| {
402                        this.state = State::Search((this.create_search_state)(window, cx));
403                        this.state.focus_handle(cx).focus(window, cx);
404                        cx.notify();
405                    },
406                ))
407            })
408            .on_action(cx.listener(Self::confirm_toolchain))
409            .map(|this| match &self.state {
410                AddState::Path { picker, .. } => this.child(picker.clone()),
411                AddState::Name {
412                    editor,
413                    scope_picker,
414                    ..
415                } => {
416                    let scope_options = [
417                        ToolchainScope::Global,
418                        ToolchainScope::Project,
419                        ToolchainScope::Subproject(
420                            self.worktree_root_path.clone(),
421                            self.root_path.path.clone(),
422                        ),
423                    ];
424
425                    let mut navigable_scope_picker = Navigable::new(
426                        v_flex()
427                            .child(
428                                h_flex()
429                                    .w_full()
430                                    .p_2()
431                                    .border_b_1()
432                                    .border_color(theme.colors().border)
433                                    .child(editor.clone()),
434                            )
435                            .child(
436                                v_flex()
437                                    .child(
438                                        Label::new("Scope")
439                                            .size(LabelSize::Small)
440                                            .color(Color::Muted)
441                                            .mt_1()
442                                            .ml_2(),
443                                    )
444                                    .child(List::new().children(
445                                        scope_options.iter().enumerate().map(|(i, scope)| {
446                                            let is_selected = *scope == scope_picker.selected_scope;
447                                            let label = scope.label();
448                                            let description = scope.description();
449                                            let scope_clone_for_action = scope.clone();
450                                            let scope_clone_for_click = scope.clone();
451
452                                            div()
453                                                .id(SharedString::from(format!("scope-option-{i}")))
454                                                .track_focus(&scope_picker.entries[i].focus_handle)
455                                                .on_action(cx.listener(
456                                                    move |this, _: &menu::Confirm, _, cx| {
457                                                        this.select_scope(
458                                                            scope_clone_for_action.clone(),
459                                                            cx,
460                                                        );
461                                                    },
462                                                ))
463                                                .child(
464                                                    ListItem::new(SharedString::from(format!(
465                                                        "scope-{i}"
466                                                    )))
467                                                    .toggle_state(
468                                                        is_selected
469                                                            || scope_picker.entries[i]
470                                                                .focus_handle
471                                                                .contains_focused(window, cx),
472                                                    )
473                                                    .inset(true)
474                                                    .spacing(ListItemSpacing::Sparse)
475                                                    .child(
476                                                        h_flex()
477                                                            .gap_2()
478                                                            .child(Label::new(label))
479                                                            .child(
480                                                                Label::new(description)
481                                                                    .size(LabelSize::Small)
482                                                                    .color(Color::Muted),
483                                                            ),
484                                                    )
485                                                    .on_click(cx.listener(move |this, _, _, cx| {
486                                                        this.select_scope(
487                                                            scope_clone_for_click.clone(),
488                                                            cx,
489                                                        );
490                                                    })),
491                                                )
492                                        }),
493                                    ))
494                                    .child(Divider::horizontal())
495                                    .child(h_flex().p_1p5().justify_end().map(|this| {
496                                        let is_disabled = editor.read(cx).is_empty(cx);
497                                        let handle = self.focus_handle(cx);
498                                        this.child(
499                                            Button::new("add-toolchain", label)
500                                                .disabled(is_disabled)
501                                                .key_binding(KeyBinding::for_action_in(
502                                                    &menu::Confirm,
503                                                    &handle,
504                                                    cx,
505                                                ))
506                                                .on_click(cx.listener(|this, _, window, cx| {
507                                                    this.confirm_toolchain(
508                                                        &menu::Confirm,
509                                                        window,
510                                                        cx,
511                                                    );
512                                                }))
513                                                .map(|this| {
514                                                    if false {
515                                                        this.with_animation(
516                                                            "inspecting-user-toolchain",
517                                                            Animation::new(Duration::from_millis(
518                                                                500,
519                                                            ))
520                                                            .repeat()
521                                                            .with_easing(pulsating_between(
522                                                                0.4, 0.8,
523                                                            )),
524                                                            |label, delta| label.alpha(delta),
525                                                        )
526                                                        .into_any()
527                                                    } else {
528                                                        this.into_any_element()
529                                                    }
530                                                }),
531                                        )
532                                    })),
533                            )
534                            .into_any_element(),
535                    );
536
537                    for entry in &scope_picker.entries {
538                        navigable_scope_picker = navigable_scope_picker.entry(entry.clone());
539                    }
540
541                    this.child(navigable_scope_picker.render(window, cx))
542                }
543            })
544    }
545}
546
547#[derive(Clone)]
548enum State {
549    Search(SearchState),
550    AddToolchain(Entity<AddToolchainState>),
551}
552
553impl RenderOnce for State {
554    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
555        match self {
556            State::Search(state) => state.picker.into_any_element(),
557            State::AddToolchain(state) => state.into_any_element(),
558        }
559    }
560}
561impl ToolchainSelector {
562    fn register(
563        workspace: &mut Workspace,
564        _window: Option<&mut Window>,
565        _: &mut Context<Workspace>,
566    ) {
567        workspace.register_action(move |workspace, _: &Select, window, cx| {
568            Self::toggle(workspace, window, cx);
569        });
570        workspace.register_action(move |workspace, _: &AddToolchain, window, cx| {
571            let Some(toolchain_selector) = workspace.active_modal::<Self>(cx) else {
572                Self::toggle(workspace, window, cx);
573                return;
574            };
575
576            toolchain_selector.update(cx, |toolchain_selector, cx| {
577                toolchain_selector.handle_add_toolchain(&AddToolchain, window, cx);
578            });
579        });
580    }
581
582    fn toggle(
583        workspace: &mut Workspace,
584        window: &mut Window,
585        cx: &mut Context<Workspace>,
586    ) -> Option<()> {
587        let buffer = workspace
588            .active_item(cx)?
589            .act_as::<Editor>(cx)?
590            .read(cx)
591            .active_buffer(cx)?;
592        let project = workspace.project().clone();
593
594        let language_name = buffer.read(cx).language()?.name();
595        let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
596        let relative_path: Arc<RelPath> = buffer.read(cx).file()?.path().parent()?.into();
597        let worktree_root_path = project
598            .read(cx)
599            .worktree_for_id(worktree_id, cx)?
600            .read(cx)
601            .abs_path();
602        let weak = workspace.weak_handle();
603        cx.spawn_in(window, async move |workspace, cx| {
604            let active_toolchain = project
605                .read_with(cx, |this, cx| {
606                    this.active_toolchain(
607                        ProjectPath {
608                            worktree_id,
609                            path: relative_path.clone(),
610                        },
611                        language_name.clone(),
612                        cx,
613                    )
614                })
615                .await;
616            workspace
617                .update_in(cx, |this, window, cx| {
618                    this.toggle_modal(window, cx, move |window, cx| {
619                        ToolchainSelector::new(
620                            weak,
621                            project,
622                            active_toolchain,
623                            worktree_id,
624                            worktree_root_path,
625                            relative_path,
626                            language_name,
627                            window,
628                            cx,
629                        )
630                    });
631                })
632                .ok();
633            anyhow::Ok(())
634        })
635        .detach();
636
637        Some(())
638    }
639
640    fn new(
641        workspace: WeakEntity<Workspace>,
642        project: Entity<Project>,
643        active_toolchain: Option<Toolchain>,
644        worktree_id: WorktreeId,
645        worktree_root: Arc<Path>,
646        relative_path: Arc<RelPath>,
647        language_name: LanguageName,
648        window: &mut Window,
649        cx: &mut Context<Self>,
650    ) -> Self {
651        let language_registry = project.read(cx).languages().clone();
652        cx.spawn({
653            let language_name = language_name.clone();
654            async move |this, cx| {
655                let language = language_registry
656                    .language_for_name(&language_name.0)
657                    .await
658                    .ok();
659                this.update(cx, |this, cx| {
660                    this.language = language;
661                    cx.notify();
662                })
663                .ok();
664            }
665        })
666        .detach();
667        let project_clone = project.clone();
668        let language_name_clone = language_name.clone();
669        let relative_path_clone = relative_path.clone();
670
671        let create_search_state = Arc::new(move |window: &mut Window, cx: &mut Context<Self>| {
672            let toolchain_selector = cx.entity().downgrade();
673            let picker = cx.new(|cx| {
674                let delegate = ToolchainSelectorDelegate::new(
675                    active_toolchain.clone(),
676                    toolchain_selector,
677                    workspace.clone(),
678                    worktree_id,
679                    worktree_root.clone(),
680                    project_clone.clone(),
681                    relative_path_clone.clone(),
682                    language_name_clone.clone(),
683                    window,
684                    cx,
685                );
686                Picker::uniform_list(delegate, window, cx)
687            });
688            let picker_focus_handle = picker.focus_handle(cx);
689            picker.update(cx, |picker, _| {
690                picker.delegate.focus_handle = picker_focus_handle.clone();
691            });
692            SearchState { picker }
693        });
694
695        Self {
696            state: State::Search(create_search_state(window, cx)),
697            create_search_state,
698            language: None,
699            project,
700            language_name,
701            worktree_id,
702            relative_path,
703        }
704    }
705
706    fn handle_add_toolchain(
707        &mut self,
708        _: &AddToolchain,
709        window: &mut Window,
710        cx: &mut Context<Self>,
711    ) {
712        if matches!(self.state, State::Search(_)) {
713            let Ok(state) = AddToolchainState::new(
714                self.project.clone(),
715                self.language_name.clone(),
716                ProjectPath {
717                    worktree_id: self.worktree_id,
718                    path: self.relative_path.clone(),
719                },
720                window,
721                cx,
722            ) else {
723                return;
724            };
725            self.state = State::AddToolchain(state);
726            self.state.focus_handle(cx).focus(window, cx);
727            cx.notify();
728        }
729    }
730}
731
732impl Render for ToolchainSelector {
733    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
734        let mut key_context = KeyContext::new_with_defaults();
735        key_context.add("ToolchainSelector");
736
737        v_flex()
738            .key_context(key_context)
739            .w(rems(34.))
740            .on_action(cx.listener(Self::handle_add_toolchain))
741            .child(self.state.clone().render(window, cx))
742    }
743}
744
745impl Focusable for ToolchainSelector {
746    fn focus_handle(&self, cx: &App) -> FocusHandle {
747        self.state.focus_handle(cx)
748    }
749}
750
751impl EventEmitter<DismissEvent> for ToolchainSelector {}
752impl ModalView for ToolchainSelector {}
753
754pub struct ToolchainSelectorDelegate {
755    toolchain_selector: WeakEntity<ToolchainSelector>,
756    candidates: Arc<[(Toolchain, Option<ToolchainScope>)]>,
757    matches: Vec<StringMatch>,
758    selected_index: usize,
759    workspace: WeakEntity<Workspace>,
760    worktree_id: WorktreeId,
761    worktree_abs_path_root: Arc<Path>,
762    relative_path: Arc<RelPath>,
763    placeholder_text: Arc<str>,
764    add_toolchain_text: Arc<str>,
765    project: Entity<Project>,
766    focus_handle: FocusHandle,
767    _fetch_candidates_task: Task<Option<()>>,
768}
769
770impl ToolchainSelectorDelegate {
771    fn new(
772        active_toolchain: Option<Toolchain>,
773        toolchain_selector: WeakEntity<ToolchainSelector>,
774        workspace: WeakEntity<Workspace>,
775        worktree_id: WorktreeId,
776        worktree_abs_path_root: Arc<Path>,
777        project: Entity<Project>,
778        relative_path: Arc<RelPath>,
779        language_name: LanguageName,
780        window: &mut Window,
781        cx: &mut Context<Picker<Self>>,
782    ) -> Self {
783        let _project = project.clone();
784        let path_style = project.read(cx).path_style(cx);
785
786        let _fetch_candidates_task = cx.spawn_in(window, {
787            async move |this, cx| {
788                let meta = _project
789                    .read_with(cx, |this, _| {
790                        Project::toolchain_metadata(this.languages().clone(), language_name.clone())
791                    })
792                    .await?;
793                let relative_path = this
794                    .update(cx, |this, cx| {
795                        this.delegate.add_toolchain_text = format!(
796                            "Add {}",
797                            meta.term.as_ref().to_case(convert_case::Case::Title)
798                        )
799                        .into();
800                        cx.notify();
801                        this.delegate.relative_path.clone()
802                    })
803                    .ok()?;
804
805                let Toolchains {
806                    toolchains: available_toolchains,
807                    root_path: relative_path,
808                    user_toolchains,
809                } = _project
810                    .update(cx, |this, cx| {
811                        this.available_toolchains(
812                            ProjectPath {
813                                worktree_id,
814                                path: relative_path.clone(),
815                            },
816                            language_name,
817                            cx,
818                        )
819                    })
820                    .await?;
821                let pretty_path = {
822                    if relative_path.is_empty() {
823                        Cow::Borrowed("worktree root")
824                    } else {
825                        Cow::Owned(format!("`{}`", relative_path.display(path_style)))
826                    }
827                };
828                let placeholder_text =
829                    format!("Select a {} for {pretty_path}…", meta.term.to_lowercase(),).into();
830                let _ = this.update_in(cx, move |this, window, cx| {
831                    this.delegate.relative_path = relative_path;
832                    this.delegate.placeholder_text = placeholder_text;
833                    this.refresh_placeholder(window, cx);
834                });
835
836                let _ = this.update_in(cx, move |this, window, cx| {
837                    this.delegate.candidates = user_toolchains
838                        .into_iter()
839                        .flat_map(|(scope, toolchains)| {
840                            toolchains
841                                .into_iter()
842                                .map(move |toolchain| (toolchain, Some(scope.clone())))
843                        })
844                        .chain(
845                            available_toolchains
846                                .toolchains
847                                .into_iter()
848                                .map(|toolchain| (toolchain, None)),
849                        )
850                        .collect();
851
852                    if let Some(active_toolchain) = active_toolchain
853                        && let Some(position) = this
854                            .delegate
855                            .candidates
856                            .iter()
857                            .position(|(toolchain, _)| *toolchain == active_toolchain)
858                    {
859                        this.delegate.set_selected_index(position, window, cx);
860                    }
861                    this.update_matches(this.query(cx), window, cx);
862                });
863
864                Some(())
865            }
866        });
867        let placeholder_text = "Select a toolchain…".to_string().into();
868        Self {
869            toolchain_selector,
870            candidates: Default::default(),
871            matches: vec![],
872            selected_index: 0,
873            workspace,
874            worktree_id,
875            worktree_abs_path_root,
876            placeholder_text,
877            relative_path,
878            _fetch_candidates_task,
879            project,
880            focus_handle: cx.focus_handle(),
881            add_toolchain_text: Arc::from("Add Toolchain"),
882        }
883    }
884    fn relativize_path(
885        path: SharedString,
886        worktree_root: &Path,
887        path_style: PathStyle,
888    ) -> SharedString {
889        Path::new(&path.as_ref())
890            .strip_prefix(&worktree_root)
891            .ok()
892            .and_then(|suffix| suffix.to_str())
893            .map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into())
894            .unwrap_or(path)
895    }
896}
897
898impl PickerDelegate for ToolchainSelectorDelegate {
899    type ListItem = ListItem;
900
901    fn name() -> &'static str {
902        "toolchain selector"
903    }
904
905    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
906        self.placeholder_text.clone()
907    }
908
909    fn match_count(&self) -> usize {
910        self.matches.len()
911    }
912
913    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
914        if let Some(string_match) = self.matches.get(self.selected_index) {
915            let (toolchain, _) = self.candidates[string_match.candidate_id].clone();
916            if let Some(workspace_id) = self
917                .workspace
918                .read_with(cx, |this, _| this.database_id())
919                .ok()
920                .flatten()
921            {
922                let workspace = self.workspace.clone();
923                let worktree_id = self.worktree_id;
924                let worktree_abs_path_root = self.worktree_abs_path_root.clone();
925                let path = self.relative_path.clone();
926                let relative_path = self.relative_path.clone();
927                let db = workspace::WorkspaceDb::global(cx);
928                cx.spawn_in(window, async move |_, cx| {
929                    db.set_toolchain(
930                        workspace_id,
931                        worktree_abs_path_root,
932                        relative_path,
933                        toolchain.clone(),
934                    )
935                    .await
936                    .log_err();
937                    workspace
938                        .update(cx, |this, cx| {
939                            this.project().update(cx, |this, cx| {
940                                this.activate_toolchain(
941                                    ProjectPath { worktree_id, path },
942                                    toolchain,
943                                    cx,
944                                )
945                            })
946                        })
947                        .ok()?
948                        .await;
949                    Some(())
950                })
951                .detach();
952            }
953        }
954        self.dismissed(window, cx);
955    }
956
957    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
958        self.toolchain_selector
959            .update(cx, |_, cx| cx.emit(DismissEvent))
960            .log_err();
961    }
962
963    fn selected_index(&self) -> usize {
964        self.selected_index
965    }
966
967    fn set_selected_index(
968        &mut self,
969        ix: usize,
970        _window: &mut Window,
971        _: &mut Context<Picker<Self>>,
972    ) {
973        self.selected_index = ix;
974    }
975
976    fn update_matches(
977        &mut self,
978        query: String,
979        window: &mut Window,
980        cx: &mut Context<Picker<Self>>,
981    ) -> gpui::Task<()> {
982        let background = cx.background_executor().clone();
983        let candidates = self.candidates.clone();
984        let worktree_root_path = self.worktree_abs_path_root.clone();
985        let path_style = self.project.read(cx).path_style(cx);
986        cx.spawn_in(window, async move |this, cx| {
987            let matches = if query.is_empty() {
988                candidates
989                    .into_iter()
990                    .enumerate()
991                    .map(|(index, (candidate, _))| {
992                        let path = Self::relativize_path(
993                            candidate.path.clone(),
994                            &worktree_root_path,
995                            path_style,
996                        );
997                        let string = format!("{}{}", candidate.name, path);
998                        StringMatch {
999                            candidate_id: index,
1000                            string,
1001                            positions: Vec::new(),
1002                            score: 0.0,
1003                        }
1004                    })
1005                    .collect()
1006            } else {
1007                let candidates = candidates
1008                    .into_iter()
1009                    .enumerate()
1010                    .map(|(candidate_id, (toolchain, _))| {
1011                        let path = Self::relativize_path(
1012                            toolchain.path.clone(),
1013                            &worktree_root_path,
1014                            path_style,
1015                        );
1016                        let string = format!("{}{}", toolchain.name, path);
1017                        StringMatchCandidate::new(candidate_id, &string)
1018                    })
1019                    .collect::<Vec<_>>();
1020                match_strings(
1021                    &candidates,
1022                    &query,
1023                    false,
1024                    true,
1025                    100,
1026                    &Default::default(),
1027                    background,
1028                )
1029                .await
1030            };
1031
1032            this.update(cx, |this, cx| {
1033                let delegate = &mut this.delegate;
1034                delegate.matches = matches;
1035                delegate.selected_index = delegate
1036                    .selected_index
1037                    .min(delegate.matches.len().saturating_sub(1));
1038                cx.notify();
1039            })
1040            .log_err();
1041        })
1042    }
1043
1044    fn render_match(
1045        &self,
1046        ix: usize,
1047        selected: bool,
1048        _: &mut Window,
1049        cx: &mut Context<Picker<Self>>,
1050    ) -> Option<Self::ListItem> {
1051        let mat = &self.matches.get(ix)?;
1052        let (toolchain, scope) = &self.candidates.get(mat.candidate_id)?;
1053
1054        let label = toolchain.name.clone();
1055        let path_style = self.project.read(cx).path_style(cx);
1056        let path = Self::relativize_path(
1057            toolchain.path.clone(),
1058            &self.worktree_abs_path_root,
1059            path_style,
1060        );
1061        let (name_highlights, mut path_highlights) = mat
1062            .positions
1063            .iter()
1064            .cloned()
1065            .partition::<Vec<_>, _>(|index| *index < label.len());
1066        path_highlights.iter_mut().for_each(|index| {
1067            *index -= label.len();
1068        });
1069        let id: SharedString = format!("toolchain-{ix}",).into();
1070        Some(
1071            ListItem::new(id)
1072                .inset(true)
1073                .spacing(ListItemSpacing::Sparse)
1074                .toggle_state(selected)
1075                .child(HighlightedLabel::new(label, name_highlights))
1076                .child(
1077                    HighlightedLabel::new(path, path_highlights)
1078                        .size(LabelSize::Small)
1079                        .color(Color::Muted),
1080                )
1081                .when_some(scope.as_ref(), |this, scope| {
1082                    let id: SharedString = format!(
1083                        "delete-custom-toolchain-{}-{}",
1084                        toolchain.name, toolchain.path
1085                    )
1086                    .into();
1087                    let toolchain = toolchain.clone();
1088                    let scope = scope.clone();
1089
1090                    this.end_slot(IconButton::new(id, IconName::Trash).on_click(cx.listener(
1091                        move |this, _, _, cx| {
1092                            this.delegate.project.update(cx, |this, cx| {
1093                                this.remove_toolchain(toolchain.clone(), scope.clone(), cx)
1094                            });
1095
1096                            this.delegate.matches.retain_mut(|m| {
1097                                if m.candidate_id == ix {
1098                                    return false;
1099                                } else if m.candidate_id > ix {
1100                                    m.candidate_id -= 1;
1101                                }
1102                                true
1103                            });
1104
1105                            this.delegate.candidates = this
1106                                .delegate
1107                                .candidates
1108                                .iter()
1109                                .enumerate()
1110                                .filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone()))
1111                                .collect();
1112
1113                            if this.delegate.selected_index >= ix {
1114                                this.delegate.selected_index =
1115                                    this.delegate.selected_index.saturating_sub(1);
1116                            }
1117                            cx.stop_propagation();
1118                            cx.notify();
1119                        },
1120                    )))
1121                }),
1122        )
1123    }
1124    fn render_footer(
1125        &self,
1126        _window: &mut Window,
1127        cx: &mut Context<Picker<Self>>,
1128    ) -> Option<AnyElement> {
1129        Some(
1130            v_flex()
1131                .rounded_b_md()
1132                .child(Divider::horizontal())
1133                .child(
1134                    h_flex()
1135                        .p_1p5()
1136                        .gap_0p5()
1137                        .justify_end()
1138                        .child(
1139                            Button::new("xd", self.add_toolchain_text.clone())
1140                                .key_binding(KeyBinding::for_action_in(
1141                                    &AddToolchain,
1142                                    &self.focus_handle,
1143                                    cx,
1144                                ))
1145                                .on_click(|_, window, cx| {
1146                                    window.dispatch_action(Box::new(AddToolchain), cx)
1147                                }),
1148                        )
1149                        .child(
1150                            Button::new("select", "Select")
1151                                .key_binding(KeyBinding::for_action_in(
1152                                    &menu::Confirm,
1153                                    &self.focus_handle,
1154                                    cx,
1155                                ))
1156                                .on_click(|_, window, cx| {
1157                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1158                                }),
1159                        ),
1160                )
1161                .into_any_element(),
1162        )
1163    }
1164}
1165
Served at tenant.openagents/omega Member data and write actions are omitted.