repl_sessions_ui.rs

  1use editor::Editor;
  2use gpui::{
  3    actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
  4    FontWeight, Subscription, View,
  5};
  6use std::collections::HashMap;
  7use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding, ListItem, Tooltip};
  8use util::ResultExt as _;
  9use workspace::item::ItemEvent;
 10use workspace::WorkspaceId;
 11use workspace::{item::Item, Workspace};
 12
 13use crate::jupyter_settings::JupyterSettings;
 14use crate::repl_store::ReplStore;
 15use crate::{KernelSpecification, KERNEL_DOCS_URL};
 16
 17actions!(
 18    repl,
 19    [
 20        Run,
 21        RunInPlace,
 22        ClearOutputs,
 23        Sessions,
 24        Interrupt,
 25        Shutdown,
 26        Restart,
 27        RefreshKernelspecs
 28    ]
 29);
 30
 31pub fn init(cx: &mut AppContext) {
 32    cx.observe_new_views(
 33        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
 34            workspace.register_action(|workspace, _: &Sessions, cx| {
 35                let existing = workspace
 36                    .active_pane()
 37                    .read(cx)
 38                    .items()
 39                    .find_map(|item| item.downcast::<ReplSessionsPage>());
 40
 41                if let Some(existing) = existing {
 42                    workspace.activate_item(&existing, true, true, cx);
 43                } else {
 44                    let repl_sessions_page = ReplSessionsPage::new(cx);
 45                    workspace.add_item_to_active_pane(Box::new(repl_sessions_page), None, true, cx)
 46                }
 47            });
 48
 49            workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
 50                let store = ReplStore::global(cx);
 51                store.update(cx, |store, cx| {
 52                    store.refresh_kernelspecs(cx).detach();
 53                });
 54            });
 55        },
 56    )
 57    .detach();
 58
 59    cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
 60        if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
 61            return;
 62        }
 63
 64        let is_local_project = editor
 65            .workspace()
 66            .map(|workspace| workspace.read(cx).project().read(cx).is_local())
 67            .unwrap_or(false);
 68
 69        if !is_local_project {
 70            return;
 71        }
 72
 73        let editor_handle = cx.view().downgrade();
 74
 75        editor
 76            .register_action({
 77                let editor_handle = editor_handle.clone();
 78                move |_: &Run, cx| {
 79                    if !JupyterSettings::enabled(cx) {
 80                        return;
 81                    }
 82
 83                    crate::run(editor_handle.clone(), true, cx).log_err();
 84                }
 85            })
 86            .detach();
 87
 88        editor
 89            .register_action({
 90                let editor_handle = editor_handle.clone();
 91                move |_: &RunInPlace, cx| {
 92                    if !JupyterSettings::enabled(cx) {
 93                        return;
 94                    }
 95
 96                    crate::run(editor_handle.clone(), false, cx).log_err();
 97                }
 98            })
 99            .detach();
