repl_editor.rs

  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::*, 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 WindowContext) -> 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 WindowContext) -> Option<Arc<Language>> {
315    editor
316        .update(cx, |editor, cx| {
317            let selection = editor.selections.newest::<usize>(cx);
318            let buffer = editor.buffer().read(cx).snapshot(cx);
319            buffer.language_at(selection.head()).cloned()
320        })
321        .ok()
322        .flatten()
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use gpui::{AppContext, Context};
329    use indoc::indoc;
330    use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
331
332    #[gpui::test]
333    fn test_snippet_ranges(cx: &mut AppContext) {
334        // Create a test language
335        let test_language = Arc::new(Language::new(
336            LanguageConfig {
337                name: "TestLang".into(),
338                line_comments: vec!["# ".into()],
339                ..Default::default()
340            },
341            None,
342        ));
343
344        let buffer = cx.new_model(|cx| {
345            Buffer::local(
346                indoc! { r#"
347                    print(1 + 1)
348                    print(2 + 2)
349
350                    print(4 + 4)
351
352
353                "# },
354                cx,
355            )
356            .with_language(test_language, cx)
357        });
358        let snapshot = buffer.read(cx).snapshot();
359
360        // Single-point selection
361        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
362        let snippets = snippets
363            .into_iter()
364            .map(|range| snapshot.text_for_range(range).collect::<String>())
365            .collect::<Vec<_>>();
366        assert_eq!(snippets, vec!["print(1 + 1)"]);
367
368        // Multi-line selection
369        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
370        let snippets = snippets
371            .into_iter()
372            .map(|range| snapshot.text_for_range(range).collect::<String>())
373            .collect::<Vec<_>>();
374        assert_eq!(
375            snippets,
376            vec![indoc! { r#"
377                print(1 + 1)
378                print(2 + 2)"# }]
379        );
380
381        // Trimming multiple trailing blank lines
382        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
383
384        let snippets = snippets
385            .into_iter()
386            .map(|range| snapshot.text_for_range(range).collect::<String>())
387            .collect::<Vec<_>>();
388        assert_eq!(
389            snippets,
390            vec![indoc! { r#"
391                print(1 + 1)
392                print(2 + 2)
393
394                print(4 + 4)"# }]
395        );
396    }
397
398    #[gpui::test]
399    fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
400        // Create a test language
401        let test_language = Arc::new(Language::new(
402            LanguageConfig {
403                name: "TestLang".into(),
404                line_comments: vec!["# ".into()],
405                ..Default::default()
406            },
407            None,
408        ));
409
410        let buffer = cx.new_model(|cx| {
411            Buffer::local(
412                indoc! { r#"
413                    # Hello!
414                    # %% [markdown]
415                    # This is some arithmetic
416                    print(1 + 1)
417                    print(2 + 2)
418
419                    # %%
420                    print(3 + 3)
421                    print(4 + 4)
422
423                    print(5 + 5)
424
425
426
427                "# },
428                cx,
429            )
430            .with_language(test_language, cx)
431        });
432        let snapshot = buffer.read(cx).snapshot();
433
434        // Jupytext snippet surrounding an empty selection
435        let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
436
437        let snippets = snippets
438            .into_iter()
439            .map(|range| snapshot.text_for_range(range).collect::<String>())
440            .collect::<Vec<_>>();
441        assert_eq!(
442            snippets,
443            vec![indoc! { r#"
444                # %% [markdown]
445                # This is some arithmetic
446                print(1 + 1)
447                print(2 + 2)"# }]
448        );
449
450        // Jupytext snippets intersecting a non-empty selection
451        let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
452        let snippets = snippets
453            .into_iter()
454            .map(|range| snapshot.text_for_range(range).collect::<String>())
455            .collect::<Vec<_>>();
456        assert_eq!(
457            snippets,
458            vec![
459                indoc! { r#"
460                    # %% [markdown]
461                    # This is some arithmetic
462                    print(1 + 1)
463                    print(2 + 2)"#
464                },
465                indoc! { r#"
466                    # %%
467                    print(3 + 3)
468                    print(4 + 4)
469
470                    print(5 + 5)"#
471                }
472            ]
473        );
474    }
475
476    #[gpui::test]
477    fn test_markdown_code_blocks(cx: &mut AppContext) {
478        let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
479        let typescript = languages::language(
480            "typescript",
481            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
482        );
483        let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
484        let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
485        language_registry.add(markdown.clone());
486        language_registry.add(typescript.clone());
487        language_registry.add(python.clone());
488
489        // Two code blocks intersecting with selection
490        let buffer = cx.new_model(|cx| {
491            let mut buffer = Buffer::local(
492                indoc! { r#"
493                    Hey this is Markdown!
494
495                    ```typescript
496                    let foo = 999;
497                    console.log(foo + 1999);
498                    ```
499
500                    ```typescript
501                    console.log("foo")
502                    ```
503                    "#
504                },
505                cx,
506            );
507            buffer.set_language_registry(language_registry.clone());
508            buffer.set_language(Some(markdown.clone()), cx);
509            buffer
510        });
511        let snapshot = buffer.read(cx).snapshot();
512
513        let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
514        let snippets = snippets
515            .into_iter()
516            .map(|range| snapshot.text_for_range(range).collect::<String>())
517            .collect::<Vec<_>>();
518
519        assert_eq!(
520            snippets,
521            vec![
522                indoc! { r#"
523                    let foo = 999;
524                    console.log(foo + 1999);
525                    "#
526                },
527                "console.log(\"foo\")\n"
528            ]
529        );
530
531        // Three code blocks intersecting with selection
532        let buffer = cx.new_model(|cx| {
533            let mut buffer = Buffer::local(
534                indoc! { r#"
535                    Hey this is Markdown!
536
537                    ```typescript
538                    let foo = 999;
539                    console.log(foo + 1999);
540                    ```
541
542                    ```ts
543                    console.log("foo")
544                    ```
545
546                    ```typescript
547                    console.log("another code block")
548                    ```
549                "# },
550                cx,
551            );
552            buffer.set_language_registry(language_registry.clone());
553            buffer.set_language(Some(markdown.clone()), cx);
554            buffer
555        });
556        let snapshot = buffer.read(cx).snapshot();
557
558        let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
559        let snippets = snippets
560            .into_iter()
561            .map(|range| snapshot.text_for_range(range).collect::<String>())
562            .collect::<Vec<_>>();
563
564        assert_eq!(
565            snippets,
566            vec![
567                indoc! { r#"
568                    let foo = 999;
569                    console.log(foo + 1999);
570                    "#
571                },
572                "console.log(\"foo\")\n",
573                "console.log(\"another code block\")\n",
574            ]
575        );
576
577        // Python code block
578        let buffer = cx.new_model(|cx| {
579            let mut buffer = Buffer::local(
580                indoc! { r#"
581                    Hey this is Markdown!
582
583                    ```python
584                    print("hello there")
585                    print("hello there")
586                    print("hello there")
587                    ```
588                "# },
589                cx,
590            );
591            buffer.set_language_registry(language_registry.clone());
592            buffer.set_language(Some(markdown.clone()), cx);
593            buffer
594        });
595        let snapshot = buffer.read(cx).snapshot();
596
597        let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
598        let snippets = snippets
599            .into_iter()
600            .map(|range| snapshot.text_for_range(range).collect::<String>())
601            .collect::<Vec<_>>();
602
603        assert_eq!(
604            snippets,
605            vec![indoc! { r#"
606                print("hello there")
607                print("hello there")
608                print("hello there")
609                "#
610            },]
611        );
612    }
613}