kernel_options.rs

  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}