100    })
101    .detach();
102}
103
104pub struct ReplSessionsPage {
105    focus_handle: FocusHandle,
106    _subscriptions: Vec<Subscription>,
107}
108
109impl ReplSessionsPage {
110    pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
111        cx.new_view(|cx: &mut ViewContext<Self>| {
112            let focus_handle = cx.focus_handle();
113
114            let subscriptions = vec![
115                cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
116                cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
117            ];
118
119            Self {
120                focus_handle,
121                _subscriptions: subscriptions,
122            }
123        })
124    }
125}
126
127impl EventEmitter<ItemEvent> for ReplSessionsPage {}
128
129impl FocusableView for ReplSessionsPage {
130    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
131        self.focus_handle.clone()
132    }
133}
134
135impl Item for ReplSessionsPage {
136    type Event = ItemEvent;
137
138    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
139        Some("REPL Sessions".into())
140    }
141
142    fn telemetry_event_text(&self) -> Option<&'static str> {
143        Some("repl sessions")
144    }
145
146    fn show_toolbar(&self) -> bool {
147        false
148    }
149
150    fn clone_on_split(
151        &self,
152        _workspace_id: Option<WorkspaceId>,
153        _: &mut ViewContext<Self>,
154    ) -> Option<View<Self>> {
155        None
156    }
157
158    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
159        f(*event)
160    }
161}
162
163impl Render for ReplSessionsPage {
164    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
165        let store = ReplStore::global(cx);
166
167        let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
168            (
169                store.kernel_specifications().cloned().collect::<Vec<_>>(),
170                store.sessions().cloned().collect::<Vec<_>>(),
171            )
172        });
173
174        // When there are no kernel specifications, show a link to the Zed docs explaining how to
175        // install kernels. It can be assumed they don't have a running kernel if we have no
176        // specifications.
177        if kernel_specifications.is_empty() {
178            let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
179
180            return ReplSessionsContainer::new("No Jupyter Kernels Available")
181                .child(Label::new(instructions))
182                .child(
183                    h_flex().w_full().p_4().justify_center().gap_2().child(
184                        ButtonLike::new("install-kernels")
185                            .style(ButtonStyle::Filled)
186                            .size(ButtonSize::Large)
187                            .layer(ElevationIndex::ModalSurface)
188                            .child(Label::new("Install Kernels"))
189                            .on_click(move |_, cx| {
190                                cx.open_url(
191                                    "https://zed.dev/docs/repl#language-specific-instructions",
192                                )
193                            }),
194                    ),
195                );
196        }
197
198        let mut kernels_by_language: HashMap<SharedString, Vec<&KernelSpecification>> =
199            kernel_specifications
200                .iter()
201                .map(|spec| (spec.language(), spec))
202                .fold(HashMap::new(), |mut acc, (language, spec)| {
203                    acc.entry(language).or_default().push(spec);
204                    acc
205                });
206
207        for kernels in kernels_by_language.values_mut() {
208            kernels.sort_by_key(|a| a.name())
209        }
210
211        // Convert to a sorted Vec of tuples
212        let mut sorted_kernels: Vec<(SharedString, Vec<&KernelSpecification>)> =
213            kernels_by_language.into_iter().collect();
214        sorted_kernels.sort_by(|a, b| a.0.cmp(&b.0));
215
216        let kernels_available = v_flex()
217            .child(Label::new("Kernels available").size(LabelSize::Large))
218            .gap_2()
219            .child(
220                h_flex()
221                    .child(Label::new(
222                        "Defaults indicated with a checkmark. Learn how to change your default kernel in the ",
223                    ))
224                    .child(
225                        ButtonLike::new("configure-kernels")
226                            .style(ButtonStyle::Filled)
227                            // .size(ButtonSize::Compact)
228                            .layer(ElevationIndex::Surface)
229                            .child(Label::new("REPL documentation"))
230                            .child(Icon::new(IconName::Link))
231                            .on_click(move |_, cx| {
232                                cx.open_url(KERNEL_DOCS_URL)
233                            }),
234                    ),
235            )
236            .children(sorted_kernels.into_iter().map(|(language, specs)| {
237                let chosen_kernel = store.read(cx).kernelspec(&language, cx);
238
239                v_flex()
240                    .gap_1()
241                    .child(Label::new(language.clone()).weight(FontWeight::BOLD))
242                    .children(specs.into_iter().map(|spec| {
243                        let is_choice = if let Some(chosen_kernel) = &chosen_kernel {
244                            chosen_kernel == spec
245                        } else {
246                            false
247                        };
248
249                        let path = spec.path();
250
251                        ListItem::new(path.clone())
252                            .selectable(false)
253                            .tooltip({
254                                let path = path.clone();
255                                move |cx| Tooltip::text(path.clone(), cx)})
256                            .child(
257                                h_flex()
258                                    .gap_1()
259                                    .child(div().id(path.clone()).child(Label::new(spec.name())))
260                                    .when(is_choice, |el| {
261
262                                        let language = language.clone();
263
264                                        el.child(
265
266                                        div().id("check").tooltip(move |cx| Tooltip::text(format!("Default Kernel for {language}"), cx))
267                                            .child(Icon::new(IconName::Check)))}),
268                            )
269
270                    }))
271            }));
272
273        // When there are no sessions, show the command to run code in an editor
274        if sessions.is_empty() {
275            let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
276
277            return ReplSessionsContainer::new("No Jupyter Kernel Sessions")
278                .child(
279                    v_flex()
280                        .child(Label::new(instructions))
281                        .children(KeyBinding::for_action(&Run, cx)),
282                )
283                .child(div().pt_3().child(kernels_available));
284        }
285
286        ReplSessionsContainer::new("Jupyter Kernel Sessions")
287            .children(sessions)
288            .child(kernels_available)
289    }
290}
291
292#[derive(IntoElement)]
293struct ReplSessionsContainer {
294    title: SharedString,
295    children: Vec<AnyElement>,
296}
297
298impl ReplSessionsContainer {
299    pub fn new(title: impl Into<SharedString>) -> Self {
300        Self {
301            title: title.into(),
302            children: Vec::new(),
303        }
304    }
305}
306
307impl ParentElement for ReplSessionsContainer {
308    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
309        self.children.extend(elements)
310    }
311}
312
313impl RenderOnce for ReplSessionsContainer {
314    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
315        v_flex()
316            .p_4()
317            .gap_2()
318            .size_full()
319            .child(Label::new(self.title).size(LabelSize::Large))
320            .children(self.children)
321    }
322}