Files
endorphin-tessera-gui/src/shell/developer_tools/sources.rs
T

162 lines
6.5 KiB
Rust

//! 绘制 AST、当前源码快照和云文件视图。
//! 当前快照是发布源的只读投影;云文件 provider 的可用性与打开状态分别反馈,不把查看代码当成执行代码。
//!
use super::*;
/// 根据云文件/当前快照模式构建左右两栏,展示文件或 AST 选择,并生成只读源码行和跳转动作。
pub(super) fn build(
p: &mut Painter,
state: &mut DeveloperToolsState,
doc: &RenderDocument,
viewport: Rect,
) {
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);
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));
let code_x = x + tree + 2.0;
p.label(x + 10.0, 69.0, "CLOUD FILES", MUTED, false);
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::<Vec<_>>();
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);
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);
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,
);
let mut tree_lines = Vec::new();
if let Some(root) = &doc.root {
let mut flat = Vec::new();
// 将发布 AST 展平成带深度的行;行动作保存节点稳定 ID 与起始行,用于选择并跳到对应代码。
flatten(root, 0, &mut flat);
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));
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::<Vec<_>>();
p.lines(
Rect::new(code_x, 92.0, width - code_x, bottom - 92.0),
// 源码区与 AST 树各自保存滚动位置,选择节点并不把树文本误当作源码内容。
Region::Source,
&code_lines,
state,
false,
);
}
}