//! 绘制 AST、当前源码快照和云文件视图。 //! 当前快照是发布源的只读投影;云文件 provider 的可用性与打开状态分别反馈,不把查看代码当成执行代码。 //! use super::*; /// 根据云文件/当前快照模式构建左右两栏,展示文件或 AST 选择,并生成只读源码行和跳转动作。 // developer_tools::build 在 Sources 标签选中时调用,左侧生成可激活动作,右侧只读展示文本并供 SourceSelection 复制。 pub(super) fn build( p: &mut Painter, state: &mut DeveloperToolsState, doc: &RenderDocument, viewport: Rect, ) { // 横向原点/宽度来自工具画布,AST 或文件树与源码面板都使用这个局部坐标系。 let x = viewport.x; let w = viewport.width; // width 在本函数表示右边界坐标,w 才是视口宽度;用于计算右侧源码区域和按钮位置。 let width = viewport.x + viewport.width; let bottom = viewport.height - 22.0; p.fill(Rect::new(x, 32.0, w, 30.0), BAR); // 两个切换按钮仅发出 Snapshot/Cloud 动作,改变当前展示模式,不替换文档或执行文件。 p.button( Rect::new(x + 3.0, 32.0, 139.0, 29.0), "Current snapshot", Action::Snapshot, !state.cloud, ); p.button( Rect::new(x + 145.0, 32.0, 107.0, 29.0), "Cloud files", Action::Cloud, state.cloud, ); // 两种数据源分开投影:云模式用 provider 的 files/opened,快照模式只读已发布文档 source/root。 if state.cloud { p.button( Rect::new(width - 92.0, 32.0, 88.0, 29.0), "Refresh", Action::CloudRefresh, false, ); // 树宽至少 120,通常为右侧内容保留 180;窄窗口仍保持树最小宽,交由绘制裁剪处理。 let tree = state.tree_width.clamp(120.0, (w - 180.0).max(120.0)); // 树右边界后预留分隔线间隙,再作为源码区左边界;width 是工具右边界坐标而非可用宽度。 let code_x = x + tree + 2.0; p.label(x + 10.0, 69.0, "CLOUD FILES", MUTED, false); // 列表来自最近成功 provider 刷新,每行只携带显示路径和不透明 ID;这里不会同步调用外部服务。 let file_lines = state .cloud_sources .files .iter() .map(|file| { let mut l = Line::new(&file.path, TEXT); // 动作传递 provider 的不透明文件 ID,显示路径不参与本地磁盘读取或代码执行。 l.action = Some(Action::CloudOpen(file.id.clone())); l }) .collect::>(); p.lines( Rect::new(x + 1.0, 92.0, tree - 1.0, bottom - 92.0), Region::Tree, &file_lines, state, false, ); // 登记树/源码的竖向分隔线,真实覆盖绘制和命中由父入口最后完成。 p.split(Rect::new(x + tree, 63.0, 1.0, bottom - 63.0), Split::Tree); // 云文本与 provider 错误组合成本帧行列表,旧打开内容可与新错误同时展示。 let mut source_lines = Vec::new(); // 成功打开的云文本逐行加显示行号;这里不替换 doc.source,也不触发程序热重载。 if let Some(source) = &state.cloud_sources.opened { p.label(code_x + 10.0, 69.0, &source.path, MUTED, false); // 显示加一基行号和两空格前缀,复制时 Painter 按约定剥除前七字符;原始 code 不被改写。 for (i, line) in source.code.split('\n').enumerate() { source_lines.push(Line::new(format!("{:>5} {}", i + 1, line), TEXT)); } } else { multiline( &mut source_lines, "No cloud file open\n\nNo provider connected.\nCloud files will open here as full source.\nCurrent snapshot contains your last successful script.", MUTED, ); } // provider 错误附在当前源码下方;即使读取失败仍可展示上次成功打开的内容。 // 错误作为追加行显示,不清掉文件列表或已打开文本;重绘不会重试失败请求。 if !state.cloud_sources.error.is_empty() { source_lines.push(Line::new(&state.cloud_sources.error, ERROR)); } p.lines( Rect::new(code_x, 92.0, width - code_x, bottom - 92.0), Region::Source, &source_lines, state, false, ); } else { // 快照模式沿用同一个树宽状态,切换模式不另设隐藏的分隔条宽度。 let tree = state.tree_width.clamp(120.0, (w - 180.0).max(120.0)); let code_x = x + tree + 2.0; p.label(x + 10.0, 69.0, "ASTER AST", MUTED, false); p.label( code_x + 10.0, 69.0, &format!( "{} · {} lines · read only", if doc .root .as_ref() // REPL 文档展示为交互记录,普通脚本展示为 app.main.endor;仅改变标题,不合成另一份源码。 .is_some_and(|root| root.kind == "repl-session") { "Console transcript" } else { "app.main.endor" }, doc.source.split('\n').count() ), MUTED, false, ); // AST 行与源码行分开收集,Tree/Source 两个 Region 各自维护滚动和宽度缓存。 let mut tree_lines = Vec::new(); // 只展开已发布文档中的 CodeNode 树,不从当前 Console 输入临时解析一棵新 AST。 if let Some(root) = &doc.root { // flatten 保存深度及节点借用,不复制原树;显示文本和跳行动作在后面的循环构造。 let mut flat = Vec::new(); // 将发布 AST 展平成带深度的行;行动作保存节点稳定 ID 与起始行,用于选择并跳到对应代码。 flatten(root, 0, &mut flat); // 深度只影响缩进,选中身份用稳定节点 ID,起始行号另给 Action::Node 用于源码定位。 for (depth, n) in flat { let mut l = Line::new(format!("{}{} {}", " ".repeat(depth), n.kind, n.name), TEXT); l.selected = state.selected_node_id.as_deref() == Some(&n.id); l.action = Some(Action::Node(n.id.clone(), n.start_line)); tree_lines.push(l); } } else { tree_lines.push(Line::new("No submitted source", MUTED)); } p.lines( Rect::new(x + 1.0, 92.0, tree - 1.0, bottom - 92.0), Region::Tree, &tree_lines, state, false, ); p.split(Rect::new(x + tree, 63.0, 1.0, bottom - 63.0), Split::Tree); // 每次重绘按当前文档查节点,旧选择 ID 若已不存在则不高亮,避免沿用旧 AST 行范围。 let selected = state .selected_node_id .as_deref() .and_then(|id| doc.node(id)); // 展示副本统一换行并展开 Tab,所以选择复制的是工具显示文本;原 doc.source 保持不变。 let code_lines = doc .source // 仅显示投影统一 CRLF 并把 Tab 展开为四空格;原始文档文本保持不变。 .replace("\r\n", "\n") .split('\n') .enumerate() .map(|(i, s)| { let mut l = Line::new(format!("{:>5} {}", i + 1, s.replace('\t', " ")), TEXT); // i 是零基行号,AST 行号是一基;条件等价于 start_line <= i+1 <= end_line。 l.selected = selected.is_some_and(|n| i + 1 >= n.start_line && i < n.end_line); l }) .collect::>(); p.lines( Rect::new(code_x, 92.0, width - code_x, bottom - 92.0), // 源码区与 AST 树各自保存滚动位置,选择节点并不把树文本误当作源码内容。 Region::Source, &code_lines, state, false, ); } }