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::*, 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}