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 project = editor.project().cloned();
83
84 let is_local_project = project
85 .as_ref()
86 .map(|project| project.read(cx).is_local())
87 .unwrap_or(false);
88
89 if !is_local_project {
90 return;
91 }
92
93 let buffer = editor.buffer().read(cx).as_singleton();
94
95 let language = buffer
96 .as_ref()
97 .and_then(|buffer| buffer.read(cx).language());
98
99 let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
100
101 let editor_handle = cx.entity().downgrade();
102
103 if let Some(language) = language
104 && language.name() == "Python".into()
105 && let (Some(project_path), Some(project)) = (project_path, project)
106 {
107 let store = ReplStore::global(cx);
108 store.update(cx, |store, cx| {
109 store
110 .refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
111 .detach_and_log_err(cx);
112 });
113 }
114
115 editor
116 .register_action({
117 let editor_handle = editor_handle.clone();
118 move |_: &Run, window, cx| {
119 if !JupyterSettings::enabled(cx) {
120 return;
121 }
122
123 crate::run(editor_handle.clone(), true, window, cx).log_err();
124 }
125 })
126 .detach();
127
128 editor
129 .register_action({
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 to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) {
194 f(*event)
195 }
196}
197
198impl Render for ReplSessionsPage {
199 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
200 let store = ReplStore::global(cx);
201
202 let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
203 (
204 store
205 .pure_jupyter_kernel_specifications()
206 .cloned()
207 .collect::<Vec<_>>(),
208 store.sessions().cloned().collect::<Vec<_>>(),
209 )
210 });
211
212 // When there are no kernel specifications, show a link to the Zed docs explaining how to
213 // install kernels. It can be assumed they don't have a running kernel if we have no
214 // specifications.
215 if kernel_specifications.is_empty() {
216 let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
217
218 return ReplSessionsContainer::new("No Jupyter Kernels Available")
219 .child(Label::new(instructions))
220 .child(
221 h_flex().w_full().p_4().justify_center().gap_2().child(
222 ButtonLike::new("install-kernels")
223 .style(ButtonStyle::Filled)
224 .size(ButtonSize::Large)
225 .layer(ElevationIndex::ModalSurface)
226 .child(Label::new("Install Kernels"))
227 .on_click(move |_, _, cx| {
228 cx.open_url(
229 "https://zed.dev/docs/repl#language-specific-instructions",
230 )
231 }),
232 ),
233 );
234 }
235
236 // When there are no sessions, show the command to run code in an editor
237 if sessions.is_empty() {
238 let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
239
240 return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child(
241 v_flex()
242 .child(Label::new(instructions))
243 .child(KeyBinding::for_action(&Run, cx)),
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, _window: &mut Window, _cx: &mut App) -> 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}