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