kernel_options.rs

  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}