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