repl_editor.rs

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