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