repl_sessions_ui.rs

  1use editor::Editor;
  2use gpui::{
  3    actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
  4    Subscription, View,
  5};
  6use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
  7use util::ResultExt as _;
  8use workspace::item::ItemEvent;
  9use workspace::WorkspaceId;
 10use workspace::{item::Item, Workspace};
 11
 12use crate::components::KernelListItem;
 13use crate::jupyter_settings::JupyterSettings;
 14use crate::repl_store::ReplStore;
 15
 16actions!(
 17    repl,
 18    [
 19        Run,
 20        ClearOutputs,
 21        Sessions,
 22        Interrupt,
 23        Shutdown,
 24        RefreshKernelspecs
 25    ]
 26);
 27
 28pub fn init(cx: &mut AppContext) {
 29    cx.observe_new_views(
 30        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
 31            workspace.register_action(|workspace, _: &Sessions, cx| {
 32                let existing = workspace
 33                    .active_pane()
 34                    .read(cx)
 35                    .items()
 36                    .find_map(|item| item.downcast::<ReplSessionsPage>());
 37
 38                if let Some(existing) = existing {
 39                    workspace.activate_item(&existing, true, true, cx);
 40                } else {
 41                    let repl_sessions_page = ReplSessionsPage::new(cx);
 42                    workspace.add_item_to_active_pane(Box::new(repl_sessions_page), None, true, cx)
 43                }
 44            });
 45
 46            workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
 47                let store = ReplStore::global(cx);
 48                store.update(cx, |store, cx| {
 49                    store.refresh_kernelspecs(cx).detach();
 50                });
 51            });
 52        },
 53    )
 54    .detach();
 55
 56    cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
 57        if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
 58            return;
 59        }
 60
 61        let editor_handle = cx.view().downgrade();
 62
 63        editor
 64            .register_action({
 65                let editor_handle = editor_handle.clone();
 66                move |_: &Run, cx| {
 67                    if !JupyterSettings::enabled(cx) {
 68                        return;
 69                    }
 70
 71                    crate::run(editor_handle.clone(), cx).log_err();
 72                }
 73            })
 74            .detach();
 75
 76        editor
 77            .register_action({
 78                let editor_handle = editor_handle.clone();
 79                move |_: &ClearOutputs, cx| {
 80                    if !JupyterSettings::enabled(cx) {
 81                        return;
 82                    }
 83
 84                    crate::clear_outputs(editor_handle.clone(), cx);
 85                }
 86            })
 87            .detach();
 88
 89        editor
 90            .register_action({
 91                let editor_handle = editor_handle.clone();
 92                move |_: &Interrupt, cx| {
 93                    if !JupyterSettings::enabled(cx) {
 94                        return;
 95                    }
 96
 97                    crate::interrupt(editor_handle.clone(), cx);
 98                }
 99            })
100            .detach();
101
102        editor
103            .register_action({
104                let editor_handle = editor_handle.clone();
105                move |_: &Shutdown, cx| {
106                    if !JupyterSettings::enabled(cx) {
107                        return;
108                    }
109
110                    crate::shutdown(editor_handle.clone(), cx);
111                }
112            })
113            .detach();
114    })
115    .detach();
116}
117
118pub struct ReplSessionsPage {
119    focus_handle: FocusHandle,
120    _subscriptions: Vec<Subscription>,
121}
122
123impl ReplSessionsPage {
124    pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
125        cx.new_view(|cx: &mut ViewContext<Self>| {
126            let focus_handle = cx.focus_handle();
127
128            let subscriptions = vec![
129                cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
130                cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
131            ];
132
133            Self {
134                focus_handle,
135                _subscriptions: subscriptions,
136            }
137        })
138    }
139}
140
141impl EventEmitter<ItemEvent> for ReplSessionsPage {}
142
143impl FocusableView for ReplSessionsPage {
144    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
145        self.focus_handle.clone()
146    }
147}
148
149impl Item for ReplSessionsPage {
150    type Event = ItemEvent;
151
152    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
153        Some("REPL Sessions".into())
154    }
155
156    fn telemetry_event_text(&self) -> Option<&'static str> {
157        Some("repl sessions")
158    }
159
160    fn show_toolbar(&self) -> bool {
161        false
162    }
163
164    fn clone_on_split(
165        &self,
166        _workspace_id: Option<WorkspaceId>,
167        _: &mut ViewContext<Self>,
168    ) -> Option<View<Self>> {
169        None
170    }
171
172    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
173        f(*event)
174    }
175}
176
177impl Render for ReplSessionsPage {
178    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
179        let store = ReplStore::global(cx);
180
181        let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
182            (
183                store.kernel_specifications().cloned().collect::<Vec<_>>(),
184                store.sessions().cloned().collect::<Vec<_>>(),
185            )
186        });
187
188        // When there are no kernel specifications, show a link to the Zed docs explaining how to
189        // install kernels. It can be assumed they don't have a running kernel if we have no
190        // specifications.
191        if kernel_specifications.is_empty() {
192            let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
193
194            return ReplSessionsContainer::new("No Jupyter Kernels Available")
195                .child(Label::new(instructions))
196                .child(
197                    h_flex().w_full().p_4().justify_center().gap_2().child(
198                        ButtonLike::new("install-kernels")
199                            .style(ButtonStyle::Filled)
200                            .size(ButtonSize::Large)
201                            .layer(ElevationIndex::ModalSurface)
202                            .child(Label::new("Install Kernels"))
203                            .on_click(move |_, cx| {
204                                cx.open_url(
205                                    "https://docs.jupyter.org/en/latest/install/kernels.html",
206                                )
207                            }),
208                    ),
209                );
210        }
211
212        // When there are no sessions, show the command to run code in an editor
213        if sessions.is_empty() {
214            let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
215
216            return ReplSessionsContainer::new("No Jupyter Kernel Sessions")
217                .child(
218                    v_flex()
219                        .child(Label::new(instructions))
220                        .children(KeyBinding::for_action(&Run, cx)),
221                )
222                .child(Label::new("Kernels available").size(LabelSize::Large))
223                .children(kernel_specifications.into_iter().map(|spec| {
224                    KernelListItem::new(spec.clone()).child(
225                        h_flex()
226                            .gap_2()
227                            .child(Label::new(spec.name))
228                            .child(Label::new(spec.kernelspec.language).color(Color::Muted)),
229                    )
230                }));
231        }
232
233        ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
234    }
235}
236
237#[derive(IntoElement)]
238struct ReplSessionsContainer {
239    title: SharedString,
240    children: Vec<AnyElement>,
241}
242
243impl ReplSessionsContainer {
244    pub fn new(title: impl Into<SharedString>) -> Self {
245        Self {
246            title: title.into(),
247            children: Vec::new(),
248        }
249    }
250}
251
252impl ParentElement for ReplSessionsContainer {
253    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
254        self.children.extend(elements)
255    }
256}
257
258impl RenderOnce for ReplSessionsContainer {
259    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
260        v_flex()
261            .p_4()
262            .gap_2()
263            .size_full()
264            .child(Label::new(self.title).size(LabelSize::Large))
265            .children(self.children)
266    }
267}