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 display_snapshot = editor.display_snapshot(cx);
481            let selection = editor.selections.newest::<usize>(&display_snapshot);
482            display_snapshot
483                .buffer_snapshot()
484                .language_at(selection.head())
485                .cloned()
486        })
487        .ok()
488        .flatten()
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use gpui::App;
495    use indoc::indoc;
496    use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
497
498    #[gpui::test]
499    fn test_snippet_ranges(cx: &mut App) {
500        // Create a test language
501        let test_language = Arc::new(Language::new(
502            LanguageConfig {
503                name: "TestLang".into(),
504                line_comments: vec!["# ".into()],
505                ..Default::default()
506            },
507            None,
508        ));
509
510        let buffer = cx.new(|cx| {
511            Buffer::local(
512                indoc! { r#"
513                    print(1 + 1)
514                    print(2 + 2)
515
516                    print(4 + 4)
517
518
519                "# },
520                cx,
521            )
522            .with_language(test_language, cx)
523        });
524        let snapshot = buffer.read(cx).snapshot();
525
526        // Single-point selection
527        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
528        let snippets = snippets
529            .into_iter()
530            .map(|range| snapshot.text_for_range(range).collect::<String>())
531            .collect::<Vec<_>>();
532        assert_eq!(snippets, vec!["print(1 + 1)"]);
533
534        // Multi-line selection
535        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
536        let snippets = snippets
537            .into_iter()
538            .map(|range| snapshot.text_for_range(range).collect::<String>())
539            .collect::<Vec<_>>();
540        assert_eq!(
541            snippets,
542            vec![indoc! { r#"
543                print(1 + 1)
544                print(2 + 2)"# }]
545        );
546
547        // Trimming multiple trailing blank lines
548        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
549
550        let snippets = snippets
551            .into_iter()
552            .map(|range| snapshot.text_for_range(range).collect::<String>())
553            .collect::<Vec<_>>();
554        assert_eq!(
555            snippets,
556            vec![indoc! { r#"
557                print(1 + 1)
558                print(2 + 2)
559
560                print(4 + 4)"# }]
561        );
562    }
563
564    #[gpui::test]
565    fn test_jupytext_snippet_ranges(cx: &mut App) {
566        // Create a test language
567        let test_language = Arc::new(Language::new(
568            LanguageConfig {
569                name: "TestLang".into(),
570                line_comments: vec!["# ".into()],
571                ..Default::default()
572            },
573            None,
574        ));
575
576        let buffer = cx.new(|cx| {
577            Buffer::local(
578                indoc! { r#"
579                    # Hello!
580                    # %% [markdown]
581                    # This is some arithmetic
582                    print(1 + 1)
583                    print(2 + 2)
584
585                    # %%
586                    print(3 + 3)
587                    print(4 + 4)
588
589                    print(5 + 5)
590
591
592
593                "# },
594                cx,
595            )
596            .with_language(test_language, cx)
597        });
598        let snapshot = buffer.read(cx).snapshot();
599
600        // Jupytext snippet surrounding an empty selection
601        let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
602
603        let snippets = snippets
604            .into_iter()
605            .map(|range| snapshot.text_for_range(range).collect::<String>())
606            .collect::<Vec<_>>();
607        assert_eq!(
608            snippets,
609            vec![indoc! { r#"
610                # %% [markdown]
611                # This is some arithmetic
612                print(1 + 1)
613                print(2 + 2)"# }]
614        );
615
616        // Jupytext snippets intersecting a non-empty selection
617        let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
618        let snippets = snippets
619            .into_iter()
620            .map(|range| snapshot.text_for_range(range).collect::<String>())
621            .collect::<Vec<_>>();
622        assert_eq!(
623            snippets,
624            vec![
625                indoc! { r#"
626                    # %% [markdown]
627                    # This is some arithmetic
628                    print(1 + 1)
629                    print(2 + 2)"#
630                },
631                indoc! { r#"
632                    # %%
633                    print(3 + 3)
634                    print(4 + 4)
635
636                    print(5 + 5)"#
637                }
638            ]
639        );
640    }
641
642    #[gpui::test]
643    fn test_markdown_code_blocks(cx: &mut App) {
644        use crate::kernels::LocalKernelSpecification;
645        use jupyter_protocol::JupyterKernelspec;
646
647        // Initialize settings
648        settings::init(cx);
649        editor::init(cx);
650
651        // Initialize the ReplStore with a fake filesystem
652        let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
653        ReplStore::init(fs, cx);
654
655        // Add mock kernel specifications for TypeScript and Python
656        let store = ReplStore::global(cx);
657        store.update(cx, |store, cx| {
658            let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
659                name: "typescript".into(),
660                kernelspec: JupyterKernelspec {
661                    argv: vec![],
662                    display_name: "TypeScript".into(),
663                    language: "typescript".into(),
664                    interrupt_mode: None,
665                    metadata: None,
666                    env: None,
667                },
668                path: std::path::PathBuf::new(),
669            });
670
671            let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
672                name: "python".into(),
673                kernelspec: JupyterKernelspec {
674                    argv: vec![],
675                    display_name: "Python".into(),
676                    language: "python".into(),
677                    interrupt_mode: None,
678                    metadata: None,
679                    env: None,
680                },
681                path: std::path::PathBuf::new(),
682            });
683
684            store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
685        });
686
687        let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
688        let typescript = languages::language(
689            "typescript",
690            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
691        );
692        let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
693        let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
694        language_registry.add(markdown.clone());
695        language_registry.add(typescript);
696        language_registry.add(python);
697
698        // Two code blocks intersecting with selection
699        let buffer = cx.new(|cx| {
700            let mut buffer = Buffer::local(
701                indoc! { r#"
702                    Hey this is Markdown!
703
704                    ```typescript
705                    let foo = 999;
706                    console.log(foo + 1999);
707                    ```
708
709                    ```typescript
710                    console.log("foo")
711                    ```
712                    "#
713                },
714                cx,
715            );
716            buffer.set_language_registry(language_registry.clone());
717            buffer.set_language(Some(markdown.clone()), cx);
718            buffer
719        });
720        let snapshot = buffer.read(cx).snapshot();
721
722        let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
723        let snippets = snippets
724            .into_iter()
725            .map(|range| snapshot.text_for_range(range).collect::<String>())
726            .collect::<Vec<_>>();
727
728        assert_eq!(
729            snippets,
730            vec![
731                indoc! { r#"
732                    let foo = 999;
733                    console.log(foo + 1999);
734                    "#
735                },
736                "console.log(\"foo\")\n"
737            ]
738        );
739
740        // Three code blocks intersecting with selection
741        let buffer = cx.new(|cx| {
742            let mut buffer = Buffer::local(
743                indoc! { r#"
744                    Hey this is Markdown!
745
746                    ```typescript
747                    let foo = 999;
748                    console.log(foo + 1999);
749                    ```
750
751                    ```ts
752                    console.log("foo")
753                    ```
754
755                    ```typescript
756                    console.log("another code block")
757                    ```
758                "# },
759                cx,
760            );
761            buffer.set_language_registry(language_registry.clone());
762            buffer.set_language(Some(markdown.clone()), cx);
763            buffer
764        });
765        let snapshot = buffer.read(cx).snapshot();
766
767        let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
768        let snippets = snippets
769            .into_iter()
770            .map(|range| snapshot.text_for_range(range).collect::<String>())
771            .collect::<Vec<_>>();
772
773        assert_eq!(
774            snippets,
775            vec![
776                indoc! { r#"
777                    let foo = 999;
778                    console.log(foo + 1999);
779                    "#
780                },
781                "console.log(\"foo\")\n",
782                "console.log(\"another code block\")\n",
783            ]
784        );
785
786        // Python code block
787        let buffer = cx.new(|cx| {
788            let mut buffer = Buffer::local(
789                indoc! { r#"
790                    Hey this is Markdown!
791
792                    ```python
793                    print("hello there")
794                    print("hello there")
795                    print("hello there")
796                    ```
797                "# },
798                cx,
799            );
800            buffer.set_language_registry(language_registry.clone());
801            buffer.set_language(Some(markdown.clone()), cx);
802            buffer
803        });
804        let snapshot = buffer.read(cx).snapshot();
805
806        let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
807        let snippets = snippets
808            .into_iter()
809            .map(|range| snapshot.text_for_range(range).collect::<String>())
810            .collect::<Vec<_>>();
811
812        assert_eq!(
813            snippets,
814            vec![indoc! { r#"
815                print("hello there")
816                print("hello there")
817                print("hello there")
818                "#
819            },]
820        );
821    }
822}