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 (runnable_ranges, next_cell_point) =
31 runnable_ranges(&buffer.read(cx).snapshot(), selected_range);
32
33 for runnable_range in runnable_ranges {
34 let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
35 continue;
36 };
37
38 let kernel_specification = store.update(cx, |store, cx| {
39 store
40 .kernelspec(&language, cx)
41 .with_context(|| format!("No kernel found for language: {}", language.name()))
42 })?;
43
44 let fs = store.read(cx).fs().clone();
45 let telemetry = store.read(cx).telemetry().clone();
46
47 let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
48 {
49 session
50 } else {
51 let weak_editor = editor.downgrade();
52 let session = cx
53 .new_view(|cx| Session::new(weak_editor, fs, telemetry, kernel_specification, cx));
54
55 editor.update(cx, |_editor, cx| {
56 cx.notify();
57
58 cx.subscribe(&session, {
59 let store = store.clone();
60 move |_this, _session, event, cx| match event {
61 SessionEvent::Shutdown(shutdown_event) => {
62 store.update(cx, |store, _cx| {
63 store.remove_session(shutdown_event.entity_id());
64 });
65 }
66 }
67 })
68 .detach();
69 });
70
71 store.update(cx, |store, _cx| {
72 store.insert_session(editor.entity_id(), session.clone());
73 });
74
75 session
76 };
77
78 let selected_text;
79 let anchor_range;
80 let next_cursor;
81 {
82 let snapshot = multibuffer.read(cx).read(cx);
83 selected_text = snapshot
84 .text_for_range(runnable_range.clone())
85 .collect::<String>();
86 anchor_range = snapshot.anchor_before(runnable_range.start)
87 ..snapshot.anchor_after(runnable_range.end);
88 next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
89 }
90
91 session.update(cx, |session, cx| {
92 session.execute(selected_text, anchor_range, next_cursor, cx);
93 });
94 }
95
96 anyhow::Ok(())
97}
98
99pub enum SessionSupport {
100 ActiveSession(View<Session>),
101 Inactive(Box<KernelSpecification>),
102 RequiresSetup(Arc<str>),
103 Unsupported,
104}
105
106pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
107 let store = ReplStore::global(cx);
108 let entity_id = editor.entity_id();
109
110 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
111 return SessionSupport::ActiveSession(session);
112 };
113
114 let Some(language) = get_language(editor, cx) else {
115 return SessionSupport::Unsupported;
116 };
117 let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
118
119 match kernelspec {
120 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
121 None => {
122 if language_supported(&language) {
123 SessionSupport::RequiresSetup(language.name())
124 } else {
125 SessionSupport::Unsupported
126 }
127 }
128 }
129}
130
131pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
132 let store = ReplStore::global(cx);
133 let entity_id = editor.entity_id();
134 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
135 return;
136 };
137 session.update(cx, |session, cx| {
138 session.clear_outputs(cx);
139 cx.notify();
140 });
141}
142
143pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
144 let store = ReplStore::global(cx);
145 let entity_id = editor.entity_id();
146 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
147 return;
148 };
149
150 session.update(cx, |session, cx| {
151 session.interrupt(cx);
152 cx.notify();
153 });
154}
155
156pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
157 let store = ReplStore::global(cx);
158 let entity_id = editor.entity_id();
159 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
160 return;
161 };
162
163 session.update(cx, |session, cx| {
164 session.shutdown(cx);
165 cx.notify();
166 });
167}
168
169fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
170 let mut snippet_end_row = end_row;
171 while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
172 snippet_end_row -= 1;
173 }
174 Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
175}
176
177// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to
178fn jupytext_cells(
179 buffer: &BufferSnapshot,
180 range: Range<Point>,
181) -> (Vec<Range<Point>>, Option<Point>) {
182 let mut current_row = range.start.row;
183
184 let Some(language) = buffer.language() else {
185 return (Vec::new(), None);
186 };
187
188 let default_scope = language.default_scope();
189 let comment_prefixes = default_scope.line_comment_prefixes();
190 if comment_prefixes.is_empty() {
191 return (Vec::new(), None);
192 }
193
194 let jupytext_prefixes = comment_prefixes
195 .iter()
196 .map(|comment_prefix| format!("{comment_prefix}%%"))
197 .collect::<Vec<_>>();
198
199 let mut snippet_start_row = None;
200 loop {
201 if jupytext_prefixes
202 .iter()
203 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
204 {
205 snippet_start_row = Some(current_row);
206 break;
207 } else if current_row > 0 {
208 current_row -= 1;
209 } else {
210 break;
211 }
212 }
213
214 let mut snippets = Vec::new();
215 if let Some(mut snippet_start_row) = snippet_start_row {
216 for current_row in range.start.row + 1..=buffer.max_point().row {
217 if jupytext_prefixes
218 .iter()
219 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
220 {
221 snippets.push(cell_range(buffer, snippet_start_row, current_row - 1));
222
223 if current_row <= range.end.row {
224 snippet_start_row = current_row;
225 } else {
226 // Return our snippets as well as the next point for moving the cursor to
227 return (snippets, Some(Point::new(current_row, 0)));
228 }
229 }
230 }
231
232 // Go to the end of the buffer (no more jupytext cells found)
233 snippets.push(cell_range(
234 buffer,
235 snippet_start_row,
236 buffer.max_point().row,
237 ));
238 }
239
240 (snippets, None)
241}
242
243fn runnable_ranges(
244 buffer: &BufferSnapshot,
245 range: Range<Point>,
246) -> (Vec<Range<Point>>, Option<Point>) {
247 if let Some(language) = buffer.language() {
248 if language.name().as_ref() == "Markdown" {
249 return (markdown_code_blocks(buffer, range.clone()), None);
250 }
251 }
252
253 let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone());
254 if !jupytext_snippets.is_empty() {
255 return (jupytext_snippets, next_cursor);
256 }
257
258 let snippet_range = cell_range(buffer, range.start.row, range.end.row);
259 let start_language = buffer.language_at(snippet_range.start);
260 let end_language = buffer.language_at(snippet_range.end);
261
262 if start_language
263 .zip(end_language)
264 .map_or(false, |(start, end)| start == end)
265 {
266 (vec![snippet_range], None)
267 } else {
268 (Vec::new(), None)
269 }
270}
271
272// We allow markdown code blocks to end in a trailing newline in order to render the output
273// below the final code fence. This is different than our behavior for selections and Jupytext cells.
274fn markdown_code_blocks(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
275 buffer
276 .injections_intersecting_range(range)
277 .filter(|(_, language)| language_supported(language))
278 .map(|(content_range, _)| {
279 buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
280 })
281 .collect()
282}
283
284fn language_supported(language: &Arc<Language>) -> bool {
285 match language.name().as_ref() {
286 "TypeScript" | "Python" => true,
287 _ => false,
288 }
289}
290
291fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
292 let editor = editor.upgrade()?;
293 let selection = editor.read(cx).selections.newest::<usize>(cx);
294 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
295 buffer.language_at(selection.head()).cloned()
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use gpui::{Context, Task};
302 use indoc::indoc;
303 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
304
305 #[gpui::test]
306 fn test_snippet_ranges(cx: &mut AppContext) {
307 // Create a test language
308 let test_language = Arc::new(Language::new(
309 LanguageConfig {
310 name: "TestLang".into(),
311 line_comments: vec!["# ".into()],
312 ..Default::default()
313 },
314 None,
315 ));
316
317 let buffer = cx.new_model(|cx| {
318 Buffer::local(
319 indoc! { r#"
320 print(1 + 1)
321 print(2 + 2)
322
323 print(4 + 4)
324
325
326 "# },
327 cx,
328 )
329 .with_language(test_language, cx)
330 });
331 let snapshot = buffer.read(cx).snapshot();
332
333 // Single-point selection
334 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
335 let snippets = snippets
336 .into_iter()
337 .map(|range| snapshot.text_for_range(range).collect::<String>())
338 .collect::<Vec<_>>();
339 assert_eq!(snippets, vec!["print(1 + 1)"]);
340
341 // Multi-line selection
342 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
343 let snippets = snippets
344 .into_iter()
345 .map(|range| snapshot.text_for_range(range).collect::<String>())
346 .collect::<Vec<_>>();
347 assert_eq!(
348 snippets,
349 vec![indoc! { r#"
350 print(1 + 1)
351 print(2 + 2)"# }]
352 );
353
354 // Trimming multiple trailing blank lines
355 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
356
357 let snippets = snippets
358 .into_iter()
359 .map(|range| snapshot.text_for_range(range).collect::<String>())
360 .collect::<Vec<_>>();
361 assert_eq!(
362 snippets,
363 vec![indoc! { r#"
364 print(1 + 1)
365 print(2 + 2)
366
367 print(4 + 4)"# }]
368 );
369 }
370
371 #[gpui::test]
372 fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
373 // Create a test language
374 let test_language = Arc::new(Language::new(
375 LanguageConfig {
376 name: "TestLang".into(),
377 line_comments: vec!["# ".into()],
378 ..Default::default()
379 },
380 None,
381 ));
382
383 let buffer = cx.new_model(|cx| {
384 Buffer::local(
385 indoc! { r#"
386 # Hello!
387 # %% [markdown]
388 # This is some arithmetic
389 print(1 + 1)
390 print(2 + 2)
391
392 # %%
393 print(3 + 3)
394 print(4 + 4)
395
396 print(5 + 5)
397
398
399
400 "# },
401 cx,
402 )
403 .with_language(test_language, cx)
404 });
405 let snapshot = buffer.read(cx).snapshot();
406
407 // Jupytext snippet surrounding an empty selection
408 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
409
410 let snippets = snippets
411 .into_iter()
412 .map(|range| snapshot.text_for_range(range).collect::<String>())
413 .collect::<Vec<_>>();
414 assert_eq!(
415 snippets,
416 vec![indoc! { r#"
417 # %% [markdown]
418 # This is some arithmetic
419 print(1 + 1)
420 print(2 + 2)"# }]
421 );
422
423 // Jupytext snippets intersecting a non-empty selection
424 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
425 let snippets = snippets
426 .into_iter()
427 .map(|range| snapshot.text_for_range(range).collect::<String>())
428 .collect::<Vec<_>>();
429 assert_eq!(
430 snippets,
431 vec![
432 indoc! { r#"
433 # %% [markdown]
434 # This is some arithmetic
435 print(1 + 1)
436 print(2 + 2)"#
437 },
438 indoc! { r#"
439 # %%
440 print(3 + 3)
441 print(4 + 4)
442
443 print(5 + 5)"#
444 }
445 ]
446 );
447 }
448
449 #[gpui::test]
450 fn test_markdown_code_blocks(cx: &mut AppContext) {
451 let markdown = languages::language("markdown", tree_sitter_md::language());
452 let typescript =
453 languages::language("typescript", tree_sitter_typescript::language_typescript());
454 let python = languages::language("python", tree_sitter_python::language());
455 let language_registry = Arc::new(LanguageRegistry::new(
456 Task::ready(()),
457 cx.background_executor().clone(),
458 ));
459 language_registry.add(markdown.clone());
460 language_registry.add(typescript.clone());
461 language_registry.add(python.clone());
462
463 // Two code blocks intersecting with selection
464 let buffer = cx.new_model(|cx| {
465 let mut buffer = Buffer::local(
466 indoc! { r#"
467 Hey this is Markdown!
468
469 ```typescript
470 let foo = 999;
471 console.log(foo + 1999);
472 ```
473
474 ```typescript
475 console.log("foo")
476 ```
477 "#
478 },
479 cx,
480 );
481 buffer.set_language_registry(language_registry.clone());
482 buffer.set_language(Some(markdown.clone()), cx);
483 buffer
484 });
485 let snapshot = buffer.read(cx).snapshot();
486
487 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
488 let snippets = snippets
489 .into_iter()
490 .map(|range| snapshot.text_for_range(range).collect::<String>())
491 .collect::<Vec<_>>();
492
493 assert_eq!(
494 snippets,
495 vec![
496 indoc! { r#"
497 let foo = 999;
498 console.log(foo + 1999);
499 "#
500 },
501 "console.log(\"foo\")\n"
502 ]
503 );
504
505 // Three code blocks intersecting with selection
506 let buffer = cx.new_model(|cx| {
507 let mut buffer = Buffer::local(
508 indoc! { r#"
509 Hey this is Markdown!
510
511 ```typescript
512 let foo = 999;
513 console.log(foo + 1999);
514 ```
515
516 ```ts
517 console.log("foo")
518 ```
519
520 ```typescript
521 console.log("another code block")
522 ```
523 "# },
524 cx,
525 );
526 buffer.set_language_registry(language_registry.clone());
527 buffer.set_language(Some(markdown.clone()), cx);
528 buffer
529 });
530 let snapshot = buffer.read(cx).snapshot();
531
532 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
533 let snippets = snippets
534 .into_iter()
535 .map(|range| snapshot.text_for_range(range).collect::<String>())
536 .collect::<Vec<_>>();
537
538 assert_eq!(
539 snippets,
540 vec![
541 indoc! { r#"
542 let foo = 999;
543 console.log(foo + 1999);
544 "#
545 },
546 "console.log(\"foo\")\n",
547 "console.log(\"another code block\")\n",
548 ]
549 );
550
551 // Python code block
552 let buffer = cx.new_model(|cx| {
553 let mut buffer = Buffer::local(
554 indoc! { r#"
555 Hey this is Markdown!
556
557 ```python
558 print("hello there")
559 print("hello there")
560 print("hello there")
561 ```
562 "# },
563 cx,
564 );
565 buffer.set_language_registry(language_registry.clone());
566 buffer.set_language(Some(markdown.clone()), cx);
567 buffer
568 });
569 let snapshot = buffer.read(cx).snapshot();
570
571 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
572 let snippets = snippets
573 .into_iter()
574 .map(|range| snapshot.text_for_range(range).collect::<String>())
575 .collect::<Vec<_>>();
576
577 assert_eq!(
578 snippets,
579 vec![indoc! { r#"
580 print("hello there")
581 print("hello there")
582 print("hello there")
583 "#
584 },]
585 );
586 }
587}