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 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}