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