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