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