1use crate::kernels::KernelSpecification;
2use crate::repl_store::ReplStore;
3use crate::KERNEL_DOCS_URL;
4
5use gpui::DismissEvent;
6
7use gpui::FontWeight;
8use picker::Picker;
9use picker::PickerDelegate;
10use project::WorktreeId;
11
12use std::sync::Arc;
13use ui::ListItemSpacing;
14
15use gpui::SharedString;
16use gpui::Task;
17use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
18
19type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
20
21#[derive(IntoElement)]
22pub struct KernelSelector<T: PopoverTrigger> {
23 handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
24 on_select: OnSelect,
25 trigger: T,
26 info_text: Option<SharedString>,
27 worktree_id: WorktreeId,
28}
29
30pub struct KernelPickerDelegate {
31 all_kernels: Vec<KernelSpecification>,
32 filtered_kernels: Vec<KernelSpecification>,
33 selected_kernelspec: Option<KernelSpecification>,
34 on_select: OnSelect,
35}
36
37impl<T: PopoverTrigger> KernelSelector<T> {
38 pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
39 KernelSelector {
40 on_select,
41 handle: None,
42 trigger,
43 info_text: None,
44 worktree_id,
45 }
46 }
47
48 pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>) -> Self {
49 self.handle = Some(handle);
50 self
51 }
52
53 pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
54 self.info_text = Some(text.into());
55 self
56 }
57}
58
59impl PickerDelegate for KernelPickerDelegate {
60 type ListItem = ListItem;
61
62 fn match_count(&self) -> usize {
63 self.filtered_kernels.len()
64 }
65
66 fn selected_index(&self) -> usize {
67 if let Some(kernelspec) = self.selected_kernelspec.as_ref() {
68 self.filtered_kernels
69 .iter()
70 .position(|k| k == kernelspec)
71 .unwrap_or(0)
72 } else {
73 0
74 }
75 }
76
77 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
78 self.selected_kernelspec = self.filtered_kernels.get(ix).cloned();
79 cx.notify();
80 }
81
82 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
83 "Select a kernel...".into()
84 }
85
86 fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
87 let all_kernels = self.all_kernels.clone();
88
89 if query.is_empty() {
90 self.filtered_kernels = all_kernels;
91 return Task::Ready(Some(()));
92 }
93
94 self.filtered_kernels = if query.is_empty() {
95 all_kernels
96 } else {
97 all_kernels
98 .into_iter()
99 .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase()))
100 .collect()
101 };
102
103 return Task::Ready(Some(()));
104 }
105
106 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
107 if let Some(kernelspec) = &self.selected_kernelspec {
108 (self.on_select)(kernelspec.clone(), cx.window_context());
109 cx.emit(DismissEvent);
110 }
111 }
112
113 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
114
115 fn render_match(
116 &self,
117 ix: usize,
118 selected: bool,
119 _cx: &mut ViewContext<Picker<Self>>,
120 ) -> Option<Self::ListItem> {
121 let kernelspec = self.filtered_kernels.get(ix)?;
122
123 let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec);
124
125 Some(
126 ListItem::new(ix)
127 .inset(true)
128 .spacing(ListItemSpacing::Sparse)
129 .selected(selected)
130 .child(
131 v_flex()
132 .min_w(px(600.))
133 .w_full()
134 .gap_0p5()
135 .child(
136 h_flex()
137 .w_full()
138 .gap_1()
139 .child(Label::new(kernelspec.name()).weight(FontWeight::MEDIUM))
140 .child(
141 Label::new(kernelspec.language())
142 .size(LabelSize::Small)
143 .color(Color::Muted),
144 ),
145 )
146 .child(
147 Label::new(kernelspec.path())
148 .size(LabelSize::XSmall)
149 .color(Color::Muted),
150 ),
151 )
152 .when(is_selected, |item| {
153 item.end_slot(
154 Icon::new(IconName::Check)
155 .color(Color::Accent)
156 .size(IconSize::Small),
157 )
158 }),
159 )
160 }
161
162 fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
163 Some(
164 h_flex()
165 .w_full()
166 .border_t_1()
167 .border_color(cx.theme().colors().border_variant)
168 .p_1()
169 .gap_4()
170 .child(
171 Button::new("kernel-docs", "Kernel Docs")
172 .icon(IconName::ExternalLink)
173 .icon_size(IconSize::XSmall)
174 .icon_color(Color::Muted)
175 .icon_position(IconPosition::End)
176 .on_click(move |_, cx| cx.open_url(KERNEL_DOCS_URL)),
177 )
178 .into_any(),
179 )
180 }
181}
182
183impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
184 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
185 let store = ReplStore::global(cx).read(cx);
186
187 let all_kernels: Vec<KernelSpecification> = store
188 .kernel_specifications_for_worktree(self.worktree_id)
189 .cloned()
190 .collect();
191
192 let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx);
193
194 let delegate = KernelPickerDelegate {
195 on_select: self.on_select,
196 all_kernels: all_kernels.clone(),
197 filtered_kernels: all_kernels,
198 selected_kernelspec,
199 };
200
201 let picker_view = cx.new_view(|cx| {
202 let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
203 picker
204 });
205
206 PopoverMenu::new("kernel-switcher")
207 .menu(move |_cx| Some(picker_view.clone()))
208 .trigger(self.trigger)
209 .attach(gpui::AnchorCorner::BottomLeft)
210 .when_some(self.handle, |menu, handle| menu.with_handle(handle))
211 }
212}