1use crate::KERNEL_DOCS_URL;
2use crate::kernels::KernelSpecification;
3use crate::repl_store::ReplStore;
4
5use gpui::AnyView;
6use gpui::DismissEvent;
7
8use gpui::FontWeight;
9use picker::Picker;
10use picker::PickerDelegate;
11use project::WorktreeId;
12
13use std::sync::Arc;
14use ui::ListItemSpacing;
15
16use gpui::SharedString;
17use gpui::Task;
18use ui::{ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*};
19
20type OnSelect = Box<dyn Fn(KernelSpecification, &mut Window, &mut App)>;
21
22#[derive(IntoElement)]
23pub struct KernelSelector<T, TT>
24where
25 T: PopoverTrigger + ButtonCommon,
26 TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
27{
28 handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
29 on_select: OnSelect,
30 trigger: T,
31 tooltip: TT,
32 info_text: Option<SharedString>,
33 worktree_id: WorktreeId,
34}
35
36pub struct KernelPickerDelegate {
37 all_kernels: Vec<KernelSpecification>,
38 filtered_kernels: Vec<KernelSpecification>,
39 selected_kernelspec: Option<KernelSpecification>,
40 on_select: OnSelect,
41}
42
43// Helper function to truncate long paths
44fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
45 if path.len() <= max_length {
46 path.to_string().into()
47 } else {
48 let truncated = path.chars().rev().take(max_length - 3).collect::<String>();
49 format!("...{}", truncated.chars().rev().collect::<String>()).into()
50 }
51}
52
53impl<T, TT> KernelSelector<T, TT>
54where
55 T: PopoverTrigger + ButtonCommon,
56 TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
57{
58 pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self {
59 KernelSelector {
60 on_select,
61 handle: None,
62 trigger,
63 tooltip,
64 info_text: None,
65 worktree_id,
66 }
67 }
68
69 pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>) -> Self {
70 self.handle = Some(handle);
71 self
72 }
73
74 pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
75 self.info_text = Some(text.into());
76 self
77 }
78}
79
80impl PickerDelegate for KernelPickerDelegate {
81 type ListItem = ListItem;
82
83 fn match_count(&self) -> usize {
84 self.filtered_kernels.len()
85 }
86
87 fn selected_index(&self) -> usize {
88 if let Some(kernelspec) = self.selected_kernelspec.as_ref() {
89 self.filtered_kernels
90 .iter()
91 .position(|k| k == kernelspec)
92 .unwrap_or(0)
93 } else {
94 0
95 }
96 }
97
98 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
99 self.selected_kernelspec = self.filtered_kernels.get(ix).cloned();
100 cx.notify();
101 }
102
103 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
104 "Select a kernel...".into()
105 }
106
107 fn update_matches(
108 &mut self,
109 query: String,
110 _window: &mut Window,
111 _cx: &mut Context<Picker<Self>>,
112 ) -> Task<()> {
113 let all_kernels = self.all_kernels.clone();
114
115 if query.is_empty() {
116 self.filtered_kernels = all_kernels;
117 return Task::ready(());
118 }
119
120 self.filtered_kernels = if query.is_empty() {
121 all_kernels
122 } else {
123 all_kernels
124 .into_iter()
125 .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase()))
126 .collect()
127 };
128
129 Task::ready(())
130 }
131
132 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
133 if let Some(kernelspec) = &self.selected_kernelspec {
134 (self.on_select)(kernelspec.clone(), window, cx);
135 cx.emit(DismissEvent);
136 }
137 }
138
139 fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
140
141 fn render_match(
142 &self,
143 ix: usize,
144 selected: bool,
145 _: &mut Window,
146 cx: &mut Context<Picker<Self>>,
147 ) -> Option<Self::ListItem> {
148 let kernelspec = self.filtered_kernels.get(ix)?;
149 let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec);
150 let icon = kernelspec.icon(cx);
151
152 let (name, kernel_type, path_or_url) = match kernelspec {
153 KernelSpecification::Jupyter(_) => (kernelspec.name(), "Jupyter", None),
154 KernelSpecification::PythonEnv(_) => (
155 kernelspec.name(),
156 "Python Env",
157 Some(truncate_path(&kernelspec.path(), 42)),
158 ),
159 KernelSpecification::Remote(_) => (
160 kernelspec.name(),
161 "Remote",
162 Some(truncate_path(&kernelspec.path(), 42)),
163 ),
164 };
165
166 Some(
167 ListItem::new(ix)
168 .inset(true)
169 .spacing(ListItemSpacing::Sparse)
170 .toggle_state(selected)
171 .child(
172 h_flex()
173 .w_full()
174 .gap_3()
175 .child(icon.color(Color::Default).size(IconSize::Medium))
176 .child(
177 v_flex()
178 .flex_grow()
179 .gap_0p5()
180 .child(
181 h_flex()
182 .justify_between()
183 .child(
184 div().w_48().text_ellipsis().child(
185 Label::new(name)
186 .weight(FontWeight::MEDIUM)
187 .size(LabelSize::Default),
188 ),
189 )
190 .when_some(path_or_url, |flex, path| {
191 flex.text_ellipsis().child(
192 Label::new(path)
193 .size(LabelSize::Small)
194 .color(Color::Muted),
195 )
196 }),
197 )
198 .child(
199 h_flex()
200 .gap_1()
201 .child(
202 Label::new(kernelspec.language())
203 .size(LabelSize::Small)
204 .color(Color::Muted),
205 )
206 .child(
207 Label::new(kernel_type)
208 .size(LabelSize::Small)
209 .color(Color::Muted),
210 ),
211 ),
212 ),
213 )
214 .when(is_selected, |item| {
215 item.end_slot(
216 Icon::new(IconName::Check)
217 .color(Color::Accent)
218 .size(IconSize::Small),
219 )
220 }),
221 )
222 }
223
224 fn render_footer(
225 &self,
226 _: &mut Window,
227 cx: &mut Context<Picker<Self>>,
228 ) -> Option<gpui::AnyElement> {
229 Some(
230 h_flex()
231 .w_full()
232 .border_t_1()
233 .border_color(cx.theme().colors().border_variant)
234 .p_1()
235 .gap_4()
236 .child(
237 Button::new("kernel-docs", "Kernel Docs")
238 .icon(IconName::ArrowUpRight)
239 .icon_size(IconSize::Small)
240 .icon_color(Color::Muted)
241 .icon_position(IconPosition::End)
242 .on_click(move |_, _, cx| cx.open_url(KERNEL_DOCS_URL)),
243 )
244 .into_any(),
245 )
246 }
247}
248
249impl<T, TT> RenderOnce for KernelSelector<T, TT>
250where
251 T: PopoverTrigger + ButtonCommon,
252 TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
253{
254 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
255 let store = ReplStore::global(cx).read(cx);
256
257 let all_kernels: Vec<KernelSpecification> = store
258 .kernel_specifications_for_worktree(self.worktree_id)
259 .cloned()
260 .collect();
261
262 let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx);
263
264 let delegate = KernelPickerDelegate {
265 on_select: self.on_select,
266 all_kernels: all_kernels.clone(),
267 filtered_kernels: all_kernels,
268 selected_kernelspec,
269 };
270
271 let picker_view = cx.new(|cx| {
272 Picker::uniform_list(delegate, window, cx)
273 .width(rems(30.))
274 .max_height(Some(rems(20.).into()))
275 });
276
277 PopoverMenu::new("kernel-switcher")
278 .menu(move |_window, _cx| Some(picker_view.clone()))
279 .trigger_with_tooltip(self.trigger, self.tooltip)
280 .attach(gpui::Corner::BottomLeft)
281 .when_some(self.handle, |menu, handle| menu.with_handle(handle))
282 }
283}