1//! REPL operations on an [`Editor`].
2
3use std::ops::Range;
4use std::sync::Arc;
5
6use anyhow::{Context, Result};
7use editor::Editor;
8use gpui::{prelude::*, AppContext, Entity, View, WeakView, WindowContext};
9use language::{BufferSnapshot, Language, Point};
10
11use crate::repl_store::ReplStore;
12use crate::session::SessionEvent;
13use crate::{KernelSpecification, Session};
14
15pub fn run(editor: WeakView<Editor>, cx: &mut WindowContext) -> Result<()> {
16 let store = ReplStore::global(cx);
17 if !store.read(cx).is_enabled() {
18 return Ok(());
19 }
20
21 let editor = editor.upgrade().context("editor was dropped")?;
22 let selected_range = editor
23 .update(cx, |editor, cx| editor.selections.newest_adjusted(cx))
24 .range();
25 let multibuffer = editor.read(cx).buffer().clone();
26 let Some(buffer) = multibuffer.read(cx).as_singleton() else {
27 return Ok(());
28 };
29
30 let (ranges, next_cell_point) = snippet_ranges(&buffer.read(cx).snapshot(), selected_range);
31
32 for range in ranges {
33 let Some(language) = multibuffer.read(cx).language_at(range.start, cx) else {
34 continue;
35 };
36
37 let kernel_specification = store.update(cx, |store, cx| {
38 store
39 .kernelspec(&language, cx)
40 .with_context(|| format!("No kernel found for language: {}", language.name()))
41 })?;
42
43 let fs = store.read(cx).fs().clone();
44 let telemetry = store.read(cx).telemetry().clone();
45
46 let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
47 {
48 session
49 } else {
50 let weak_editor = editor.downgrade();
51 let session = cx
52 .new_view(|cx| Session::new(weak_editor, fs, telemetry, kernel_specification, cx));
53
54 editor.update(cx, |_editor, cx| {
55 cx.notify();
56
57 cx.subscribe(&session, {
58 let store = store.clone();
59 move |_this, _session, event, cx| match event {
60 SessionEvent::Shutdown(shutdown_event) => {
61 store.update(cx, |store, _cx| {
62 store.remove_session(shutdown_event.entity_id());
63 });
64 }
65 }
66 })
67 .detach();
68 });
69
70 store.update(cx, |store, _cx| {
71 store.insert_session(editor.entity_id(), session.clone());
72 });
73
74 session
75 };
76
77 let selected_text;
78 let anchor_range;
79 let next_cursor;
80 {
81 let snapshot = multibuffer.read(cx).read(cx);
82 selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
83 anchor_range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end);
84 next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
85 }
86
87 session.update(cx, |session, cx| {
88 session.execute(selected_text, anchor_range, next_cursor, cx);
89 });
90 }
91
92 anyhow::Ok(())
93}
94
95pub enum SessionSupport {
96 ActiveSession(View<Session>),
97 Inactive(Box<KernelSpecification>),
98 RequiresSetup(Arc<str>),
99 Unsupported,
100}
101
102pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
103 let store = ReplStore::global(cx);
104 let entity_id = editor.entity_id();
105
106 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
107 return SessionSupport::ActiveSession(session);
108 };
109
110 let Some(language) = get_language(editor, cx) else {
111 return SessionSupport::Unsupported;
112 };
113 let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
114
115 match kernelspec {
116 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
117 None => match language.name().as_ref() {
118 "TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
119 _ => SessionSupport::Unsupported,
120 },
121 }
122}
123
124pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
125 let store = ReplStore::global(cx);
126 let entity_id = editor.entity_id();
127 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
128 return;
129 };
130 session.update(cx, |session, cx| {
131 session.clear_outputs(cx);
132 cx.notify();
133 });
134}
135
136pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
137 let store = ReplStore::global(cx);
138 let entity_id = editor.entity_id();
139 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
140 return;
141 };
142
143 session.update(cx, |session, cx| {
144 session.interrupt(cx);
145 cx.notify();
146 });
147}
148
149pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
150 let store = ReplStore::global(cx);
151 let entity_id = editor.entity_id();
152 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
153 return;
154 };
155
156 session.update(cx, |session, cx| {
157 session.shutdown(cx);
158 cx.notify();
159 });
160}
161
162fn snippet_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
163 let mut snippet_end_row = end_row;
164 while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
165 snippet_end_row -= 1;
166 }
167 Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
168}
169
170// Returns the ranges of the snippets in the buffer and the next range for moving the cursor to
171fn jupytext_snippets(
172 buffer: &BufferSnapshot,
173 range: Range<Point>,
174) -> (Vec<Range<Point>>, Option<Point>) {
175 let mut current_row = range.start.row;
176
177 let Some(language) = buffer.language() else {
178 return (Vec::new(), None);
179 };
180
181 let default_scope = language.default_scope();
182 let comment_prefixes = default_scope.line_comment_prefixes();
183 if comment_prefixes.is_empty() {
184 return (Vec::new(), None);
185 }
186
187 let jupytext_prefixes = comment_prefixes
188 .iter()
189 .map(|comment_prefix| format!("{comment_prefix}%%"))
190 .collect::<Vec<_>>();
191
192 let mut snippet_start_row = None;
193 loop {
194 if jupytext_prefixes
195 .iter()
196 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
197 {
198 snippet_start_row = Some(current_row);
199 break;
200 } else if current_row > 0 {
201 current_row -= 1;
202 } else {
203 break;
204 }
205 }
206
207 let mut snippets = Vec::new();
208 if let Some(mut snippet_start_row) = snippet_start_row {
209 for current_row in range.start.row + 1..=buffer.max_point().row {
210 if jupytext_prefixes
211 .iter()
212 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
213 {
214 snippets.push(snippet_range(buffer, snippet_start_row, current_row - 1));
215
216 if current_row <= range.end.row {
217 snippet_start_row = current_row;
218 } else {
219 // Return our snippets as well as the next range for moving the cursor to
220 return (snippets, Some(Point::new(current_row, 0)));
221 }
222 }
223 }
224
225 // Go to the end of the buffer (no more jupytext cells found)
226 snippets.push(snippet_range(
227 buffer,
228 snippet_start_row,
229 buffer.max_point().row,
230 ));
231 }
232
233 (snippets, None)
234}
235
236fn snippet_ranges(
237 buffer: &BufferSnapshot,
238 range: Range<Point>,
239) -> (Vec<Range<Point>>, Option<Point>) {
240 let (jupytext_snippets, next_cursor) = jupytext_snippets(buffer, range.clone());
241 if !jupytext_snippets.is_empty() {
242 return (jupytext_snippets, next_cursor);
243 }
244
245 let snippet_range = snippet_range(buffer, range.start.row, range.end.row);
246 let start_language = buffer.language_at(snippet_range.start);
247 let end_language = buffer.language_at(snippet_range.end);
248
249 if let Some((start, end)) = start_language.zip(end_language) {
250 if start == end {
251 return (vec![snippet_range], None);
252 }
253 }
254
255 (Vec::new(), None)
256}
257
258fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
259 let editor = editor.upgrade()?;
260 let selection = editor.read(cx).selections.newest::<usize>(cx);
261 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
262 buffer.language_at(selection.head()).cloned()
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use gpui::Context;
269 use indoc::indoc;
270 use language::{Buffer, Language, LanguageConfig};
271
272 #[gpui::test]
273 fn test_snippet_ranges(cx: &mut AppContext) {
274 // Create a test language
275 let test_language = Arc::new(Language::new(
276 LanguageConfig {
277 name: "TestLang".into(),
278 line_comments: vec!["# ".into()],
279 ..Default::default()
280 },
281 None,
282 ));
283
284 let buffer = cx.new_model(|cx| {
285 Buffer::local(
286 indoc! { r#"
287 print(1 + 1)
288 print(2 + 2)
289
290 print(4 + 4)
291
292
293 "# },
294 cx,
295 )
296 .with_language(test_language, cx)
297 });
298 let snapshot = buffer.read(cx).snapshot();
299
300 // Single-point selection
301 let (snippets, _) = snippet_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
302 let snippets = snippets
303 .into_iter()
304 .map(|range| snapshot.text_for_range(range).collect::<String>())
305 .collect::<Vec<_>>();
306 assert_eq!(snippets, vec!["print(1 + 1)"]);
307
308 // Multi-line selection
309 let (snippets, _) = snippet_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
310 let snippets = snippets
311 .into_iter()
312 .map(|range| snapshot.text_for_range(range).collect::<String>())
313 .collect::<Vec<_>>();
314 assert_eq!(
315 snippets,
316 vec![indoc! { r#"
317 print(1 + 1)
318 print(2 + 2)"# }]
319 );
320
321 // Trimming multiple trailing blank lines
322 let (snippets, _) = snippet_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
323
324 let snippets = snippets
325 .into_iter()
326 .map(|range| snapshot.text_for_range(range).collect::<String>())
327 .collect::<Vec<_>>();
328 assert_eq!(
329 snippets,
330 vec![indoc! { r#"
331 print(1 + 1)
332 print(2 + 2)
333
334 print(4 + 4)"# }]
335 );
336 }
337
338 #[gpui::test]
339 fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
340 // Create a test language
341 let test_language = Arc::new(Language::new(
342 LanguageConfig {
343 name: "TestLang".into(),
344 line_comments: vec!["# ".into()],
345 ..Default::default()
346 },
347 None,
348 ));
349
350 let buffer = cx.new_model(|cx| {
351 Buffer::local(
352 indoc! { r#"
353 # Hello!
354 # %% [markdown]
355 # This is some arithmetic
356 print(1 + 1)
357 print(2 + 2)
358
359 # %%
360 print(3 + 3)
361 print(4 + 4)
362
363 print(5 + 5)
364
365
366
367 "# },
368 cx,
369 )
370 .with_language(test_language, cx)
371 });
372 let snapshot = buffer.read(cx).snapshot();
373
374 // Jupytext snippet surrounding an empty selection
375 let (snippets, _) = snippet_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
376
377 let snippets = snippets
378 .into_iter()
379 .map(|range| snapshot.text_for_range(range).collect::<String>())
380 .collect::<Vec<_>>();
381 assert_eq!(
382 snippets,
383 vec![indoc! { r#"
384 # %% [markdown]
385 # This is some arithmetic
386 print(1 + 1)
387 print(2 + 2)"# }]
388 );
389
390 // Jupytext snippets intersecting a non-empty selection
391 let (snippets, _) = snippet_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
392 let snippets = snippets
393 .into_iter()
394 .map(|range| snapshot.text_for_range(range).collect::<String>())
395 .collect::<Vec<_>>();
396 assert_eq!(
397 snippets,
398 vec![
399 indoc! { r#"
400 # %% [markdown]
401 # This is some arithmetic
402 print(1 + 1)
403 print(2 + 2)"#
404 },
405 indoc! { r#"
406 # %%
407 print(3 + 3)
408 print(4 + 4)
409
410 print(5 + 5)"#
411 }
412 ]
413 );
414 }
415}