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