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
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, cx: &mut ViewContext<Picker<Self>>) {
88 self.selected_kernelspec = self.filtered_kernels.get(ix).cloned();
89 cx.notify();
90 }
91
92 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
93 "Select a kernel...".into()
94 }
95
96 fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
97 let all_kernels = self.all_kernels.clone();
98
99 if query.is_empty() {
100 self.filtered_kernels = all_kernels;
101 return Task::ready(());
102 }
103
104 self.filtered_kernels = if query.is_empty() {
105 all_kernels
106 } else {
107 all_kernels
108 .into_iter()
109 .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase()))
110 .collect()
111 };
112
113 return Task::ready(());
114 }
115
116 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
117 if let Some(kernelspec) = &self.selected_kernelspec {
118 (self.on_select)(kernelspec.clone(), cx.window_context());
119 cx.emit(DismissEvent);
120 }
121 }
122
123 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
124
125 fn render_match(
126 &self,
127 ix: usize,
128 selected: bool,
129 cx: &mut ViewContext<Picker<Self>>,
130 ) -> Option<Self::ListItem> {
131 let kernelspec = self.filtered_kernels.get(ix)?;
132 let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec);
133 let icon = kernelspec.icon(cx);
134
135 let (name, kernel_type, path_or_url) = match kernelspec {
136 KernelSpecification::Jupyter(_) => (kernelspec.name(), "Jupyter", None),
137 KernelSpecification::PythonEnv(_) => (
138 kernelspec.name(),
139 "Python Env",
140 Some(truncate_path(&kernelspec.path(), 42)),
141 ),
142 KernelSpecification::Remote(_) => (
143 kernelspec.name(),
144 "Remote",
145 Some(truncate_path(&kernelspec.path(), 42)),
146 ),
147 };
148
149 Some(
150 ListItem::new(ix)
151 .inset(true)
152 .spacing(ListItemSpacing::Sparse)
153 .toggle_state(selected)
154 .child(
155 h_flex()
156 .w_full()
157 .gap_3()
158 .child(icon.color(Color::Default).size(IconSize::Medium))
159 .child(
160 v_flex()
161 .flex_grow()
162 .gap_0p5()
163 .child(
164 h_flex()
165 .justify_between()
166 .child(
167 div().w_48().text_ellipsis().child(
168 Label::new(name)
169 .weight(FontWeight::MEDIUM)
170 .size(LabelSize::Default),
171 ),
172 )
173 .when_some(path_or_url.clone(), |flex, path| {
174 flex.text_ellipsis().child(
175 Label::new(path)
176 .size(LabelSize::Small)
177 .color(Color::Muted),
178 )
179 }),
180 )
181 .child(
182 h_flex()
183 .gap_1()
184 .child(
185 Label::new(kernelspec.language())
186 .size(LabelSize::Small)
187 .color(Color::Muted),
188 )
189 .child(
190 Label::new(kernel_type)
191 .size(LabelSize::Small)
192 .color(Color::Muted),
193 ),
194 ),
195 ),
196 )
197 .when(is_selected, |item| {
198 item.end_slot(
199 Icon::new(IconName::Check)
200 .color(Color::Accent)
201 .size(IconSize::Small),
202 )
203 }),
204 )
205 }
206
207 fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
208 Some(
209 h_flex()
210 .w_full()
211 .border_t_1()
212 .border_color(cx.theme().colors().border_variant)
213 .p_1()
214 .gap_4()
215 .child(
216 Button::new("kernel-docs", "Kernel Docs")
217 .icon(IconName::ExternalLink)
218 .icon_size(IconSize::XSmall)
219 .icon_color(Color::Muted)
220 .icon_position(IconPosition::End)
221 .on_click(move |_, cx| cx.open_url(KERNEL_DOCS_URL)),
222 )
223 .into_any(),
224 )
225 }
226}
227
228impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
229 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
230 let store = ReplStore::global(cx).read(cx);
231
232 let all_kernels: Vec<KernelSpecification> = store
233 .kernel_specifications_for_worktree(self.worktree_id)
234 .cloned()
235 .collect();
236
237 let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx);
238
239 let delegate = KernelPickerDelegate {
240 on_select: self.on_select,
241 all_kernels: all_kernels.clone(),
242 filtered_kernels: all_kernels,
243 selected_kernelspec,
244 };
245
246 let picker_view = cx.new_view(|cx| {
247 let picker = Picker::uniform_list(delegate, cx)
248 .width(rems(30.))
249 .max_height(Some(rems(20.).into()));
250 picker
251 });
252
253 PopoverMenu::new("kernel-switcher")
254 .menu(move |_cx| Some(picker_view.clone()))
255 .trigger(self.trigger)
256 .attach(gpui::Corner::BottomLeft)
257 .when_some(self.handle, |menu, handle| menu.with_handle(handle))
258 }
259}