repl_sessions_ui.rs

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