repl_sessions_ui.rs

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