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 RunInPlace,
21 ClearOutputs,
22 Sessions,
23 Interrupt,
24 Shutdown,
25 RefreshKernelspecs
26 ]
27);
28
29pub fn init(cx: &mut AppContext) {
30 cx.observe_new_views(
31 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
32 workspace.register_action(|workspace, _: &Sessions, cx| {
33 let existing = workspace
34 .active_pane()
35 .read(cx)
36 .items()
37 .find_map(|item| item.downcast::<ReplSessionsPage>());
38
39 if let Some(existing) = existing {
40 workspace.activate_item(&existing, true, true, cx);
41 } else {
42 let repl_sessions_page = ReplSessionsPage::new(cx);
43 workspace.add_item_to_active_pane(Box::new(repl_sessions_page), None, true, cx)
44 }
45 });
46
47 workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
48 let store = ReplStore::global(cx);
49 store.update(cx, |store, cx| {
50 store.refresh_kernelspecs(cx).detach();
51 });
52 });
53 },
54 )
55 .detach();
56
57 cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
58 if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
59 return;
60 }
61
62 let editor_handle = cx.view().downgrade();
63
64 editor
65 .register_action({
66 let editor_handle = editor_handle.clone();
67 move |_: &Run, cx| {
68 if !JupyterSettings::enabled(cx) {
69 return;
70 }
71
72 crate::run(editor_handle.clone(), true, cx).log_err();
73 }
74 })
75 .detach();
76
77 editor
78 .register_action({
79 let editor_handle = editor_handle.clone();
80 move |_: &RunInPlace, cx| {
81 if !JupyterSettings::enabled(cx) {
82 return;
83 }
84
85 crate::run(editor_handle.clone(), false, cx).log_err();
86 }
87 })
88 .detach();
89
90 editor
91 .register_action({
92 let editor_handle = editor_handle.clone();
93 move |_: &ClearOutputs, cx| {
94 if !JupyterSettings::enabled(cx) {
95 return;
96 }
97
98 crate::clear_outputs(editor_handle.clone(), cx);
99 }
100 })
101 .detach();
102
103 editor
104 .register_action({
105 let editor_handle = editor_handle.clone();
106 move |_: &Interrupt, cx| {
107 if !JupyterSettings::enabled(cx) {
108 return;
109 }
110
111 crate::interrupt(editor_handle.clone(), cx);
112 }
113 })
114 .detach();
115
116 editor
117 .register_action({
118 let editor_handle = editor_handle.clone();
119 move |_: &Shutdown, cx| {
120 if !JupyterSettings::enabled(cx) {
121 return;
122 }
123
124 crate::shutdown(editor_handle.clone(), cx);
125 }
126 })
127 .detach();
128 })
129 .detach();
130}
131
132pub struct ReplSessionsPage {
133 focus_handle: FocusHandle,
134 _subscriptions: Vec<Subscription>,
135}
136
137impl ReplSessionsPage {
138 pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
139 cx.new_view(|cx: &mut ViewContext<Self>| {
140 let focus_handle = cx.focus_handle();
141
142 let subscriptions = vec![
143 cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
144 cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
145 ];
146
147 Self {
148 focus_handle,
149 _subscriptions: subscriptions,
150 }
151 })
152 }
153}
154
155impl EventEmitter<ItemEvent> for ReplSessionsPage {}
156
157impl FocusableView for ReplSessionsPage {
158 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
159 self.focus_handle.clone()
160 }
161}
162
163impl Item for ReplSessionsPage {
164 type Event = ItemEvent;
165
166 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
167 Some("REPL Sessions".into())
168 }
169
170 fn telemetry_event_text(&self) -> Option<&'static str> {
171 Some("repl sessions")
172 }
173
174 fn show_toolbar(&self) -> bool {
175 false
176 }
177
178 fn clone_on_split(
179 &self,
180 _workspace_id: Option<WorkspaceId>,
181 _: &mut ViewContext<Self>,
182 ) -> Option<View<Self>> {
183 None
184 }
185
186 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
187 f(*event)
188 }
189}
190
191impl Render for ReplSessionsPage {
192 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
193 let store = ReplStore::global(cx);
194
195 let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
196 (
197 store.kernel_specifications().cloned().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://docs.jupyter.org/en/latest/install/kernels.html",
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")
231 .child(
232 v_flex()
233 .child(Label::new(instructions))
234 .children(KeyBinding::for_action(&Run, cx)),
235 )
236 .child(Label::new("Kernels available").size(LabelSize::Large))
237 .children(kernel_specifications.into_iter().map(|spec| {
238 KernelListItem::new(spec.clone()).child(
239 h_flex()
240 .gap_2()
241 .child(Label::new(spec.name))
242 .child(Label::new(spec.kernelspec.language).color(Color::Muted)),
243 )
244 }));
245 }
246
247 ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
248 }
249}
250
251#[derive(IntoElement)]
252struct ReplSessionsContainer {
253 title: SharedString,
254 children: Vec<AnyElement>,
255}
256
257impl ReplSessionsContainer {
258 pub fn new(title: impl Into<SharedString>) -> Self {
259 Self {
260 title: title.into(),
261 children: Vec::new(),
262 }
263 }
264}
265
266impl ParentElement for ReplSessionsContainer {
267 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
268 self.children.extend(elements)
269 }
270}
271
272impl RenderOnce for ReplSessionsContainer {
273 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
274 v_flex()
275 .p_4()
276 .gap_2()
277 .size_full()
278 .child(Label::new(self.title).size(LabelSize::Large))
279 .children(self.children)
280 }
281}