Skip to main content

yggdrasil/components/code_runner/runner/
body.rs

1//! 激活后才加载的分包:CodeMirror 编辑器、xterm 终端与执行(SSE / 轮询)逻辑。
2//!
3//! 外壳([`super::CodeRunner`],留在调用方所在的包)渲染静态代码占位、标题栏、容器与
4//! 输出区,并用 `use_near_viewport` 决定是否激活;激活(接近视口,或用户点击占位 /
5//! 「运行」)后才在 `<Suspense>` 里加载本分包。分包函数不渲染任何 DOM:它只在外壳已经
6//! 渲染好的容器上挂载编辑器与终端,并消费外壳经信号发来的命令(见 `execution`)。
7//! 所有 JS 句柄(编辑器、终端、SSE 流)都由分包持有,随分包所有者清理而销毁。
8
9use super::{execution::Execution, RunnerExample};
10use crate::bridges::library::LibraryResource;
11use crate::theme::ResolvedTheme;
12use leptos::prelude::*;
13
14/// 外壳传给分包的全部参数(信号与只读配置,均不含 JS 句柄)。
15#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
16#[derive(Clone)]
17pub(super) struct RunnerBody {
18    pub language: String,
19    pub editor_mount: NodeRef<leptos::html::Div>,
20    pub output_mount: NodeRef<leptos::html::Div>,
21    pub example_output: Option<RunnerExample>,
22    pub source_signal: RwSignal<String>,
23    pub vim_enabled: RwSignal<bool>,
24    pub vim_mode: RwSignal<Option<String>>,
25    pub run: Callback<()>,
26    pub activated: RwSignal<bool>,
27    pub editor_ready: RwSignal<bool>,
28    pub editor_failed: RwSignal<bool>,
29    pub terminal_failed: RwSignal<bool>,
30    pub mount_retry: RwSignal<u32>,
31    pub editor_command: RwSignal<Option<String>>,
32    pub editor_library: LibraryResource,
33    pub terminal_library: LibraryResource,
34    pub execution: Execution,
35    pub resolved_theme: Memo<ResolvedTheme>,
36}
37
38/// 分包入口:在已渲染的容器上挂载编辑器与终端并接管执行;自身不产生 DOM。
39///
40/// 首次调用时加载分包;服务端与未激活时从不调用。
41#[leptos::lazy]
42pub(super) fn code_runner_body(args: RunnerBody) -> AnyView {
43    #[cfg(target_arch = "wasm32")]
44    mount(args);
45    #[cfg(not(target_arch = "wasm32"))]
46    let _ = args;
47    ().into_any()
48}
49
50#[cfg(target_arch = "wasm32")]
51fn mount(args: RunnerBody) {
52    use super::state::RunState;
53    use super::stream::{self, StreamHandle, TerminalSlot};
54    use super::{editor, terminal};
55
56    let RunnerBody {
57        language,
58        editor_mount,
59        output_mount,
60        example_output,
61        source_signal,
62        vim_enabled,
63        vim_mode,
64        run,
65        activated,
66        editor_ready,
67        editor_failed,
68        terminal_failed,
69        mount_retry,
70        editor_command,
71        editor_library,
72        terminal_library,
73        execution,
74        resolved_theme,
75    } = args;
76
77    editor::use_editor(
78        language,
79        editor_mount,
80        source_signal,
81        vim_enabled,
82        vim_mode,
83        run,
84        editor_ready,
85        editor_library,
86        activated,
87        editor_failed,
88        mount_retry,
89        editor_command,
90        resolved_theme,
91    );
92
93    let term_handle: TerminalSlot = StoredValue::new_local(None);
94    let terminal_ready = RwSignal::new(false);
95    terminal::use_terminal(
96        output_mount,
97        example_output,
98        execution,
99        terminal_library,
100        term_handle,
101        terminal_ready,
102        terminal_failed,
103        mount_retry,
104        resolved_theme,
105    );
106
107    // SSE 流句柄:运行结束或组件卸载时丢弃(Drop 里关闭连接并摘掉监听)。
108    let stream_handle: StoredValue<Option<StreamHandle>, LocalStorage> =
109        StoredValue::new_local(None);
110    on_cleanup(move || {
111        let _ = stream_handle.try_set_value(None);
112    });
113    Effect::new(move |_| {
114        if !execution.state.with(RunState::is_running) {
115            let _ = stream_handle.try_set_value(None);
116        }
117    });
118
119    // 取走外壳提交的运行请求。终端挂载后才提交,保证输出一到就有地方写。
120    // 提交后保持「准备中」,排队与容器启动由流里的 stage 事件推进。
121    Effect::new(move |_| {
122        if !terminal_ready.get() {
123            return;
124        }
125        let Some(request) = execution.pending.get() else {
126            return;
127        };
128        execution.pending.set(None);
129        stream_handle.set_value(None);
130        leptos::task::spawn_local(async move {
131            match crate::api::code_runner::execute::start_exec_stream(request).await {
132                Ok(id) => match stream::start(&id, execution, term_handle) {
133                    // 组件已卸载时 try_set_value 把句柄交还,随即丢弃并关闭连接。
134                    Ok(handle) => drop(stream_handle.try_set_value(Some(handle))),
135                    Err(_) => stream::poll_result(&id, execution, term_handle).await,
136                },
137                Err(error) => {
138                    let _ = execution.state.try_set(RunState::rejected(&error));
139                }
140            }
141        });
142    });
143}