Compare commits
11
Commits
4480f936f3
...
8d70ee1659
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d70ee1659 | ||
|
|
04c4b9f710 | ||
|
|
e6345e3309 | ||
|
|
e8ae19d084 | ||
|
|
e322f01edb | ||
|
|
ff071f132f | ||
|
|
13e71507d6 | ||
|
|
340c81084a | ||
|
|
69608f9c70 | ||
|
|
44de0a1f70 | ||
|
|
6d58b681cf |
No files matched your search
+660
@@ -0,0 +1,660 @@
|
||||
// 示例是手写的短 Rust,不是仓库里的文件。名字照桌面端和接口服务:AgentRow、start_server。
|
||||
// 库只覆盖 serde、thiserror、axum、sqlx、tauri。tokio 不单独成卡,异步写在函数上。
|
||||
// 颜色取自代码阅读原型里的 VS Code Dark Modern。
|
||||
|
||||
const C = {
|
||||
editor: "#1f1f1f",
|
||||
side: "#181818",
|
||||
border: "#2b2b2b",
|
||||
borderStrong: "#3c3c3c",
|
||||
fg: "#cccccc",
|
||||
fgStrong: "#e8e8e8",
|
||||
desc: "#9d9d9d",
|
||||
dim: "#6e7681",
|
||||
activityIdle: "#868686",
|
||||
activityOn: "#d7d7d7",
|
||||
focus: "#0078d4",
|
||||
selection: "#04395e",
|
||||
input: "#313131",
|
||||
field: "#111111",
|
||||
toggleOn: "#37373d",
|
||||
comment: "#6a9955",
|
||||
control: "#c586c0",
|
||||
storage: "#569cd6",
|
||||
variable: "#9cdcfe",
|
||||
fn: "#dcdcaa",
|
||||
type: "#4ec9b0",
|
||||
string: "#ce9178",
|
||||
folder: "#8b9ba8",
|
||||
rust: "#dea584",
|
||||
toml: "#c0754a",
|
||||
added: "#2ea043",
|
||||
get: "#61affe",
|
||||
};
|
||||
|
||||
const KIND = {
|
||||
use: { label: "导入", color: "#c586c0", icon: "import" },
|
||||
struct: { label: "结构体", color: "#4ec9b0", icon: "braces" },
|
||||
enum: { label: "枚举", color: "#b180d7", icon: "list-ordered" },
|
||||
impl: { label: "实现", color: "#dcdcaa", icon: "square-function" },
|
||||
fn: { label: "函数", color: "#dcdcaa", icon: "square-function" },
|
||||
trait: { label: "特质", color: "#569cd6", icon: "boxes" },
|
||||
error: { label: "错误", color: "#f14c4c", icon: "triangle-alert" },
|
||||
route: { label: "接口", color: "#61afef", icon: "globe" },
|
||||
query: { label: "查询", color: "#ce9178", icon: "database" },
|
||||
command: { label: "命令", color: "#c0754a", icon: "app-window" },
|
||||
};
|
||||
|
||||
const TREE = [
|
||||
{ kind: "folder", name: "agents", open: true, depth: 0 },
|
||||
{ kind: "folder", name: "src", open: true, depth: 1 },
|
||||
{ kind: "file", name: "agent.rs", depth: 2 },
|
||||
{ kind: "file", name: "error.rs", depth: 2 },
|
||||
{ kind: "file", name: "routes.rs", depth: 2 },
|
||||
{ kind: "file", name: "store.rs", depth: 2 },
|
||||
{ kind: "file", name: "Cargo.toml", depth: 1, ext: "TM" },
|
||||
];
|
||||
|
||||
function solid(color) {
|
||||
return [{ type: "solid", color }];
|
||||
}
|
||||
|
||||
function text(parent, content, o) {
|
||||
const s = String(content);
|
||||
const opt = o || {};
|
||||
const mono = Boolean(opt.mono) && !/[^\x00-\x7F]/.test(s);
|
||||
const node = {
|
||||
type: "text",
|
||||
name: opt.name || s.slice(0, 32),
|
||||
content: s,
|
||||
fontFamily: mono ? "Menlo" : "PingFang SC",
|
||||
fontSize: opt.size || 13,
|
||||
fontWeight: opt.weight || 400,
|
||||
lineHeight: opt.lh || 1.45,
|
||||
fill: solid(opt.color || C.fg),
|
||||
height: "fit_content",
|
||||
};
|
||||
if (opt.wrap) {
|
||||
node.width = opt.width || "fill_container";
|
||||
node.textGrowth = "fixed-width";
|
||||
} else {
|
||||
node.width = "fit_content";
|
||||
node.textGrowth = "auto";
|
||||
}
|
||||
return I(parent, node);
|
||||
}
|
||||
|
||||
function icon(parent, name, color, size) {
|
||||
return I(parent, {
|
||||
type: "icon_font",
|
||||
name: name,
|
||||
iconFontName: name,
|
||||
width: size,
|
||||
height: size,
|
||||
fill: solid(color),
|
||||
});
|
||||
}
|
||||
|
||||
function row(parent, name, o) {
|
||||
const opt = o || {};
|
||||
return I(parent, {
|
||||
type: "frame",
|
||||
name,
|
||||
layout: "horizontal",
|
||||
width: opt.width || "fill_container",
|
||||
height: opt.height || "fit_content",
|
||||
gap: opt.gap == null ? 8 : opt.gap,
|
||||
padding: opt.padding,
|
||||
alignItems: opt.align || "center",
|
||||
justifyContent: opt.justify,
|
||||
fill: opt.fill ? solid(opt.fill) : undefined,
|
||||
cornerRadius: opt.radius,
|
||||
});
|
||||
}
|
||||
|
||||
function col(parent, name, o) {
|
||||
const opt = o || {};
|
||||
return I(parent, {
|
||||
type: "frame",
|
||||
name,
|
||||
layout: "vertical",
|
||||
width: opt.width || "fill_container",
|
||||
height: "fit_content",
|
||||
gap: opt.gap == null ? 4 : opt.gap,
|
||||
padding: opt.padding,
|
||||
alignItems: opt.align,
|
||||
fill: opt.fill ? solid(opt.fill) : undefined,
|
||||
cornerRadius: opt.radius,
|
||||
stroke: opt.stroke ? { thickness: 1, fill: solid(opt.stroke) } : undefined,
|
||||
clipContent: false,
|
||||
});
|
||||
}
|
||||
|
||||
function prop(parent, label, value, color, mono) {
|
||||
const line = row(parent, "Prop / " + label, { gap: 12, align: "start" });
|
||||
const key = I(line, { type: "frame", name: "Key", width: 72, height: "fit_content", layout: "vertical" });
|
||||
text(key, label, { size: 12, color: C.desc, wrap: true });
|
||||
const val = I(line, { type: "frame", name: "Value", width: "fill_container", height: "fit_content", layout: "vertical" });
|
||||
if (typeof value === "function") value(val);
|
||||
else text(val, value, { size: 13, color: color || C.fg, mono: mono !== false, wrap: true });
|
||||
return line;
|
||||
}
|
||||
|
||||
function tag(parent, label, color) {
|
||||
const pill = I(parent, {
|
||||
type: "frame",
|
||||
name: "Tag / " + label,
|
||||
layout: "horizontal",
|
||||
width: "fit_content",
|
||||
height: "fit_content",
|
||||
padding: [1, 6],
|
||||
cornerRadius: 4,
|
||||
alignItems: "center",
|
||||
fill: solid("#2a2a2a"),
|
||||
stroke: { thickness: 1, fill: solid(color) },
|
||||
});
|
||||
text(pill, label, { size: 11, color });
|
||||
return pill;
|
||||
}
|
||||
|
||||
function nameLine(parent, name, color, extra) {
|
||||
const line = row(parent, "Prop / 名称", { gap: 12 });
|
||||
const key = I(line, { type: "frame", name: "Key", width: 72, height: "fit_content" });
|
||||
text(key, "名称", { size: 12, color: C.desc });
|
||||
text(line, name, { size: 13, color: color || C.fn, mono: true, weight: 600 });
|
||||
if (extra) extra(line);
|
||||
}
|
||||
|
||||
function table(parent, columns, rows) {
|
||||
const wrap = col(parent, "Table wrap", { gap: 0, padding: [2, 0, 0, 4] });
|
||||
const grid = I(wrap, {
|
||||
type: "frame",
|
||||
name: "Table",
|
||||
layout: "vertical",
|
||||
width: "fill_container",
|
||||
height: "fit_content",
|
||||
cornerRadius: 4,
|
||||
stroke: { thickness: 1, fill: solid(C.borderStrong) },
|
||||
clipContent: true,
|
||||
gap: 0,
|
||||
});
|
||||
function draw(cells, header) {
|
||||
const line = I(grid, {
|
||||
type: "frame",
|
||||
name: header ? "Header" : "Row",
|
||||
layout: "horizontal",
|
||||
width: "fill_container",
|
||||
height: "fit_content",
|
||||
alignItems: "stretch",
|
||||
fill: header ? solid(C.side) : undefined,
|
||||
});
|
||||
cells.forEach((cell, index) => {
|
||||
const box = I(line, {
|
||||
type: "frame",
|
||||
name: columns[index].name,
|
||||
layout: "vertical",
|
||||
width: columns[index].width,
|
||||
height: "fit_content",
|
||||
padding: [5, 8],
|
||||
justifyContent: "center",
|
||||
});
|
||||
if (typeof cell === "function") cell(box);
|
||||
else text(box, cell, {
|
||||
size: 12,
|
||||
color: header ? C.desc : (columns[index].color || C.fg),
|
||||
mono: !header && columns[index].mono !== false,
|
||||
wrap: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
draw(columns.map((column) => column.name), true);
|
||||
for (const cells of rows) draw(cells, false);
|
||||
}
|
||||
|
||||
function tools(parent) {
|
||||
const bar = row(parent, "Tools", { width: "fit_content", gap: 2 });
|
||||
for (const name of ["sparkles", "pencil"]) {
|
||||
const button = I(bar, {
|
||||
type: "frame",
|
||||
name: name,
|
||||
layout: "horizontal",
|
||||
width: 22,
|
||||
height: 22,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cornerRadius: 4,
|
||||
});
|
||||
icon(button, name, C.desc, 14);
|
||||
}
|
||||
}
|
||||
|
||||
function card(parent, kind, draw) {
|
||||
const node = I(parent, {
|
||||
type: "frame",
|
||||
name: kind.label,
|
||||
layout: "horizontal",
|
||||
width: "fill_container",
|
||||
height: "fit_content",
|
||||
gap: 10,
|
||||
padding: [8, 10, 8, 8],
|
||||
cornerRadius: 8,
|
||||
alignItems: "start",
|
||||
fill: solid("#242424"),
|
||||
stroke: { thickness: 1, fill: solid(kind.color) },
|
||||
clipContent: false,
|
||||
});
|
||||
const fold = I(node, {
|
||||
type: "frame",
|
||||
name: "Fold",
|
||||
layout: "horizontal",
|
||||
width: 14,
|
||||
height: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
||||
icon(fold, "chevron-down", C.desc, 14);
|
||||
icon(node, kind.icon, kind.color, 18);
|
||||
const content = col(node, "Content", { gap: 4 });
|
||||
draw(content);
|
||||
tools(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function activity(parent) {
|
||||
const bar = I(parent, {
|
||||
type: "frame",
|
||||
name: "Activity bar",
|
||||
layout: "vertical",
|
||||
width: 48,
|
||||
height: "fit_content",
|
||||
justifyContent: "space_between",
|
||||
fill: solid(C.side),
|
||||
});
|
||||
const top = col(bar, "Top", { width: 48, gap: 0 });
|
||||
const icons = ["files", "search", "git-branch", "play", "monitor", "blocks", "rocket", "book-open-text"];
|
||||
for (const name of icons) {
|
||||
const on = name === "book-open-text";
|
||||
const cell = I(top, {
|
||||
type: "frame",
|
||||
name: "Item / " + name,
|
||||
layout: "horizontal",
|
||||
width: 48,
|
||||
height: 42,
|
||||
alignItems: "center",
|
||||
justifyContent: on ? "start" : "center",
|
||||
gap: on ? 10 : 0,
|
||||
});
|
||||
if (on) I(cell, { type: "rectangle", name: "Active bar", width: 2, height: 42, fill: solid(C.focus) });
|
||||
icon(cell, name, on ? C.activityOn : C.activityIdle, 22);
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
|
||||
function sidebar(parent, selected) {
|
||||
const side = col(parent, "Sidebar", { width: 280, gap: 0, fill: C.side });
|
||||
const header = row(side, "Header", { height: 34, padding: [0, 12, 0, 16], justify: "space_between" });
|
||||
text(header, "代码阅读", { size: 11, color: C.fg });
|
||||
const actions = row(header, "Actions", { width: "fit_content", gap: 10 });
|
||||
for (const name of ["git-pull-request", "file-plus", "refresh-cw"]) icon(actions, name, C.fg, 14);
|
||||
const list = col(side, "Tree", { gap: 0, padding: [4, 0, 8, 0] });
|
||||
for (const item of TREE) {
|
||||
const line = I(list, {
|
||||
type: "frame",
|
||||
name: item.name,
|
||||
layout: "horizontal",
|
||||
width: "fill_container",
|
||||
height: 26,
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: [0, 8, 0, 10 + item.depth * 14],
|
||||
fill: item.name === selected ? solid(C.selection) : undefined,
|
||||
});
|
||||
if (item.kind === "folder") icon(line, item.open ? "chevron-down" : "chevron-right", C.fg, 14);
|
||||
else I(line, { type: "frame", name: "Twisty", width: 14, height: 14 });
|
||||
if (item.kind === "folder") icon(line, "folder", C.folder, 15);
|
||||
else text(line, item.ext || "RS", { size: 9, weight: 700, color: item.ext === "TM" ? C.toml : C.rust });
|
||||
text(line, item.name, { size: 13, color: item.name === selected ? C.fgStrong : C.fg });
|
||||
}
|
||||
return side;
|
||||
}
|
||||
|
||||
function editorChrome(parent, file) {
|
||||
const bar = row(parent, "Editor title", { height: 36, justify: "space_between", fill: C.side, gap: 0 });
|
||||
const tab = I(bar, {
|
||||
type: "frame",
|
||||
name: "Tab",
|
||||
layout: "vertical",
|
||||
width: 148,
|
||||
height: "fill_container",
|
||||
fill: solid(C.editor),
|
||||
gap: 0,
|
||||
});
|
||||
I(tab, { type: "rectangle", name: "Active", width: "fill_container", height: 2, fill: solid(C.focus) });
|
||||
const label = row(tab, "Tab label", { height: "fill_container", padding: [0, 12], gap: 6 });
|
||||
text(label, "RS", { size: 9, weight: 700, color: C.rust });
|
||||
text(label, file, { size: 13, color: C.fg });
|
||||
const mode = I(bar, {
|
||||
type: "frame",
|
||||
name: "Mode",
|
||||
layout: "horizontal",
|
||||
width: "fit_content",
|
||||
height: "fit_content",
|
||||
padding: [2, 2],
|
||||
gap: 2,
|
||||
cornerRadius: 5,
|
||||
alignItems: "center",
|
||||
fill: solid(C.field),
|
||||
stroke: { thickness: 1, fill: solid(C.borderStrong) },
|
||||
});
|
||||
const edit = row(mode, "Seg / 编辑", { width: "fit_content", padding: [2, 8], gap: 4, radius: 3 });
|
||||
icon(edit, "pencil", C.desc, 13);
|
||||
text(edit, "编辑", { size: 12, color: C.desc });
|
||||
const preview = row(mode, "Seg / 预览", { width: "fit_content", padding: [2, 8], gap: 4, radius: 3, fill: C.toggleOn });
|
||||
icon(preview, "eye", C.fgStrong, 13);
|
||||
text(preview, "预览", { size: 12, color: C.fgStrong });
|
||||
I(bar, { type: "frame", name: "Pad", width: 8, height: 8 });
|
||||
}
|
||||
|
||||
function crumb(parent, file) {
|
||||
const line = row(parent, "Breadcrumb", { padding: [6, 16], gap: 6 });
|
||||
const parts = ["agents", "src", file];
|
||||
parts.forEach((part, index) => {
|
||||
if (index) text(line, "/", { size: 12, color: C.dim });
|
||||
text(line, part, { size: 12, color: index === parts.length - 1 ? C.fg : C.desc, mono: part.endsWith(".rs") });
|
||||
});
|
||||
}
|
||||
|
||||
function filters(parent, chips, count) {
|
||||
const bar = row(parent, "Filter", { height: 36, padding: [0, 16], gap: 8 });
|
||||
text(bar, "筛选", { size: 12, color: C.desc });
|
||||
for (const chip of chips) {
|
||||
const item = row(bar, "Chip / " + chip, { width: "fit_content", height: 24, padding: [0, 8], gap: 4, radius: 4, fill: C.input });
|
||||
text(item, chip, { size: 12, color: C.fg });
|
||||
icon(item, "chevron-down", C.desc, 12);
|
||||
}
|
||||
const spacer = I(bar, { type: "frame", name: "Spacer", width: "fill_container", height: 8 });
|
||||
text(bar, count + " 块", { size: 12, color: C.desc });
|
||||
return spacer;
|
||||
}
|
||||
|
||||
function status(parent) {
|
||||
const bar = row(parent, "Status", {
|
||||
height: 26,
|
||||
padding: [0, 12],
|
||||
justify: "space_between",
|
||||
fill: C.side,
|
||||
});
|
||||
const left = row(bar, "Left", { width: "fit_content", gap: 12 });
|
||||
const branch = row(left, "Branch", { width: "fit_content", gap: 4 });
|
||||
icon(branch, "git-branch", C.fg, 13);
|
||||
text(branch, "master", { size: 12 });
|
||||
const right = row(bar, "Right", { width: "fit_content", gap: 16 });
|
||||
text(right, "代码阅读", { size: 12 });
|
||||
text(right, "Rust", { size: 12 });
|
||||
}
|
||||
|
||||
function windowOf(name, x, file, chips, count, drawCards) {
|
||||
const win = I(null, {
|
||||
type: "frame",
|
||||
name,
|
||||
x,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: "fit_content",
|
||||
layout: "vertical",
|
||||
gap: 0,
|
||||
fill: solid(C.side),
|
||||
clipContent: false,
|
||||
});
|
||||
const title = row(win, "Title bar", { height: 36, padding: [0, 12], justify: "space_between" });
|
||||
const lights = row(title, "Lights", { width: "fit_content", gap: 8 });
|
||||
for (const color of ["#ff5f57", "#febc2e", "#28c840"]) {
|
||||
I(lights, { type: "ellipse", width: 12, height: 12, fill: solid(color) });
|
||||
}
|
||||
text(title, "agents — " + file, { size: 12, color: C.desc });
|
||||
I(title, { type: "frame", name: "Balance", width: 52, height: 12 });
|
||||
|
||||
const body = I(win, {
|
||||
type: "frame",
|
||||
name: "Body",
|
||||
layout: "horizontal",
|
||||
width: "fill_container",
|
||||
height: "fit_content",
|
||||
gap: 0,
|
||||
alignItems: "start",
|
||||
fill: solid(C.side),
|
||||
clipContent: false,
|
||||
});
|
||||
activity(body);
|
||||
sidebar(body, file);
|
||||
const editor = col(body, "Editor", { gap: 0, fill: C.editor });
|
||||
editorChrome(editor, file);
|
||||
crumb(editor, file);
|
||||
filters(editor, chips, count);
|
||||
const preview = col(editor, "Preview", { gap: 10, padding: [8, 20, 24, 20] });
|
||||
drawCards(preview);
|
||||
status(win);
|
||||
return win;
|
||||
}
|
||||
|
||||
function useCard(parent, source, names) {
|
||||
card(parent, KIND.use, (content) => {
|
||||
prop(content, "来源", source, C.string, true);
|
||||
prop(content, "导入项", names.length + " 个", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "名字", width: 220, color: C.variable }, { name: "别名", width: 220, color: C.dim }],
|
||||
names.map((item) => [item.name, item.alias || "—"]));
|
||||
});
|
||||
}
|
||||
|
||||
function languageCards(preview) {
|
||||
useCard(preview, "serde", [{ name: "Serialize" }]);
|
||||
useCard(preview, "std::sync", [{ name: "Arc" }]);
|
||||
|
||||
card(preview, KIND.struct, (content) => {
|
||||
nameLine(content, "AgentRow", C.type);
|
||||
prop(content, "说明", "桌面端列表里的一行代理。", C.comment, false);
|
||||
prop(content, "可见性", "公开", C.control, false);
|
||||
prop(content, "字段", "2 个", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "字段", width: 180, color: C.variable }, { name: "类型", width: 220, color: C.type }, { name: "可见性", width: 160 }],
|
||||
[
|
||||
["id", "Uuid", { text: "公开", color: C.control }],
|
||||
["name", "String", { text: "本模块", color: C.desc }],
|
||||
].map(([field, type, vis]) => [field, type, (box) => text(box, vis.text, { size: 12, color: vis.color })]));
|
||||
});
|
||||
|
||||
card(preview, KIND.enum, (content) => {
|
||||
nameLine(content, "AgentState", C.type);
|
||||
prop(content, "说明", "这个代理现在处在哪一步。", C.comment, false);
|
||||
prop(content, "变体", "3 个", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "变体", width: 220, color: C.type }, { name: "携带的数据", width: 360 }],
|
||||
[
|
||||
["Idle", "—"],
|
||||
["Running", "{ pid: u32 }"],
|
||||
["Failed", "String"],
|
||||
]);
|
||||
});
|
||||
|
||||
card(preview, KIND.impl, (content) => {
|
||||
nameLine(content, "AgentRow 的方法", C.fn);
|
||||
prop(content, "方法", "1 个", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "名称", width: 180, color: C.fn }, { name: "参数", width: 320 }, { name: "返回", width: 140, color: C.dim }],
|
||||
[["rename", "&mut self, name: String", "()"]]);
|
||||
const nested = col(content, "展开 / rename", { gap: 0, padding: [4, 0, 0, 8] });
|
||||
card(nested, KIND.fn, (body) => {
|
||||
nameLine(body, "rename", C.fn);
|
||||
prop(body, "参数", "2 个", C.desc, false);
|
||||
table(body,
|
||||
[{ name: "名称", width: 180, color: C.variable }, { name: "类型", width: 280 }],
|
||||
[["self", "&mut self"], ["name", "String"]]);
|
||||
});
|
||||
});
|
||||
|
||||
card(preview, KIND.fn, (content) => {
|
||||
nameLine(content, "start_server", C.fn);
|
||||
prop(content, "说明", "在本机拉起接口服务。", C.comment, false);
|
||||
prop(content, "异步", "是", C.storage, false);
|
||||
prop(content, "可见性", "公开", C.control, false);
|
||||
prop(content, "输入参数", "1 个", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "名称", width: 180, color: C.variable }, { name: "类型", width: 280, color: C.type }],
|
||||
[["addr", "SocketAddr"]]);
|
||||
prop(content, "返回", "Result<(), StartError>", C.type, true);
|
||||
prop(content, "分情况", "2 支", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "分支", width: 220, color: C.type }, { name: "做什么", width: 420, mono: false }],
|
||||
[
|
||||
["Ok(listener)", "记下监听地址"],
|
||||
["Err(err)", "返回 StartError"],
|
||||
]);
|
||||
});
|
||||
|
||||
card(preview, KIND.trait, (content) => {
|
||||
nameLine(content, "Store", C.storage);
|
||||
prop(content, "说明", "按 id 读写代理。", C.comment, false);
|
||||
prop(content, "方法", "2 个", C.desc, false);
|
||||
table(content,
|
||||
[{ name: "名称", width: 140, color: C.fn }, { name: "参数", width: 280 }, { name: "返回", width: 280, color: C.type }],
|
||||
[
|
||||
["get", "&self, id: Uuid", "Result<AgentRow, StoreError>"],
|
||||
["save", "&mut self, row: AgentRow", "Result<(), StoreError>"],
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function libraryCards(preview) {
|
||||
card(preview, KIND.struct, (content) => {
|
||||
nameLine(content, "AgentRow", C.type, (line) => tag(line, "可序列化", C.string));
|
||||
prop(content, "说明", "桌面端列表里的一行代理。", C.comment, false);
|
||||
prop(content, "可见性", "公开", C.control, false);
|
||||
prop(content, "选项", "字段名改驼峰", C.string, false);
|
||||
prop(content, "字段", "3 个", C.desc, false);
|
||||
table(content,
|
||||
[
|
||||
{ name: "字段", width: 140, color: C.variable },
|
||||
{ name: "类型", width: 160, color: C.type },
|
||||
{ name: "可见性", width: 120 },
|
||||
{ name: "序列化", width: 220 },
|
||||
],
|
||||
[
|
||||
["id", "Uuid", (box) => text(box, "公开", { size: 12, color: C.control }), (box) => text(box, "改名 agent_id", { size: 12, color: C.string, wrap: true })],
|
||||
["name", "String", (box) => text(box, "公开", { size: 12, color: C.control }), "—"],
|
||||
["secret", "String", (box) => text(box, "本模块", { size: 12, color: C.desc }), (box) => text(box, "不输出", { size: 12, color: C.desc })],
|
||||
]);
|
||||
});
|
||||
|
||||
card(preview, KIND.error, (content) => {
|
||||
nameLine(content, "AgentError", "#f14c4c");
|
||||
prop(content, "说明", "读写代理时出的错。", C.comment, false);
|
||||
prop(content, "变体", "2 个", C.desc, false);
|
||||
table(content,
|
||||
[
|
||||
{ name: "变体", width: 150, color: C.type },
|
||||
{ name: "何时", width: 200, mono: false },
|
||||
{ name: "展示", width: 280 },
|
||||
],
|
||||
[
|
||||
["NotFound", "这个 id 没有代理", (box) => text(box, "agent {id} 不存在", { size: 12, color: C.string, wrap: true })],
|
||||
["Upstream", "调用上游失败", (box) => {
|
||||
const line = row(box, "展示", { gap: 6 });
|
||||
text(line, "上游错误", { size: 12, color: C.string });
|
||||
tag(line, "透传来源", C.control);
|
||||
}],
|
||||
]);
|
||||
});
|
||||
|
||||
card(preview, KIND.route, (content) => {
|
||||
nameLine(content, "get_agent", C.fn);
|
||||
prop(content, "说明", "按 id 取一个代理。", C.comment, false);
|
||||
prop(content, "异步", "是", C.storage, false);
|
||||
prop(content, "请求", "GET /agents/{id}", C.desc, true);
|
||||
table(content,
|
||||
[{ name: "项", width: 140 }, { name: "值", width: 420 }],
|
||||
[
|
||||
[(box) => text(box, "方法", { size: 12, color: C.desc }), (box) => text(box, "GET", { size: 12, color: C.get, weight: 600, mono: true })],
|
||||
[(box) => text(box, "路径", { size: 12, color: C.desc }), (box) => text(box, "/agents/{id}", { size: 12, color: C.string, mono: true })],
|
||||
[(box) => text(box, "路径参数", { size: 12, color: C.desc }), (box) => text(box, "id: Uuid", { size: 12, color: C.variable, mono: true })],
|
||||
]);
|
||||
prop(content, "返回", "Result<Json<AgentRow>, StatusCode>", C.type, true);
|
||||
});
|
||||
|
||||
card(preview, KIND.query, (content) => {
|
||||
prop(content, "说明", "按 id 取一行。", C.comment, false);
|
||||
prop(content, "SQL", "SELECT id, name FROM agents WHERE id = $1", C.fg, true);
|
||||
prop(content, "参数", "$1 = id", C.variable, true);
|
||||
prop(content, "返回类型", "AgentRow", C.type, true);
|
||||
});
|
||||
|
||||
card(preview, KIND.command, (content) => {
|
||||
nameLine(content, "rename_agent", C.fn, (line) => tag(line, "桌面命令", C.toml));
|
||||
prop(content, "说明", "让窗口把这一行改名。", C.comment, false);
|
||||
prop(content, "异步", "是", C.storage, false);
|
||||
prop(content, "参数", "2 个", C.desc, false);
|
||||
table(content,
|
||||
[
|
||||
{ name: "名称", width: 140, color: C.variable },
|
||||
{ name: "类型", width: 200, color: C.type },
|
||||
{ name: "来源", width: 200, mono: false },
|
||||
],
|
||||
[
|
||||
["state", "State<App>", "窗口状态"],
|
||||
["name", "String", "调用方传入"],
|
||||
]);
|
||||
prop(content, "返回", "Result<String, String>", C.type, true);
|
||||
});
|
||||
}
|
||||
|
||||
function note(parent, label, color, sentence) {
|
||||
const line = row(parent, "Note / " + label, { gap: 10, align: "start" });
|
||||
if (label) tag(line, label, color || C.desc);
|
||||
const body = I(line, { type: "frame", name: "Sentence", width: "fill_container", height: "fit_content", layout: "vertical" });
|
||||
text(body, sentence, { size: 13, color: C.fg, wrap: true });
|
||||
}
|
||||
|
||||
function legend(x) {
|
||||
const board = I(null, {
|
||||
type: "frame",
|
||||
name: "Prototype / Rust · 标记说明",
|
||||
x,
|
||||
y: 0,
|
||||
width: 880,
|
||||
height: "fit_content",
|
||||
layout: "vertical",
|
||||
gap: 14,
|
||||
padding: 28,
|
||||
fill: solid(C.editor),
|
||||
clipContent: false,
|
||||
});
|
||||
text(board, "标记说明", { size: 18, weight: 600, color: C.fgStrong });
|
||||
|
||||
text(board, "可见性", { size: 14, weight: 600, color: C.fgStrong });
|
||||
note(board, "公开", C.control, "pub,别的模块也能用。");
|
||||
note(board, "本模块", C.desc, "没写 pub,只有这个文件里能用。");
|
||||
note(board, "父模块", C.string, "pub(super),上一层模块能用。");
|
||||
|
||||
text(board, "语言卡片", { size: 14, weight: 600, color: C.fgStrong });
|
||||
note(board, "导入", KIND.use.color, "一条 use。来源是路径,下面表是名字和别名。");
|
||||
note(board, "结构体", KIND.struct.color, "字段表:字段、类型、可见性。没有的属性不占一行。");
|
||||
note(board, "枚举", KIND.enum.color, "变体表。没带数据的格子是破折号,不硬凑类型。");
|
||||
note(board, "实现", KIND.impl.color, "标题写成「某某的方法」。点方法名展开一张函数子卡片。");
|
||||
note(board, "函数", KIND.fn.color, "异步写「是」。函数体里的 match 收成「分情况」,不展开源码。");
|
||||
note(board, "特质", KIND.trait.color, "方法表只列签名,不展开函数体。");
|
||||
|
||||
text(board, "库", { size: 14, weight: 600, color: C.fgStrong });
|
||||
note(board, "改名", C.string, "serde rename:序列化那一列写成「改名 agent_id」。");
|
||||
note(board, "不输出", C.desc, "serde skip:这一列写成「不输出」。没标的是破折号。");
|
||||
note(board, "驼峰", C.string, "rename_all = camelCase 收成选项行「字段名改驼峰」。");
|
||||
note(board, "展示", KIND.error.color, "thiserror 的文案写在「展示」列。透传的上游错误另标「透传来源」。");
|
||||
note(board, "接口", KIND.route.color, "axum 处理函数和 FastAPI 接口是同一张卡:请求表加返回。");
|
||||
note(board, "查询", KIND.query.color, "sqlx 一张小卡:一句说明、等宽 SQL、参数、返回类型。");
|
||||
note(board, "桌面命令", KIND.command.color, "tauri 命令在名称后标「桌面命令」。参数写明来自窗口状态还是调用方。");
|
||||
note(board, "异步", C.storage, "tokio 不单独做卡。spawn 和 select 仍是普通调用。");
|
||||
return board;
|
||||
}
|
||||
|
||||
windowOf("Prototype / Rust · 语言", 0, "agent.rs", ["种类", "可见性"], 7, languageCards);
|
||||
windowOf("Prototype / Rust · 库", 1360, "routes.rs", ["种类", "库"], 5, libraryCards);
|
||||
legend(2720);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,7 @@
|
||||
| 流程 | 源码 →(tree-sitter)语法树 → 通用代码块树 → 译文 |
|
||||
| 第一版翻译 | 只做中文模板直译;AI 解读以后作为另一个翻译插件加 |
|
||||
| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python、Shell 脚本(bash / sh / zsh)、Dockerfile |
|
||||
| Rust | `.rs`(VS Code 的 rust):读成导入、结构体、枚举、实现、函数、特质;serde、thiserror、axum、sqlx、tauri 各一张库卡片。tokio 不单独做。见「Rust」 |
|
||||
| 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 `activate()` 导出的 API 注册;内置实现也走同一个 API |
|
||||
| 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
|
||||
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 |
|
||||
@@ -146,9 +147,10 @@
|
||||
| command 命令 | `square-terminal` | pipeline 管道 | `square-terminal` | | |
|
||||
| directive 解析指令 | `terminal` | stage 阶段 | `layers` | run 执行 | `square-terminal` |
|
||||
| copy 复制 | `copy` | setting 设置 | `sliders-horizontal` | label 标签 | `tag` |
|
||||
| health 健康检查 | `activity` | start 启动 | `play` | | |
|
||||
| health 健康检查 | `activity` | start 启动 | `play` | trait 特质 | `network` |
|
||||
| impl 实现 | `square-function` | module 模块 | `layers` | | |
|
||||
|
||||
库规则的种类用规则给的图标(`KindBadge.icon`),跟在语言的种类图标后面:SQLAlchemy 数据表 `database`、数据类 `package`、Pydantic 数据模型 `braces`(和类型块同一个图标,颜色不同)、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`、React 组件 `atom`、副作用 `zap`、事件处理 `mouse-pointer-click`。数据类写了 `frozen=True` 再加一把锁(`lock`,悬停「不可修改」)。
|
||||
库规则的种类用规则给的图标(`KindBadge.icon`),跟在语言的种类图标后面:SQLAlchemy 数据表 `database`、数据类 `package`、Pydantic 数据模型 `braces`(和类型块同一个图标,颜色不同)、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`、React 组件 `atom`、副作用 `zap`、事件处理 `mouse-pointer-click`、serde 可序列化 `braces`、thiserror 错误 `triangle-alert`、axum 接口 `globe`、sqlx 查询 `database`、tauri 桌面命令 `square-function`。数据类写了 `frozen=True` 再加一把锁(`lock`,悬停「不可修改」)。实现块再由规则 `rust.impl` 换成卡片,左边因此有两个「实现」图标。
|
||||
|
||||
这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置共用一个,字段、标签共用一个——加库规则用的 9 个(React 组件的原子、副作用的闪电、事件处理的点击手势也在里面),lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,概念的 `icon` 只能写其中一个,写别的是类型错误。内置种类 → 图标名在 `contract.ts` 的 `KIND_ICONS`(宿主写进概念列表)。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。
|
||||
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。先是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
|
||||
@@ -572,6 +574,7 @@
|
||||
```ts
|
||||
export type BlockKind =
|
||||
| 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
|
||||
| 'trait' | 'impl' | 'module' // Rust:特质、实现、模块(属性表)
|
||||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||||
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' // shell:解释器、选项(属性表),分情况、命令、管道(一行译文)
|
||||
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start' // Dockerfile:解析指令、阶段、健康检查、启动(属性表),执行、复制、设置、标签(一行译文)
|
||||
@@ -1119,7 +1122,7 @@ export interface CommandRule {
|
||||
|
||||
### 启用、认领、优先
|
||||
|
||||
- 启用:宿主收集这份源码里各层导入块的 `ImportFacts.sources`(去掉引号),有一个等于规则的某个库、或以「库.」「库/」开头(子模块),规则才对这份源码启用。规则不用自己读导入语句。`libraries` 是空数组的规则不看导入,`languageIds` 里这几种语言的源码都启用(React:新的 JSX 写法不用导入 react。Pydantic:子类文件常常只导入父模型、不再导入 pydantic,见「内置:Pydantic 数据模型」)。
|
||||
- 启用:宿主收集这份源码里各层导入块的 `ImportFacts.sources`(去掉引号),有一个等于规则的某个库、或以「库.」「库/」「库::」开头(子模块;Rust 的路径用 `::`,`use axum::routing::get` 的来源是 `axum::routing`),规则才对这份源码启用。规则不用自己读导入语句。`libraries` 是空数组的规则不看导入,`languageIds` 里这几种语言的源码都启用(React:新的 JSX 写法不用导入 react。Pydantic:子类文件常常只导入父模型、不再导入 pydantic,见「内置:Pydantic 数据模型」)。
|
||||
- 认领:宿主从顶层往下每块问一遍启用的规则,按注册顺序,第一个认领的算(先注册的优先);被认领的块留下的子块照样问。
|
||||
- 出错:规则抛错、或替身不合约定(种类 id 没带规则 id 前缀、并进来的不是这块的子块、属性表里的引用标的不是自己 / 规则没给 `resolve` / 引用的段没带下标、放进卡片里的块不是留下的子块 / 放了几处 / 没有那一行或子行、要略去的块还有子块)时,宿主记一条警告日志(输出面板「Superpowers」),当它没认、接着问下一个规则;都没认这块照常显示。预览不会因为规则出错而失败。
|
||||
- 并进来的子块:不再单独成块,由规则挑(`absorbs`),宿主不替规则多并。没并进来的子块(方法、规则不讲的注释)不管写在哪(字段中间也算),都照源码顺序是替身的子块,预览里照常看得见。替身的编辑范围同别的块,是原块整条语句(从头部开始,到原块连同各子块的最后一行,类就是整个类),盖住留下的子块的行:替身编辑时预览不画这些子块,同一行不会同时在两处能改(见「预览(代码块树)」)。内置规则并成员时连同紧挨在它前面的注释块一起并(讲的是这个成员),讲方法的注释、最后面的注释照常留下。
|
||||
@@ -1554,7 +1557,7 @@ export interface CommandRule {
|
||||
| --- | --- |
|
||||
| 名称 | 组件名(组件色,带下标);后面跟小标签(`tag` 框):「具名导出」「默认导出」(同 import / export 关键字的颜色;`export default X`、`export { X }` 写在文件别处也算)、「客户端组件」(组件色)、「记忆化」「转发 ref」(同 const 关键字的颜色,memo / forwardRef 包着) |
|
||||
| 说明 | 组件上面的 JSDoc,Markdown |
|
||||
| 属性 | `N 个`(hint);属性类型写的是一个名字(接口、类型别名、导入的类型)时后面写「 · 来自 XxxProps」,名字带下标(⌘ + 点击跳到类型)。下面一张表格「名称 \| 类型 \| 默认值 \| 必填」,有属性带说明时多一列「说明」。没写参数时写「无」 |
|
||||
| 构造参数 | `N 个`(hint);属性类型写的是一个名字(接口、类型别名、导入的类型)时后面写「 · 来自 XxxProps」,名字带下标(⌘ + 点击跳到类型)。下面一张表格「名称 \| 类型 \| 默认值 \| 必填」,有属性带说明时多一列「说明」。没写参数时写「无」 |
|
||||
| 状态 | useState / useReducer:「名称 \| 初始值 \| 改它的」 |
|
||||
| 引用 | useRef:「名称 \| 初始值 \| 用在」 |
|
||||
| 读取的数据 | 别的钩子(`useXxx(…)` 赋给变量):「变量 \| 来自 \| 意思」 |
|
||||
@@ -1563,7 +1566,7 @@ export interface CommandRule {
|
||||
| 事件处理 | 组件体里定义的函数(函数声明、值是函数的常量):「名称 \| 绑定在 \| 做什么」,点名称在表下面展开它的子卡片 |
|
||||
| 渲染 | `N 处返回`(hint),下面是返回的 JSX 画成的结构大纲 |
|
||||
|
||||
属性表:属性类型的字段照类型里的顺序一行一个(类型字面量、同文件的接口 / 类型别名,交叉类型各部分接起来,`Readonly<…>`、`PropsWithChildren<…>` 看里面;没标注参数类型时 `const X: FC<Props>` 的泛型实参、`forwardRef<R, Props>` 的第二个泛型实参),解构里有、类型里没有的接在后面,`...rest` 最后(类型写「其余的属性」)。名称是属性名(定义处带下标:解构出来的是解构那处,没解构的是字段;跳到定义只闪这一行),`children` 后面标「子元素」、函数类型的属性(`(x) => R`、方法签名)标「回调」(组件色的小标签),解构时改了名写「(组件里叫 x)」;类型照翻译插件翻,方法签名写成函数签名;默认值取解构的默认值,没有写「—」;必填看类型的 `?`(是 / 否,storage 色),没类型可看时写了默认值的「否」、别的「—」;说明是字段上面的 JSDoc / 注释、这一行行尾的注释(comment 色,几行接成一行)。类型是导入的名字时经工作区索引查它的字段(索引 `react.propTypes`:看工作区里所有 `**/*.{ts,tsx}`,预筛有 `interface X` / `type X` 才解析,按 typescriptreact 解析;从文件顶层认出接口、类型别名,键是名字、值是各字段的名字、类型写法、可省略、回调、说明的 JSON),查法同「规则认领时查别的文件里的定义」:先照解构的出行、类型「未标注」,查到了按字段重排、重发;别的文件里的类型照写文字(type 色)。
|
||||
构造参数:属性类型的字段照类型里的顺序一行一个(类型字面量、同文件的接口 / 类型别名,交叉类型各部分接起来,`Readonly<…>`、`PropsWithChildren<…>` 看里面;没标注参数类型时 `const X: FC<Props>` 的泛型实参、`forwardRef<R, Props>` 的第二个泛型实参),解构里有、类型里没有的接在后面,`...rest` 最后(类型写「其余的属性」)。名称是属性名(定义处带下标:解构出来的是解构那处,没解构的是字段;跳到定义只闪这一行),`children` 后面标「子元素」、函数类型的属性(`(x) => R`、方法签名)标「回调」(组件色的小标签),解构时改了名写「(组件里叫 x)」;类型照翻译插件翻,方法签名写成函数签名;默认值取解构的默认值,没有写「—」;必填看类型的 `?`(是 / 否,storage 色),没类型可看时写了默认值的「否」、别的「—」;说明是字段上面的 JSDoc / 注释、这一行行尾的注释(comment 色,几行接成一行)。类型是导入的名字时经工作区索引查它的字段(索引 `react.propTypes`:看工作区里所有 `**/*.{ts,tsx}`,预筛有 `interface X` / `type X` 才解析,按 typescriptreact 解析;从文件顶层认出接口、类型别名,键是名字、值是各字段的名字、类型写法、可省略、回调、说明的 JSON),查法同「规则认领时查别的文件里的定义」:先照解构的出行、类型「未标注」,查到了按字段重排、重发;别的文件里的类型照写文字(type 色)。
|
||||
|
||||
状态、引用、读取的数据、派生值:
|
||||
|
||||
@@ -1586,14 +1589,14 @@ export interface CommandRule {
|
||||
- `cond && <X/>` 前面加「当 … 时」小框(`condition`,条件照常翻);三元一行「如果 …」,下面第一行各带「是」「否」小框(`branch`),一支是 null 时写「不渲染」;`items.map((x) => …)` 一行「对 items 的每一项 x」(`iteration` 小框)后面「key …」,回调里先算的常量写「其中 isActive = …」,再下面是每一项渲染的;JSX 注释写成一行「// …」(comment 色)。
|
||||
- 几处返回:分几段,提前 `return null`(`if (cond) return null`,主体只有这一句)写「当 … 时」(`skip` 小框)「不渲染」,提前返回 JSX 的「当 … 时」下面是它渲染的,最后一处「否则」(`branch` 小框);只有一处返回时直接是它的大纲。这些 return(和提前返回的 if)并进卡片。
|
||||
|
||||
组件体里归不进上面分区的语句(没讲进分区的注释、别的 if、循环、单独调用的钩子)照常是组件卡片留下的子块。卡片「编辑」是组件的完整代码(整条语句);副作用、处理函数子卡片各自能编辑、问 AI。代码差异在视图数据这一层比:组件卡片的属性表逐行(属性表按名称配对,加的属性标新加的一行),副作用、处理函数子卡片是块,照常配对、逐行标(改了依赖,「时机」那一行逐截标)。
|
||||
组件体里归不进上面分区的语句(没讲进分区的注释、别的 if、循环、单独调用的钩子)照常是组件卡片留下的子块。卡片「编辑」是组件的完整代码(整条语句);副作用、处理函数子卡片各自能编辑、问 AI。状态「改它的」里每一处、读取的数据的「来自」各自有同一套铅笔,打开后只出现那一段源码(整行切好、着色好,和块同一套编辑框、同一条改文件的消息):来自是这次钩子调用占的行,不是左边的变量名;改它的里 JSX 是整个属性(名字和值),处理函数、派生值、另一处读取、重置条件各开各的那段(函数、那个值、那次调用、那个条件),纯渲染没有铅笔。这段的行落在组件里面,组件正在编辑时不画这段,两边不能同时编辑。代码差异在视图数据这一层比:组件卡片的属性表逐行(属性表按名称配对,加的属性标新加的一行),副作用、处理函数子卡片是块,照常配对、逐行标(改了依赖,「时机」那一行逐截标)。
|
||||
|
||||
样例(`fixtures/desktopTitlebar.tsx.txt`,测试里逐字核对;`⟦…⟧` 是小标签、小框):
|
||||
|
||||
```
|
||||
名称: DesktopTitlebar ⟦具名导出⟧ ⟦客户端组件⟧
|
||||
说明: 窗口顶部 40px. 整条连同里面的文字都是拖拽区 (data-tauri-drag-region="deep"), 按钮除外. 侧栏开关只在侧栏顶部.
|
||||
属性: 1 个
|
||||
构造参数: 1 个
|
||||
platform | DesktopShell["platform"] | — | 是
|
||||
读取的数据: 2 个
|
||||
pathname | usePathname() | 当前路径
|
||||
@@ -1611,6 +1614,16 @@ export interface CommandRule {
|
||||
|
||||
和原型不一样的地方:原型里「做什么」「意思」、派生值的读法(「title;没有就按路径查 PAGE_TITLES」)是照设计手写的中文,实现里是注释原文、照翻译插件翻的译文和词表;条件照翻译插件读(`===` 是「全等」,原型写「是」);渲染大纲带下一层的行有折叠箭头;副作用子卡片收起时不换成一行摘要,「做什么」那一行的箭头收起它下面的语句。
|
||||
|
||||
### 内置:Rust 的库
|
||||
|
||||
文件导入了对应 crate 才启用(来源等于 crate 名,或以 `crate::` 开头)。tokio 不单独做卡片,异步已经写在函数上。样例 `fixtures/rustLibs.rs`。
|
||||
|
||||
- serde(`rules/serde.ts`,`libraries: ['serde']`):`derive` 了 `Serialize` 或 `Deserialize` 的结构体。种类 `serde.struct`,标签「可序列化」,图标 `braces`,颜色同结构体(暗 #4ec9b0 / 亮 #267f99)。名称后面再标一个「可序列化」。`rename_all = "camelCase"` 收成选项「字段名改驼峰」。字段表多一列「序列化」:`rename` 写「改名 xxx」,`skip` / `skip_serializing` 写「不输出」,没标是「—」。
|
||||
- thiserror(`rules/thiserror.ts`,`libraries: ['thiserror']`):`derive` 了 `Error` 的枚举。种类 `thiserror.error`,标签「错误」,图标 `triangle-alert`,颜色同异常(暗 #f14c4c / 亮 #cd3131)。变体表「变体 | 何时 | 展示」:何时是变体上面的文档注释,展示是 `#[error("…")]` 的文案。变体上的 `#[from]` 或 `#[source]` 在展示后面标「透传来源」。
|
||||
- axum(`rules/axum.ts`,`libraries: ['axum']`):处理函数上的 `#[get("/path")]`、`#[axum::post(…)]` 等(get / post / put / delete / patch / head / options / trace),或顶层语句里 `Router` 的 `.route(路径, get(函数))`。函数已经标了路由属性时,这句 `route` 不再单独成卡。种类 `axum.route`,标签「接口」,图标 `globe`,颜色同 FastAPI 接口(暗 #61afef / 亮 #1a6fb5)。属性表:名称、说明、异步(是)、请求(一项「方法」、一项「路径」、路径里的 `{名字}` 对上参数写成「路径参数」,`Path(id): Path<Uuid>` 写 `id: Uuid`)、返回。
|
||||
- sqlx(`rules/sqlx.ts`,`libraries: ['sqlx']`):`query!` / `query_as!` / `query_scalar!`。种类 `sqlx.query`,标签「查询」,图标 `database`,颜色暗 #ce9178 / 亮 #a31515。紧挨在前面的注释收成「说明」,不再单独成块。SQL 原文等宽,参数 `$1` 对上传入的表达式,`query_as!` / `query_scalar!` 的第一个实参是返回类型,`query!` 的返回类型是「—」。
|
||||
- tauri(`rules/tauri.ts`,`libraries: ['tauri']`):`#[tauri::command]` 的函数。种类 `tauri.command`,标签「桌面命令」,图标 `square-function`,颜色暗 #e5c07b / 亮 #9a6a00。名称后面再标「桌面命令」。参数表「名称 | 类型 | 来源」:`State<…>` 是「窗口状态」,其余是「调用方传入」。
|
||||
|
||||
## 命令规则
|
||||
|
||||
shell 脚本大半是一条条命令,一条命令读成什么取决于命令本身(`rm -rf` 是「删除(连同里面的东西、不提示)」,`tar -xzf` 是「解压 tar.gz」)。这些既不是语法(语言插件不管),也不是某种语言的措辞(翻译插件不该认识几十个命令)。命令规则是第五种插件:按命令名注册,把一条命令连同它的选项读成一句译文,像库规则按库认写法一样。
|
||||
@@ -1724,6 +1737,23 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令
|
||||
- `run_instruction` → run(整条指令,shell 写法的带 `embedded`);`copy_instruction`、`add_instruction` → copy;`label_instruction` → label;`env_instruction`、`arg_instruction` → variable(facts 的 scope 分构建参数、环境变量);WORKDIR / USER / EXPOSE / VOLUME / STOPSIGNAL / SHELL / MAINTAINER → setting;`healthcheck_instruction` → health;`cmd_instruction`、`entrypoint_instruction` → start;`onbuild_instruction` → setting,包着的指令照这些规则是它的子块;别的(语法错误)→ statement。
|
||||
- 连着写的同种指令(紧挨着下一行)并成一块:ENV、ARG 并成一个变量块(declarators 是各条的全部),LABEL 和从同一个地方复制的 COPY 并成一块(`TextBlock.group`,头部盖住全部);ADD、带 heredoc 的 COPY 不并。
|
||||
|
||||
### Rust
|
||||
|
||||
语法包是 `@vscode/tree-sitter-wasm` 里的 `tree-sitter-rust.wasm`(和 python 的 wasm 同一层:包入口就在 `wasm/`)。`languageIds: ['rust']`。样例 `fixtures/rustLang.rs`、`fixtures/rustLibs.rs`。原型是 `docs/prototype/code-reader/openpencil/exports/` 的 `rust-lang.png`、`rust-lib.png`、`rust-notes.png`。中文直译 `translators/rustZh.ts`:结构体不叫「类」,可见性不叫「访问级别」,`pub` 是「公开」。
|
||||
|
||||
可见性:`pub` 公开,没写是本模块,`pub(super)` 父模块,`pub(crate)` 和 `pub(in …)` 本 crate。公开写在 `facts.access`,其余写在块的 `access` 属性上(筛选值 `module` / `super` / `crate`)。说明是紧挨在前面的文档注释(`///`、`/**`)的第一句,句号留着。
|
||||
|
||||
- `use` → import。来源是路径去掉最后一节(`std::sync::Arc` 的来源是 `std::sync`)。导入项表「名字 | 别名」,没起别名是「—」,`*` 是「*」。
|
||||
- `struct` → class(中文直译把这个种类叫「结构体」)。字段不单独成块,表「字段 | 类型 | 可见性」。元组结构体没有这张表。
|
||||
- `enum` → enum。变体表「变体 | 携带的数据」:没带数据是「—」,`Failed(String)` 写类型,`Running { pid: u32 }` 写字段原文。
|
||||
- `fn` → function。有才显示:说明、异步(只有 `async fn` 写「是」)、可见性、输入参数表「名称 | 类型」、返回(没写是 `()`)。函数体里直接写的 `match` 收成「分情况」表「分支 | 做什么」,N 支:优先用这一支里的注释,否则是 `return` 那句或这一支的第一句,不展开成子块。
|
||||
- `trait` → trait(图标 `network`,颜色同声明关键字的蓝)。方法表「名称 | 参数 | 返回」,只列签名,不展开函数体。
|
||||
- `impl` → impl(图标 `square-function`,颜色同函数)。标题是「某类型 的方法」(`impl Trait for Type` 取 Type)。方法是子块。不看导入的规则 `rules/rustImpl.ts`(`libraries: []`,种类 `rust.impl`)把每张方法卡放进方法表那一行,点方法名展开,和 React 事件处理同一条路。方法卡的参数表叫「参数」。身体里的 `const`、`static` 不收成语句,跟文件顶层同一条路(见下)。
|
||||
- `mod 名字 { … }`(`mod_item` 有 `body`)→ module(中文「模块」,图标 `layers`,颜色同阶段)。卡片行:名称、说明(有文档注释才有)、可见性(没写也是本模块,跟结构体一样总有这一行)。身体用和文件顶层同一套规则拆成子块,排在这张卡片下面,不另做一张表把子块塞进某一行。模块自己的文档注释留在模块卡片上,身体里每项自己的文档注释留在那一项上。`mod 名字;` 没有身体,不拆,仍是一条 statement。
|
||||
- `const`(`const_item`)→ const,`static`(`static_item`)→ variable。属性表不另写一套译文,走中文模板里常量 / 变量那一套:名称、说明、访问级别(公开才有,模板里叫「公共」)、类型、值(`static` 的值叫「初始值」)。文件顶层、模块身体、impl 身体同一条路。
|
||||
- `extern "…" { … }`(`foreign_mod_item`)不单独成一块源码。里面的函数签名(`function_signature_item`,没有函数体)是函数卡片,跟普通函数一样有名称、说明、可见性、参数、返回,作为所在这一层(模块、文件顶层或 impl)的子块。
|
||||
- 其余(`let`、宏调用)→ statement。不拆函数体。`//` 注释单独成块。模块身体里宏调用后面多出来的那个空 `;`(`empty_statement`)不成块。
|
||||
|
||||
### 属性表信息(facts)从哪来
|
||||
|
||||
- TypeScript 函数:`function_declaration` / `generator_function_declaration` / `function_signature`、类里的 `method_definition` / 方法签名、值是带语句块主体函数的变量和类字段、`export default` 的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自 `formal_parameters`(`required_parameter` / `optional_parameter` 的 pattern、type、value,JavaScript 的 `assignment_pattern`),不带括号的单参数箭头函数取 `parameter`;返回类型取 `return_type` 里的类型;异步看有没有 `async`;类成员的装饰器是类主体里排在它前面的 `decorator` 兄弟节点。
|
||||
@@ -1738,6 +1768,7 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令
|
||||
- Shell:解释器取 `#!` 后面的整句,第一个词是 `env` 的是按 PATH 找、程序名是它后面那个词,否则是写死的路径、程序名是路径最后一节。选项:每个开关一项,单字母换成 bash 的长名字(e → errexit、u → nounset、x → xtrace……),`-o 名字` 照写,`-` 开、`+` 关。变量:`variable_assignment` 的 name / value,声明里没给值的只有 name。函数:name 是函数名,doc 是紧贴在上面的注释;`positional` 是主体里(不进嵌套的函数)用到的 `$N` / `${N}` / `${N:-…}`,照位置排,`local x="$N"`、`x=$N` 给它起名字;`prints` 是没重定向到别处的 `echo` / `printf`(进 if、循环、分情况,不进 `$(…)`、管道、嵌套的函数);`captured` 看同一文件里有没有 `$(函数名 …)`;`returns` 是主体里的 `return` 命令。
|
||||
- Dockerfile:解析指令是每行注释 `# 名字=值` 的名字(小写)和值;阶段取 `image_spec` 的 name / tag / digest 字段、`as` 字段(image_alias),stage 看镜像名是不是前面某个阶段的名字(不分大小写),选项是 `param`、`mount_param` 节点的原文 `--名字=值`(值在语法树里没有单独的节点,按原文拆,记下值从哪开始);变量是 `env_pair` 的 name / value、`arg_instruction` 的 name / default;健康检查的 none 看有没有 NONE,检查命令是里面的 `cmd_instruction`;数组写法取 `json_string_array`,shell 写法取 `shell_command` 里各个 `shell_fragment`(`line_continuation` 不算),只有一个 `<<EOT` 的取它下面 `heredoc_block` 从第一行到最后一行。
|
||||
- Python 变量:`assignment` 的 left / type / right,说明是紧跟在后面的属性 docstring 的字符串节点(见上面代码块树规则);right 是 `dictionary` 时 entries 是它的具名子节点(注释除外):`pair` 的 key / value(value 又是 `dictionary` 时连它的 entries)、`dictionary_splat` 里的表达式。
|
||||
- Rust:函数(含 `extern` 块里没有函数体的 `function_signature_item`)的 name / parameters / return_type,`async` 只在写了时填;`self` 的类型节点是整个接收者(`&mut self`)。结构体、枚举、特质、实现、模块的名称、说明、可见性见「Rust」(模块用 ClassFacts:name、doc、access)。`const`、`static` 的名称、类型、值是 `const_item` / `static_item` 的 name / type / value(VariableFacts 的一项 declarator),可见性同别的项:`pub` 写在 `facts.access`,其余写在块的 `access` 属性上。字段和变体携带的数据由中文直译读语法树,不另做 facts。
|
||||
|
||||
## 中文模板直译
|
||||
|
||||
@@ -1890,7 +1921,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTab
|
||||
|
||||
带自己颜色的段(`Segment.color`,库规则按库的配色给,例如 FastAPI 的 HTTP 方法、成功的状态码):暗色、亮色两个写在元素的 style 上(`--color-dark` / `--color-light`),`.cr-colored` 按主题挑一个,盖过词性的颜色;词性照旧报(`role`)。
|
||||
|
||||
种类(种类图标的颜色 + 块底 7% + 边框 35%;亮色主题同色系;图标见「预览(代码块树)」):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;enum 枚举 #b180d7(亮色 #652d90);exception 异常 #f14c4c(亮色 #cd3131,和尝试、捕获、最终同一色系,更红一些);field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框);Shell 的 interpreter 解释器同语句 #9d9d9d,options 选项 #e5c07b(亮色 #9a6a00),case 分情况同条件 #ce9178,command 命令、pipeline 管道 #d7ba7d(亮色 #7c5f12);Dockerfile 的 directive 解析指令同语句,stage 阶段 #56b6c2(亮色 #127f8c),run 执行同命令,copy 复制同导入,setting 设置同选项,label 标签同变量,health 健康检查同尝试 / 捕获,start 启动同返回(原型 DOCKER_KINDS 的颜色);库规则的种类用规则给的颜色(`badge.color` 的 dark / light,webview 写在块的 style 上按主题挑),内置 SQLAlchemy 数据表 #e8a860 / #a3590d,Celery 应用 #98c379 / #4d7f17,定时任务 #56b6c2 / #127f8c,数据类 #ef7fb0 / #b3306c,FastAPI 接口 #61afef / #1a6fb5,FastAPI 路由器 #7aa2f7 / #34548a。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。
|
||||
种类(种类图标的颜色 + 块底 7% + 边框 35%;亮色主题同色系;图标见「预览(代码块树)」):import 导入 #c586c0;function 函数、impl 实现 #dcdcaa;class 类、type 类型 #4ec9b0;trait 特质同声明关键字的蓝;enum 枚举 #b180d7(亮色 #652d90);exception 异常 #f14c4c(亮色 #cd3131,和尝试、捕获、最终同一色系,更红一些);field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框);Shell 的 interpreter 解释器同语句 #9d9d9d,options 选项 #e5c07b(亮色 #9a6a00),case 分情况同条件 #ce9178,command 命令、pipeline 管道 #d7ba7d(亮色 #7c5f12);Dockerfile 的 directive 解析指令同语句,stage 阶段、module 模块 #56b6c2(亮色 #127f8c),run 执行同命令,copy 复制同导入,setting 设置同选项,label 标签同变量,health 健康检查同尝试 / 捕获,start 启动同返回(原型 DOCKER_KINDS 的颜色);库规则的种类用规则给的颜色(`badge.color` 的 dark / light,webview 写在块的 style 上按主题挑),内置 SQLAlchemy 数据表 #e8a860 / #a3590d,Celery 应用 #98c379 / #4d7f17,定时任务 #56b6c2 / #127f8c,数据类 #ef7fb0 / #b3306c,FastAPI 接口 #61afef / #1a6fb5,FastAPI 路由器 #7aa2f7 / #34548a。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。
|
||||
|
||||
## 代码位置
|
||||
|
||||
@@ -1911,8 +1942,8 @@ vscode/src/codeReader/
|
||||
rules/enum.ts 内置 enum 库规则(Python):枚举类认成枚举块,成员照 Python 的规则认
|
||||
rules/dataclass.ts 内置 dataclasses 库规则(Python):数据类卡片,字段照 dataclasses 的规则认
|
||||
rules/fastapi.ts 内置 FastAPI 库规则(Python):路由器卡片(接口表)、接口卡片(请求表、响应表),参数照 FastAPI 的规则放,异常类的状态码、类型别名、枚举经索引查
|
||||
rules/react.ts 内置 React 库规则(TSX / JSX):组件卡片(名称标签、属性表、状态 / 引用 / 读取的数据 / 派生值 / 副作用 / 事件处理、渲染大纲),副作用、处理函数的子卡片,导入的属性类型经索引查
|
||||
rules/reactTree.ts React 规则读语法树的办法:认组件、属性、组件体里的钩子和处理函数、名字用在哪、渲染的 JSX 大纲、别的文件里的属性类型(纯函数)
|
||||
rules/react.ts 内置 React 库规则(TSX / JSX):组件卡片(名称标签、构造参数、状态 / 引用 / 读取的数据 / 派生值 / 副作用 / 事件处理、渲染大纲),副作用、处理函数的子卡片,导入的属性类型经索引查
|
||||
rules/reactTree.ts React 规则读语法树的办法:认组件、构造参数、组件体里的钩子和处理函数、名字用在哪、渲染的 JSX 大纲、别的文件里的属性类型(纯函数)
|
||||
commandRules.ts 命令规则的宿主部分:命令名 → 规则(先注册的算),规则出错记下、当它没读
|
||||
commands/reading.ts 命令规则共用的:认选项(连写拆开、带值的、长选项、--)、拼译文(动作、括号里的意思、照抄不认识的选项)
|
||||
commands/systemCommands.ts 内置命令规则:新建用户 / 用户组(useradd、adduser、addgroup / groupadd)、gpg、install、chown,管 '*' 的只问版本的命令
|
||||
@@ -1929,6 +1960,15 @@ vscode/src/codeReader/
|
||||
languages/shared.ts 语言插件共用的建树工具;异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数)
|
||||
languages/typescript.ts (call:调用的结构,new 是新建;TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor)
|
||||
languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__)
|
||||
languages/rust.ts Rust:代码块树
|
||||
rules/rustTree.ts Rust 库规则读语法树的办法(可见性、属性、字段、变体、match)
|
||||
rules/rustRows.ts Rust 属性表的行(中文直译和库规则共用)
|
||||
rules/rustImpl.ts impl 的方法放进方法表那一行
|
||||
rules/serde.ts serde:可序列化的结构体
|
||||
rules/thiserror.ts thiserror:错误枚举
|
||||
rules/axum.ts axum:接口
|
||||
rules/sqlx.ts sqlx:查询
|
||||
rules/tauri.ts tauri:桌面命令
|
||||
languages/displayNames.ts 中文名的共用部分:第一小句、行尾记号、按优先级汇总
|
||||
languages/pythonNames.ts Python 的中文名写法(Annotated、@Comment、行尾记号、docstring)
|
||||
languages/bash.ts bash / sh / zsh(shellscript):代码块树、facts(解释器、选项、按位置的参数、输出、返回)、command(命令的结构、重定向)、symbols(同一文件里的函数、变量)
|
||||
@@ -2016,6 +2056,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
||||
- 语言插件(`languages/bash.test.ts`,真实解析):`fixtures/fetchRuntime.sh` 的代码块树(种类、行号、层级,同原型 `SHELL_BLOCKS`,原型略去的行也在);elif / else 展平、while / for 的主体是子块、条件串和命令串;解释器(env、写死的路径)、选项(连写拆开、`-o`、`+`、`set --` 不是)、变量(local 一句几个、没值的、readonly 和顶层全大写写死的是常量)、函数(按位置的参数和名字、打印的几句、被 `$(…)` 拿去用、写了的 return);命令的词(写死的文字、有展开的没有)和重定向;同一文件里的名字(函数、赋值、local、for、read、展开,第几个参数不算);行尾 `# @Comment`。
|
||||
- 中文直译(`translators/bashZh.test.ts`,走 `readBlockViews`):`fetchRuntime.sh` 整份逐行同 `FETCH_READING`(即上面「Shell 脚本」的样例);展开(长度、去头去尾、截取、没设时、替换、特殊变量)、条件、重定向、命令规则(不认识的选项照抄、没有规则的照抄)、`&&` / `||`、遍历、`$(dirname "$0")`、常量;字符串色 / 变量色 / 常量色;规则出错照抄命令并报告。
|
||||
- 宿主:命令规则按语言、注册顺序给(`registry.test.ts`),shellscript 的 document 消息带命令规则读的译文、shell 的种类名、同一文件里的名字(`editor/documentMessage.test.ts`),跳到之前最近的定义、定义处算写入(`localSymbols.test.ts`)。
|
||||
- Rust(`languages/rust.test.ts`、`rules/rustLibs.test.ts`,真实解析):`fixtures/rustLang.rs` 的导入、结构体、枚举、实现(方法放进方法表那一行)、函数(异步、分情况)、特质逐行同预览;给结构体加一个字段,字段表那一行标成新增。`fixtures/rustLibs.rs` 的 serde(改名、不输出、字段名改驼峰)、thiserror(展示、透传来源)、axum(方法、路径、路径参数)、sqlx、tauri 逐行同预览;没导入 serde 时 derive 仍是结构体;改 axum 的路径,路径那一格标成改过。`axum::routing` 这种来源算导入了 axum。
|
||||
- Dockerfile:
|
||||
- 语言插件(`languages/dockerfile.test.ts`,真实解析):两份 luffy 的 Dockerfile 的代码块树(同原型,后端的三行注释是阶段说明);并块(紧挨着才并,空行、注释、别的指令隔开,COPY 要从同一个地方,ADD、heredoc 的 COPY 不并,第一个 FROM 之前的是顶层);ONBUILD 和各种设置;facts(解析指令、阶段的名称 / 序号 / 说明 / 标签 / 摘要 / 平台 / 接着前面的阶段、构建参数和环境变量并块、健康检查两种写法和 NONE、启动);嵌着的 shell 的几段(续行去掉、heredoc 正文);数组写法的命令(VOLUME、SHELL 的数组不算);同一文件里的名字(阶段名在 --from、--mount 的 from、FROM 里,变量);中文名;着色。
|
||||
- 中文直译(`translators/dockerfileZh.test.ts`,走 `readBlockViews`,RUN 交给 bash 的插件):两份 luffy 的 Dockerfile 整份逐行同 `NEXT_READING`、`BACKEND_READING`;阶段的各种写法、变量的展开和注解、RUN 的选项 / 数组写法 / heredoc / 一条循环 / 带重定向的一串、没有 shell 插件时照抄、复制和 ADD、设置、健康检查、启动;阶段名和表里变量名的源码下标、RUN 的源码按 shell 着色。
|
||||
@@ -2033,7 +2074,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
||||
- 数据类(`rules/dataclass.test.ts`):`fixtures/dataclassModels.py` 的 `SandboxAccess` 逐字同上面的样例,`AdminUserRow`、`RedirectPayResult`(`field(init=False, default=…)`)、`SmsSent`(`default_factory=dict`)的行;种类 `dataclasses.dataclass`、叫「数据类」、颜色、访问级别同原来的类;编辑范围是整个类、写回原样,`__post_init__` 是子块;类名、字段名的定义范围和带下标的词指回源码原文;装饰器的写法(别名、`dataclasses.dataclass`、`dc.dataclass`)认,pydantic 的、`other.dataclass` 不认,没导入 dataclasses 不认;每个选项的译法、默认值不讲、别的照写、`**x`、别的装饰器一行;字段规则(`ClassVar`、`typing.ClassVar` 留作子块,`InitVar` 的类型和「仅初始化时使用」,`KW_ONLY` 记号并进来、后面的字段「只能按名字传参」、单独写 `kw_only=False` 的「可按位置传参」),`field(...)` 的默认值、`default_factory`(名字、lambda)、各参数、`metadata` 照写、`doc=`,说明取上面的注释块、行尾注释、属性 docstring;没注解的类属性、字段中间的方法照源码顺序是子块,「编辑」写回整个类。
|
||||
- Pydantic 数据模型(`rules/pydantic.test.ts`):`fixtures/aigcMetadata.py` 的 `AIGCMetadata` 逐字同上面的样例(7 个无默认值字段、选项「严格 · 禁止多余字段」、`model_config` 不单成一块),`TextTrace` 的可空字段默认值是「空」;`fixtures/createShareRequest.py` 的 `CreateShareRequest` 逐字核对字段、说明、长度约束,`@field_validator` 仍是子块;种类 `pydantic.model`、叫「数据模型」、颜色、图标 `braces`;同一文件里继承模型的也算,`TypedDict`、`NamedTuple`、普通类、没导入 pydantic 的不是;别名、`Field` 的默认值 / `default_factory` / 约束、说明优先于注释,`ClassVar` 和没注解的类属性留下;类型里的名字带源码下标;索引只收本文件里能确定的模型、没写 `BaseModel` 的文件不解析,别的文件里的父类查不到先当普通类、查到再换成卡片;差异:加一个字段标新增,改一个默认值标那一格。
|
||||
- FastAPI 接口(`rules/fastapi.test.ts`):`fixtures/routes.py` 的 `update_user` 逐字同上面的样例(查到 `Forbidden` 是 403 时四条消息写在一行,没查到时不列),名字、颜色、图标、列名、筛选属性、定义范围、编辑范围是整个函数、函数体是子块;着色(URL 的 `{参数}` / 路径,花括号里的参数名 `refers` 到函数的参数,方法的 Swagger 颜色、成功的状态码绿色、自动带的 422 淡色,带下标的词指回源码原文);`fixtures/skillHubRoutes.py` 的 `list_skills`(Annotated 里的 Query、`_` 的依赖)、`get_skill`(HTTPException 的 status_code / detail)、`install`(Request 不算、`-> None` 没有内容);`fixtures/consoleRoutes.py` 的 `create_user`(`status_code=201`、Pydantic 模型、调别的函数间接抛的不算)、`list_users`(查到的类型别名 `FilterQuery` 是依赖、Literal 是简单类型);参数放在哪儿的各条(路径里的名字含 `{x:path}`、各种标记两种写法、上传文件、枚举和类型别名查到的、Pydantic 模型、`Security`、`Depends()`);标签、依赖、`responses=` 叠上路由对象的,`status.HTTP_…` 常量、`response_model=None`、`response_description`、`raise … from`、只写 `raise`、嵌套函数里的;`FastAPI()` 上的接口、别的装饰器一行;不认的(没导入、路由对象在别的文件、不是路由对象、`websocket`、不带括号)。跨文件(工作区索引 + 假的语言扩展):`update_user` 的 `Forbidden` 按定义处查到 `core/exceptions/schemas.py` 的 403,同名的别处的类不算;索引认的类上的 `status_code`(带注解、常量)、枚举类、`Annotated` 类型别名,普通类不认。路由器卡片:`fixtures/routes.py` 的 `router` 逐字同上面的样例,名字、颜色、图标、列名、编辑范围、定义范围;接口表的函数名带下标、路径里的 `{user_id}` 各 `refers` 到各自函数的参数、方法的颜色;接口卡片的「路由器」行排在说明后面、名字带下标指回路由器;写了依赖、默认响应才有这两行,`FastAPI()` 没有前缀,属性 docstring 是说明,没挂接口时「0 个」、不画表格。
|
||||
- React 组件(`rules/reactTree.test.ts`、`rules/react.test.ts`,luffy 的七个真实组件):认组件(七个文件里的组件、小写工具函数和不渲染 JSX 的不认;memo / forwardRef 包着的、默认导出、文件末尾 export 的、null 和 JSX 混着返回的);属性(行内类型字面量、同文件接口带 JSDoc、回调、children、默认值、改名、其余属性、方法签名、`FC<Props>`、`forwardRef<R, Props>`、导入的类型查不到和查到了);组件体归到哪儿、名字用在哪(副作用、渲染时重置、挂在 `<main>` 的 ref、处理函数绑在每个按钮上);渲染大纲(原生标签、组件、类、条件、三元、列表、片段、注释、几处返回)和 className 的写法。卡片:`fixtures/desktopTitlebar.tsx.txt` 逐字同上面的样例、没有留下的子块,`"use client"` 和 JSDoc 不单独成块;LegalDocPage 的各分区逐行、副作用 / 处理函数子卡片和它们放在哪一行、样式小标点开的全文;ShareButton 的 useCallback、只在挂载时的副作用;PosterDialog 没绑在 JSX 上的处理函数;差异:加一个属性、改一个副作用的依赖,组件卡片属性表那一行新加、副作用「时机」逐截标。宿主:`libraries` 空的规则都启用;略去的块不出块、有子块的不能略去;放进卡片里的子块记下行 / 子行,不合约定的记下来。webview:放进卡片里的子块画在那一行下面(`BlockTree.test.tsx`),点子行才展开的默认收着、名称是按钮、展开时那一行垫上种类色;默认折叠、跳到定义 / 上一处下一处 / 只看改动展开的是那一行(`viewState`、`reveal`、`diffView` 的测试);小标签、条件小框、类名小标的样子(`SegmentText.test.tsx`)。
|
||||
- React 组件(`rules/reactTree.test.ts`、`rules/react.test.ts`,luffy 的七个真实组件):认组件(七个文件里的组件、小写工具函数和不渲染 JSX 的不认;memo / forwardRef 包着的、默认导出、文件末尾 export 的、null 和 JSX 混着返回的);构造参数(行内类型字面量、同文件接口带 JSDoc、回调、children、默认值、改名、其余属性、方法签名、`FC<Props>`、`forwardRef<R, Props>`、导入的类型查不到和查到了);组件体归到哪儿、名字用在哪(副作用、渲染时重置、挂在 `<main>` 的 ref、处理函数绑在每个按钮上);渲染大纲(原生标签、组件、类、条件、三元、列表、片段、注释、几处返回)和 className 的写法。卡片:`fixtures/desktopTitlebar.tsx.txt` 逐字同上面的样例、没有留下的子块,`"use client"` 和 JSDoc 不单独成块;LegalDocPage 的各分区逐行、副作用 / 处理函数子卡片和它们放在哪一行、样式小标点开的全文;ShareButton 的 useCallback、只在挂载时的副作用;PosterDialog 没绑在 JSX 上的处理函数;差异:加一个属性、改一个副作用的依赖,组件卡片属性表那一行新加、副作用「时机」逐截标。宿主:`libraries` 空的规则都启用;略去的块不出块、有子块的不能略去;放进卡片里的子块记下行 / 子行,不合约定的记下来。webview:放进卡片里的子块画在那一行下面(`BlockTree.test.tsx`),点子行才展开的默认收着、名称是按钮、展开时那一行垫上种类色;默认折叠、跳到定义 / 上一处下一处 / 只看改动展开的是那一行(`viewState`、`reveal`、`diffView` 的测试);小标签、条件小框、类名小标的样子(`SegmentText.test.tsx`)。
|
||||
- 规则查别的文件里的定义(`ruleLookups.test.ts`):第一遍当没查到、记下问了什么(同一索引同名的只记一处);查到后叫重排,交出定义处正是的那一项;同一版本不再查;索引里没有同名的不问语言扩展,定义处不是索引里的项算没有,没变不叫重排;新版本先用上一版的、再查、变了才叫重排;语言扩展没答时已知的不动、隔一会儿再查。
|
||||
- 带自己颜色的词(webview `SegmentText.test.tsx`):`cr-colored`,暗色、亮色写在 style 上;讲别处名字的段 `data-offset` 记那个名字的下标。
|
||||
- 异常(`languages/exceptions.test.ts`,真实解析 + 中文直译 + 视图数据):`fixtures/appExceptions.py` 的 `AppException`、`NotFound` 逐字同「异常」的样例(列名随有没有类型、说明变),`GatewayTimeout` 的多段说明,方法是子块;`fixtures/commerceExceptions.py` 16 个类都是异常、只有说明的四行、写了 `__init__` 的构造参数;认哪些类(自带的异常类、`Warning`、不合后缀的自带类 `StopIteration` / `KeyboardInterrupt`、`pkg.errors.AppException`、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、`Mapping[...]`、`*bases` 不算,函数里的类也认);类属性(带不带注解、只写注解、说明取上面的注释块 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法、讲方法的注释、最后面的注释留作子块),构造参数去掉 self、`*args` 开头的不去;`@dataclass` 的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScript `fixtures/exceptions.ts.txt`(`extends Error`、类字段含 static、参数属性、JSDoc、写在后面的父类、类表达式、`ns.X<T>`,父类名不合习惯的照常是类)、JavaScript 的类字段;TypeScript 和 Python 同样的异常得到一样的行和列名;筛选的种类叫「异常」、没有 badge;属性的中文名和去掉行尾记号。webview:异常块默认折叠(`viewState.test.ts`),跳到类名闪整块、属性名 / 构造参数名只闪表里那一行(`reveal.test.ts`)。
|
||||
|
||||
@@ -32,6 +32,8 @@ const TREE_SITTER_WASM = [
|
||||
'tree-sitter-javascript.wasm',
|
||||
'tree-sitter-python.wasm',
|
||||
'tree-sitter-bash.wasm',
|
||||
// 包入口就在 wasm/(main 指向 wasm/tree-sitter.js),rust 的语法包和 python 的在同一层
|
||||
'tree-sitter-rust.wasm',
|
||||
]
|
||||
|
||||
// @vscode/tree-sitter-wasm 没有的语法包,从 tree-sitter-wasm(预编译、签过名的各家语法包)拿:语言名 → 它的 wasm
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
"publisher": "clurdra",
|
||||
"name": "superpowers-vscode-clurdra",
|
||||
"displayName": "Superpowers-clurdra",
|
||||
"version": "0.2.235",
|
||||
"version": "0.2.238",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"description": "Superpowers specs and plans Kanban explorer",
|
||||
"author": "clurdra",
|
||||
@@ -379,7 +379,7 @@
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "superpowers.codeReader.showPreview",
|
||||
"when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile)$/ || resourceLangId in superpowers.codeReader.languageIds)",
|
||||
"when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile|rust)$/ || resourceLangId in superpowers.codeReader.languageIds)",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
@@ -411,7 +411,7 @@
|
||||
},
|
||||
{
|
||||
"command": "superpowers.codeReader.showPreview",
|
||||
"when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile)$/ || resourceLangId in superpowers.codeReader.languageIds)"
|
||||
"when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile|rust)$/ || resourceLangId in superpowers.codeReader.languageIds)"
|
||||
},
|
||||
{
|
||||
"command": "superpowers.codeReader.showSource",
|
||||
|
||||
@@ -26,6 +26,10 @@ function factAttributes(block: CodeBlock): Partial<Record<CommonAttribute, strin
|
||||
return { access: block.facts.access, async: flag(block.facts.async) }
|
||||
case 'class':
|
||||
return { access: block.facts.access, abstract: flag(block.facts.abstract) }
|
||||
case 'trait':
|
||||
case 'impl':
|
||||
case 'module':
|
||||
return { access: block.facts.access }
|
||||
case 'enum':
|
||||
case 'exception':
|
||||
case 'const':
|
||||
|
||||
@@ -144,6 +144,7 @@ function ownName(block: CodeBlock): Node | undefined {
|
||||
case 'class':
|
||||
case 'enum':
|
||||
case 'exception':
|
||||
case 'module':
|
||||
return block.facts.name
|
||||
case 'const':
|
||||
case 'variable':
|
||||
@@ -289,6 +290,12 @@ export function toBlockViews(
|
||||
translate: node => layerTranslator ? layerTranslator.expression(node, layerDocument) : [{ text: node.text, role: 'plain' }],
|
||||
doc: node => layerTranslator?.doc(node, layerDocument),
|
||||
lookup: layerRules.lookup ?? (() => undefined),
|
||||
snippet: (node) => {
|
||||
const startRow = node.startPosition.row
|
||||
const end = node.endPosition
|
||||
const endRow = end.column === 0 && end.row > startRow ? end.row - 1 : end.row
|
||||
return sourceRows({ startIndex: node.startIndex, endIndex: node.endIndex, startRow, endRow })
|
||||
},
|
||||
}
|
||||
/** 一行译文,连着摊开实参的表、块的说明(有才有)。 */
|
||||
const textView = (block: TextBlock): { translation: PreviewText, table?: PreviewTable, doc?: Markdown } => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export type { Node }
|
||||
*/
|
||||
export type BlockKind
|
||||
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
|
||||
| 'trait' | 'impl' | 'module'
|
||||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||||
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
|
||||
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
|
||||
@@ -474,6 +475,11 @@ export interface BlockShape {
|
||||
/** 预览成属性表的块:语言插件从语法树读出结构化信息(facts),翻译插件把它排成属性表。 */
|
||||
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
|
||||
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
|
||||
/** Rust 的 trait:方法只列签名,事实和类一样(名称、说明、可见性)。 */
|
||||
export interface TraitBlock extends BlockShape { kind: 'trait', facts: ClassFacts }
|
||||
/** Rust 的 impl:标题是「某类型 的方法」,方法是子块,放进方法表的某一行。 */
|
||||
export interface ImplBlock extends BlockShape { kind: 'impl', facts: ClassFacts }
|
||||
export interface ModuleBlock extends BlockShape { kind: 'module', facts: ClassFacts }
|
||||
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
|
||||
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
|
||||
/** 异常块的头部是整个类(类属性都在属性表里讲),留下的方法(含构造函数)等是子块。 */
|
||||
@@ -491,11 +497,11 @@ export interface StageBlock extends BlockShape { kind: 'stage', facts: StageFact
|
||||
export interface HealthBlock extends BlockShape { kind: 'health', facts: HealthFacts }
|
||||
export interface StartBlock extends BlockShape { kind: 'start', facts: StartFacts }
|
||||
export type FactBlock
|
||||
= | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
|
||||
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
|
||||
| DirectiveBlock | StageBlock | HealthBlock | StartBlock
|
||||
|
||||
/** 带说明(facts.doc)的块。 */
|
||||
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
|
||||
export type DocumentedBlock = FunctionBlock | ClassBlock | TraitBlock | ImplBlock | EnumBlock | ExceptionBlock | VariableBlock
|
||||
|
||||
/** 属性表块的种类和 facts,不带块的形状(节点、头部、子块):库规则把一块认成现成的一种属性表块时给它(RuleFacts)。 */
|
||||
export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick<B, 'kind' | 'facts'> : never : never
|
||||
@@ -589,11 +595,40 @@ export type PreviewText = Inline[] | Markdown
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/**
|
||||
* 能单独打开编辑的一段源码:这几行整行,去掉共同缩进,着色和块的 code / codeTokens 同一套切法。
|
||||
* 网页不自己切。点铅笔之后只出现这一段,不换整张卡片。
|
||||
*/
|
||||
export interface SourceSnippet {
|
||||
/** 起止行,0 起,含头含尾。 */
|
||||
startLine: number
|
||||
endLine: number
|
||||
code: string
|
||||
/** 着色,拼起来等于 code。 */
|
||||
codeTokens: Segment[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性表一个子行的某一格里,能单独打开的一段源码。
|
||||
* cell 是第几格(0 起),from / to 是这一格文字里这一处的范围(含头不含尾)。
|
||||
*/
|
||||
export interface CellEdit {
|
||||
cell: number
|
||||
from: number
|
||||
to: number
|
||||
snippet: SourceSnippet
|
||||
}
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值。 */
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: Inline[][]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
|
||||
items?: PropertyItem[]
|
||||
/**
|
||||
* 这一行里能单独打开的几段源码(状态「改它的」的每一处、读取的数据的「来自」)。
|
||||
* 打开后只出现那一段。没有能打开的就不写。
|
||||
*/
|
||||
edits?: CellEdit[]
|
||||
/**
|
||||
* 排在这一行最后一格里、那格文字下面的表:实参表里一个实参的值里有跨几行写的调用时,是那个调用的实参表,
|
||||
* 表里的行又可以带 table,一层层往下套。总是展开的。
|
||||
@@ -790,6 +825,9 @@ export const KIND_ICONS: Record<BlockKind, KindIconName> = {
|
||||
import: 'import',
|
||||
function: 'square-function',
|
||||
class: 'box',
|
||||
trait: 'network',
|
||||
impl: 'square-function',
|
||||
module: 'layers',
|
||||
type: 'braces',
|
||||
enum: 'list-ordered',
|
||||
exception: 'triangle-alert',
|
||||
@@ -909,6 +947,8 @@ export interface RuleContext {
|
||||
displayLanguage?: string
|
||||
/** 用当前翻译插件把一个表达式或类型节点翻成一行译文(Translator.expression):名字带源码下标,能跳到定义、高亮各处出现;没有翻译插件时照抄源码。 */
|
||||
translate: (node: Node) => Inline[]
|
||||
/** 一个节点占的整行源码和着色(见 SourceSnippet),和块视图切行是同一套。规则把它挂在译文旁边。 */
|
||||
snippet: (node: Node) => SourceSnippet
|
||||
/** 用当前翻译插件取一段说明文字(Translator.doc);没有翻译插件、没有文字时没有。 */
|
||||
doc: (node: Node) => Markdown | undefined
|
||||
/**
|
||||
@@ -980,7 +1020,7 @@ export interface LibraryRule {
|
||||
label: string // 'SQLAlchemy 数据表'
|
||||
languageIds: string[]
|
||||
/**
|
||||
* 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」开头(它的子模块)才启用。
|
||||
* 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」「它::」开头(它的子模块;Rust 的路径用 ::)才启用。
|
||||
* 宿主按语言插件给的导入判断,规则不用自己读导入语句。空数组是不看导入、languageIds 里这几种语言的源码都启用
|
||||
* (React 组件:新的 JSX 写法不用导入 react,TSX / JSX 文件里就是 React 组件)。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use foo::{Bar as Baz, Qux};
|
||||
|
||||
/// 桌面端列表里的一行代理。
|
||||
/// 后面还有一句。
|
||||
pub struct AgentRow {
|
||||
pub id: Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(crate) struct CrateOnly {
|
||||
pub(super) up: u8,
|
||||
}
|
||||
|
||||
/// 这个代理现在处在哪一步。
|
||||
pub enum AgentState {
|
||||
Idle,
|
||||
Running { pid: u32 },
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
impl AgentRow {
|
||||
fn rename(&mut self, name: String) {}
|
||||
}
|
||||
|
||||
/// 在本机拉起接口服务。
|
||||
pub async fn start_server(addr: SocketAddr) -> Result<(), StartError> {
|
||||
match result {
|
||||
Ok(listener) => {
|
||||
// 记下监听地址
|
||||
self.listener = Some(listener);
|
||||
}
|
||||
Err(err) => {
|
||||
// 返回 StartError
|
||||
return Err(StartError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 id 读写代理。
|
||||
pub trait Store {
|
||||
fn get(&self, id: Uuid) -> Result<AgentRow, StoreError>;
|
||||
fn save(&mut self, row: AgentRow) -> Result<(), StoreError>;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 桌面端列表里的一行代理。
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentRow {
|
||||
#[serde(rename = "agent_id")]
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
#[serde(skip)]
|
||||
secret: String,
|
||||
}
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// 读写代理时出的错。
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AgentError {
|
||||
/// 这个 id 没有代理
|
||||
#[error("agent {id} 不存在")]
|
||||
NotFound { id: Uuid },
|
||||
/// 调用上游失败
|
||||
#[error("上游错误")]
|
||||
Upstream(#[from] Other),
|
||||
}
|
||||
|
||||
use axum::routing::get;
|
||||
|
||||
/// 按 id 取一个代理。
|
||||
#[get("/agents/{id}")]
|
||||
async fn get_agent(Path(id): Path<Uuid>) -> Result<Json<AgentRow>, StatusCode> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
let app = Router::new().route("/health", get(health));
|
||||
|
||||
use sqlx::query_as;
|
||||
|
||||
// 按 id 取一行。
|
||||
let row = sqlx::query_as!(AgentRow, "SELECT id, name FROM agents WHERE id = $1", id);
|
||||
|
||||
use tauri::State;
|
||||
|
||||
/// 让窗口把这一行改名。
|
||||
#[tauri::command]
|
||||
async fn rename_agent(state: State<App>, name: String) -> Result<String, String> {
|
||||
todo!()
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { TreeFilters } from './explorer/treeFilters'
|
||||
import { createBashAdapter } from './languages/bash'
|
||||
import { createDockerfileAdapter } from './languages/dockerfile'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createRustAdapter } from './languages/rust'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
import { resolveReference } from './libraryRules'
|
||||
import { indexedNames } from './nameResolution'
|
||||
@@ -27,17 +28,24 @@ import { FileNotes } from './notes/fileNotes'
|
||||
import { CodeQa } from './qa/codeQa'
|
||||
import { CodeReaderRegistry } from './registry'
|
||||
import { ReviewMode } from './review/reviewMode'
|
||||
import { axumRule } from './rules/axum'
|
||||
import { celeryRule } from './rules/celery'
|
||||
import { dataclassRule } from './rules/dataclass'
|
||||
import { enumRule } from './rules/enum'
|
||||
import { fastapiRule } from './rules/fastapi'
|
||||
import { pydanticRule } from './rules/pydantic'
|
||||
import { reactRule } from './rules/react'
|
||||
import { rustImplRule } from './rules/rustImpl'
|
||||
import { serdeRule } from './rules/serde'
|
||||
import { sqlalchemyRule } from './rules/sqlalchemy'
|
||||
import { sqlxRule } from './rules/sqlx'
|
||||
import { tauriRule } from './rules/tauri'
|
||||
import { thiserrorRule } from './rules/thiserror'
|
||||
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
|
||||
import { bashZhTranslator } from './translators/bashZh'
|
||||
import { dockerfileZhTranslator } from './translators/dockerfileZh'
|
||||
import { pythonZhTranslator } from './translators/pythonZh'
|
||||
import { rustZhTranslator } from './translators/rustZh'
|
||||
import { typescriptZhTranslator } from './translators/typescriptZh'
|
||||
import { UncommittedMode } from './uncommitted/uncommittedMode'
|
||||
import { workspaceIndexHost } from './workspaceFiles'
|
||||
@@ -147,10 +155,12 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
api.registerLanguage(createPythonAdapter(wasmDir)),
|
||||
api.registerLanguage(createBashAdapter(wasmDir)),
|
||||
api.registerLanguage(createDockerfileAdapter(wasmDir)),
|
||||
api.registerLanguage(createRustAdapter(wasmDir)),
|
||||
api.registerTranslator(typescriptZhTranslator),
|
||||
api.registerTranslator(pythonZhTranslator),
|
||||
api.registerTranslator(bashZhTranslator),
|
||||
api.registerTranslator(dockerfileZhTranslator),
|
||||
api.registerTranslator(rustZhTranslator),
|
||||
...[...shellCommandRules, ...systemCommandRules, ...packageCommandRules].map(rule => api.registerCommandRule(rule)),
|
||||
api.registerLibraryRule(sqlalchemyRule),
|
||||
api.registerLibraryRule(celeryRule),
|
||||
@@ -159,6 +169,12 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
api.registerLibraryRule(pydanticRule),
|
||||
api.registerLibraryRule(fastapiRule),
|
||||
api.registerLibraryRule(reactRule),
|
||||
api.registerLibraryRule(rustImplRule),
|
||||
api.registerLibraryRule(serdeRule),
|
||||
api.registerLibraryRule(thiserrorRule),
|
||||
api.registerLibraryRule(axumRule),
|
||||
api.registerLibraryRule(sqlxRule),
|
||||
api.registerLibraryRule(tauriRule),
|
||||
)
|
||||
return api
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { blockMarks } from '../diff/viewDiff'
|
||||
import { rustImplRule } from '../rules/rustImpl'
|
||||
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { rustZhTranslator } from '../translators/rustZh'
|
||||
import { createRustAdapter } from './rust'
|
||||
|
||||
const adapter = createRustAdapter(WASM_DIR)
|
||||
const sample = readFixture('rustLang.rs')
|
||||
|
||||
function views(source: string): Promise<BlockView[]> {
|
||||
return withBlocks(adapter, 'rust', source, blocks =>
|
||||
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
|
||||
rules: [rustImplRule],
|
||||
report: () => {},
|
||||
}))
|
||||
}
|
||||
|
||||
function card(list: BlockView[], kind: string): BlockView {
|
||||
const found = list.find(block => block.kind === kind)
|
||||
if (!found)
|
||||
throw new Error(`没有 ${kind}`)
|
||||
return found
|
||||
}
|
||||
|
||||
describe('rust 语言卡片', () => {
|
||||
it('use、结构体、枚举、实现、函数、特质', async () => {
|
||||
expect(viewLines(await views(sample))).toEqual([
|
||||
'import 1',
|
||||
' 来源: serde',
|
||||
' 导入项: 1 个',
|
||||
' Serialize | —',
|
||||
'import 2',
|
||||
' 来源: std::sync',
|
||||
' 导入项: 1 个',
|
||||
' Arc | —',
|
||||
'import 3',
|
||||
' 来源: foo',
|
||||
' 导入项: 2 个',
|
||||
' Bar | Baz',
|
||||
' Qux | —',
|
||||
'class 5',
|
||||
' 名称: AgentRow',
|
||||
' 说明: 桌面端列表里的一行代理。',
|
||||
' 可见性: 公开',
|
||||
' 字段: 2 个',
|
||||
' id | Uuid | 公开',
|
||||
' name | String | 本模块',
|
||||
'class 12',
|
||||
' 名称: CrateOnly',
|
||||
' 可见性: 本 crate',
|
||||
' 字段: 1 个',
|
||||
' up | u8 | 父模块',
|
||||
'enum 16',
|
||||
' 名称: AgentState',
|
||||
' 说明: 这个代理现在处在哪一步。',
|
||||
' 变体: 3 个',
|
||||
' Idle | —',
|
||||
' Running | { pid: u32 }',
|
||||
' Failed | String',
|
||||
'rust.impl 23',
|
||||
' 名称: AgentRow 的方法',
|
||||
' 方法: 1 个',
|
||||
' rename | &mut self, name: String | ()',
|
||||
' function 24',
|
||||
' 名称: rename',
|
||||
' 可见性: 本模块',
|
||||
' 参数: 2 个',
|
||||
' self | &mut self',
|
||||
' name | String',
|
||||
' 返回: ()',
|
||||
'function 27',
|
||||
' 名称: start_server',
|
||||
' 说明: 在本机拉起接口服务。',
|
||||
' 异步: 是',
|
||||
' 可见性: 公开',
|
||||
' 输入参数: 1 个',
|
||||
' addr | SocketAddr',
|
||||
' 返回: Result<(), StartError>',
|
||||
' 分情况: 2 支',
|
||||
' Ok(listener) | 记下监听地址',
|
||||
' Err(err) | 返回 StartError',
|
||||
'trait 41',
|
||||
' 名称: Store',
|
||||
' 说明: 按 id 读写代理。',
|
||||
' 方法: 2 个',
|
||||
' get | &self, id: Uuid | Result<AgentRow, StoreError>',
|
||||
' save | &mut self, row: AgentRow | Result<(), StoreError>',
|
||||
])
|
||||
})
|
||||
|
||||
it('实现的方法放进方法表那一行', async () => {
|
||||
const impl = card(await views(sample), 'rust.impl')
|
||||
const [method] = impl.children
|
||||
expect(method?.slot).toEqual({ row: 1, item: 0 })
|
||||
})
|
||||
|
||||
it('给结构体加一个字段,多出来的那一行标成新增', async () => {
|
||||
const before = 'pub struct AgentRow {\n name: String,\n}\n'
|
||||
const after = 'pub struct AgentRow {\n name: String,\n id: Uuid,\n}\n'
|
||||
const [oldCard] = await views(before)
|
||||
const [newCard] = await views(after)
|
||||
const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined
|
||||
const field = newCard && 'properties' in newCard ? newCard.properties.findIndex(row => row.label === '字段') : -1
|
||||
expect(field).toBeGreaterThanOrEqual(0)
|
||||
expect(marks?.rows?.changed[field]).toEqual({
|
||||
items: { changed: { 1: { added: true } }, removed: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('带身体的模块拆成子块,常量和外部函数是卡片', async () => {
|
||||
const source = [
|
||||
'/// 平台相关。',
|
||||
'pub mod platform {',
|
||||
' /// 读入。',
|
||||
' use std::io::Read;',
|
||||
' /// 上限。',
|
||||
' pub const LIMIT: u32 = 8;',
|
||||
' static NAME: &str = "a";',
|
||||
' pub fn open() -> u32 { LIMIT }',
|
||||
' extern "C" {',
|
||||
' /// 外部。',
|
||||
' pub fn bar(x: i32) -> i32;',
|
||||
' }',
|
||||
' let local = 1;',
|
||||
' println!("hi");',
|
||||
'}',
|
||||
'mod other;',
|
||||
'',
|
||||
'impl Agent {',
|
||||
' pub const N: u8 = 2;',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
const list = await views(source)
|
||||
const mod = list.find(block => block.kind === 'module')
|
||||
expect(mod?.kind).toBe('module')
|
||||
expect(list.some(block => block.kind === 'statement' && block.code.includes('const LIMIT'))).toBe(false)
|
||||
expect(list.filter(block => block.kind === 'statement').map(block => block.code)).toEqual(['mod other;'])
|
||||
expect(mod?.children.map(block => block.kind)).toEqual(['import', 'const', 'variable', 'function', 'function', 'statement', 'statement'])
|
||||
expect(viewLines(mod ? [mod] : [])).toEqual([
|
||||
'module 1',
|
||||
' 名称: platform',
|
||||
' 说明: 平台相关。',
|
||||
' 可见性: 公开',
|
||||
' import 3',
|
||||
' 来源: std::io',
|
||||
' 导入项: 1 个',
|
||||
' Read | —',
|
||||
' const 5',
|
||||
' 名称: LIMIT',
|
||||
' 说明: 上限。',
|
||||
' 访问级别: 公共',
|
||||
' 类型: u32',
|
||||
' 值: 8',
|
||||
' variable 7',
|
||||
' 名称: NAME',
|
||||
' 类型: &str',
|
||||
' 初始值: "a"',
|
||||
' function 8',
|
||||
' 名称: open',
|
||||
' 可见性: 公开',
|
||||
' 返回: u32',
|
||||
' function 10',
|
||||
' 名称: bar',
|
||||
' 说明: 外部。',
|
||||
' 可见性: 公开',
|
||||
' 输入参数: 1 个',
|
||||
' x | i32',
|
||||
' 返回: i32',
|
||||
' statement 13: 令 local = 1;',
|
||||
' statement 14: println!("hi")',
|
||||
])
|
||||
const impl = list.find(block => block.kind === 'rust.impl')
|
||||
const inner = impl?.children.find(block => block.kind === 'const')
|
||||
expect(inner?.kind).toBe('const')
|
||||
expect(viewLines(inner ? [inner] : []).join('\n')).toContain('名称: N')
|
||||
expect(viewLines(inner ? [inner] : []).join('\n')).toMatch(/类型: u8|值: 2/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,320 @@
|
||||
import type { Access, ClassBlock, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, ModuleBlock, Node, ParameterFacts, TokenRole, TraitBlock, VariableBlock } from '../contract'
|
||||
import * as path from 'node:path'
|
||||
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
|
||||
import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, named } from './shared'
|
||||
|
||||
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
|
||||
|
||||
export function createRustAdapter(wasmDir: string): LanguageAdapter {
|
||||
return {
|
||||
id: 'rust',
|
||||
languageIds: ['rust'],
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-rust.wasm'),
|
||||
toBlocks: root => topBlocks(named(root)),
|
||||
tokenRole: rustTokenRole,
|
||||
}
|
||||
}
|
||||
|
||||
/** extern 块本身不成一块源码,里面的签名跟所在这一层的别的项走同一条路。 */
|
||||
function unwrapExtern(item: Node): Node[] {
|
||||
if (item.type !== 'foreign_mod_item')
|
||||
return [item]
|
||||
const body = item.childForFieldName('body')
|
||||
return body ? named(body).flatMap(unwrapExtern) : []
|
||||
}
|
||||
|
||||
function topBlocks(items: Node[]): CodeBlock[] {
|
||||
const list = items.flatMap(unwrapExtern)
|
||||
const blocks: CodeBlock[] = []
|
||||
let index = 0
|
||||
while (index < list.length) {
|
||||
const item = list[index]
|
||||
if (isDoc(item)) {
|
||||
const docs = [item]
|
||||
let next = index + 1
|
||||
while (next < list.length && isDoc(list[next]) && list[next].startPosition.row <= docs[docs.length - 1].endPosition.row + 1)
|
||||
docs.push(list[next++])
|
||||
while (next < list.length && list[next].type === 'attribute_item')
|
||||
next++
|
||||
const target = list[next]
|
||||
if (target && target.type !== 'line_comment' && target.type !== 'block_comment' && target.type !== 'empty_statement') {
|
||||
blocks.push(withPreamble(itemBlock(target), docs))
|
||||
index = next + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (item.type === 'attribute_item' || item.type === 'empty_statement') {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if (item.type === 'line_comment' || item.type === 'block_comment')
|
||||
blocks.push(leafBlock('comment', item))
|
||||
else
|
||||
blocks.push(withPreamble(itemBlock(item), []))
|
||||
index++
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function itemBlock(item: Node): CodeBlock {
|
||||
switch (item.type) {
|
||||
case 'use_declaration':
|
||||
return useBlock(item)
|
||||
case 'struct_item':
|
||||
return structBlock(item)
|
||||
case 'enum_item':
|
||||
return enumBlock(item)
|
||||
case 'impl_item':
|
||||
return implBlock(item)
|
||||
case 'trait_item':
|
||||
return traitBlock(item)
|
||||
case 'mod_item':
|
||||
return moduleBlock(item)
|
||||
case 'const_item':
|
||||
return valueBlock(item, 'const')
|
||||
case 'static_item':
|
||||
return valueBlock(item, 'variable')
|
||||
case 'function_item':
|
||||
case 'function_signature_item':
|
||||
return functionBlock(item)
|
||||
default:
|
||||
return leafBlock('statement', item)
|
||||
}
|
||||
}
|
||||
|
||||
/** 没有身体的 `mod 名字;` 不拆。有身体的模块,身体和文件顶层同一套规则。 */
|
||||
function moduleBlock(node: Node): CodeBlock {
|
||||
const body = node.childForFieldName('body')
|
||||
if (!body)
|
||||
return leafBlock('statement', node)
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
return {
|
||||
kind: 'module',
|
||||
...compoundShape(node, body.startIndex, named(body), items => topBlocks(items)),
|
||||
...(attributes && { attributes }),
|
||||
facts: classFacts(node, access),
|
||||
}
|
||||
}
|
||||
|
||||
/** const 是常量,static 是变量;名称、类型、值交给现成的常量 / 变量属性表。 */
|
||||
function valueBlock(node: Node, kind: 'const' | 'variable'): CodeBlock {
|
||||
const name = node.childForFieldName('name')
|
||||
if (!name)
|
||||
return leafBlock('statement', node)
|
||||
const type = node.childForFieldName('type') ?? undefined
|
||||
const value = node.childForFieldName('value') ?? undefined
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
return {
|
||||
kind,
|
||||
...leafShape(node),
|
||||
...(attributes && { attributes }),
|
||||
facts: { access, declarators: [{ name, type, value }] },
|
||||
}
|
||||
}
|
||||
|
||||
/** 文档注释和写在前面的属性算进头部。文档注释的第一句是说明。 */
|
||||
function withPreamble(block: CodeBlock, docs: Node[]): CodeBlock {
|
||||
const [doc] = docs
|
||||
const attribute = leadingAttributes(block.node)[0]
|
||||
let next = block
|
||||
if (doc && isDocumentedKind(next))
|
||||
next = { ...extendHeader(next, doc), facts: { ...next.facts, doc } } as typeof next
|
||||
const from = doc ?? attribute
|
||||
if (from && next.header.startIndex > from.startIndex)
|
||||
next = extendHeader(next, from)
|
||||
return next
|
||||
}
|
||||
|
||||
function isDocumentedKind(block: CodeBlock): block is FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | VariableBlock {
|
||||
return block.kind === 'function' || block.kind === 'class' || block.kind === 'trait' || block.kind === 'impl' || block.kind === 'module' || block.kind === 'enum' || block.kind === 'const' || block.kind === 'variable'
|
||||
}
|
||||
|
||||
function isDoc(node: Node): boolean {
|
||||
return (node.type === 'line_comment' || node.type === 'block_comment') && named(node).some(child => child.type === 'outer_doc_comment_marker')
|
||||
}
|
||||
|
||||
function useBlock(node: Node): ImportBlock {
|
||||
const argument = node.childForFieldName('argument')
|
||||
const { source, items } = argument ? useParts(argument) : { items: [] }
|
||||
return { kind: 'import', ...leafShape(node), facts: { sources: source ? [source] : [], items } }
|
||||
}
|
||||
|
||||
function useParts(node: Node): { source?: Node, items: ImportItem[] } {
|
||||
switch (node.type) {
|
||||
case 'scoped_identifier': {
|
||||
const source = node.childForFieldName('path') ?? undefined
|
||||
const name = node.childForFieldName('name')
|
||||
return { source, items: name ? [{ kind: 'named', name }] : [] }
|
||||
}
|
||||
case 'scoped_use_list': {
|
||||
const source = node.childForFieldName('path') ?? undefined
|
||||
const list = node.childForFieldName('list')
|
||||
return { source, items: list ? useList(list) : [] }
|
||||
}
|
||||
case 'use_list':
|
||||
return { items: useList(node) }
|
||||
case 'use_as_clause':
|
||||
return { items: asItem(node) }
|
||||
case 'use_wildcard':
|
||||
return { items: [{ kind: 'namespace' }] }
|
||||
case 'identifier':
|
||||
return { items: [{ kind: 'named', name: node }] }
|
||||
default:
|
||||
return { items: [] }
|
||||
}
|
||||
}
|
||||
|
||||
function useList(list: Node): ImportItem[] {
|
||||
return named(list).flatMap((child) => {
|
||||
if (child.type === 'identifier')
|
||||
return [{ kind: 'named' as const, name: child }]
|
||||
if (child.type === 'use_as_clause')
|
||||
return asItem(child)
|
||||
if (child.type === 'use_wildcard')
|
||||
return [{ kind: 'namespace' as const }]
|
||||
if (child.type === 'scoped_identifier') {
|
||||
const name = child.childForFieldName('name')
|
||||
return name ? [{ kind: 'named' as const, name }] : []
|
||||
}
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
function asItem(node: Node): ImportItem[] {
|
||||
const name = node.childForFieldName('path')
|
||||
const alias = node.childForFieldName('alias') ?? undefined
|
||||
return name ? [{ kind: 'named', name, alias }] : []
|
||||
}
|
||||
|
||||
function structBlock(node: Node): CodeBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
return {
|
||||
kind: 'class',
|
||||
...shape(node),
|
||||
...(attributes && { attributes }),
|
||||
facts: classFacts(node, access),
|
||||
}
|
||||
}
|
||||
|
||||
function enumBlock(node: Node): EnumBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
const body = node.childForFieldName('body')
|
||||
const members = named(body).flatMap((variant) => {
|
||||
if (variant.type !== 'enum_variant')
|
||||
return []
|
||||
const name = variant.childForFieldName('name')
|
||||
if (!name)
|
||||
return []
|
||||
const payload = variant.childForFieldName('body') ?? undefined
|
||||
return [{ name, ...(payload && { value: payload }), docs: [] }]
|
||||
})
|
||||
return {
|
||||
kind: 'enum',
|
||||
...shape(node),
|
||||
...(attributes && { attributes }),
|
||||
facts: { name: node.childForFieldName('name') ?? node, doc: undefined, access, decorators: [], bases: [], members },
|
||||
}
|
||||
}
|
||||
|
||||
function traitBlock(node: Node): TraitBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
return { kind: 'trait', ...shape(node), ...(attributes && { attributes }), facts: classFacts(node, access) }
|
||||
}
|
||||
|
||||
function implBlock(node: Node): ImplBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
const body = node.childForFieldName('body')
|
||||
const children = body ? topBlocks(named(body)) : []
|
||||
return {
|
||||
kind: 'impl',
|
||||
...compoundShape(node, body?.startIndex ?? node.endIndex, [], () => children),
|
||||
...(attributes && { attributes }),
|
||||
facts: classFacts(node, access, implType(node)),
|
||||
}
|
||||
}
|
||||
|
||||
function functionBlock(node: Node): FunctionBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
const body = node.childForFieldName('body')
|
||||
const headerEnd = body?.startIndex ?? node.endIndex
|
||||
return {
|
||||
kind: 'function',
|
||||
node,
|
||||
header: headerSpan(node, headerEnd),
|
||||
children: [],
|
||||
...(attributes && { attributes }),
|
||||
facts: {
|
||||
name: node.childForFieldName('name') ?? undefined,
|
||||
access,
|
||||
...(isAsync(node) && { async: true }),
|
||||
decorators: leadingAttributes(node),
|
||||
parameters: parametersOf(node.childForFieldName('parameters')),
|
||||
returnType: node.childForFieldName('return_type') ?? undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function classFacts(node: Node, access: Access | undefined, name = node.childForFieldName('name') ?? undefined): ClassFacts {
|
||||
return { name, access, decorators: leadingAttributes(node), typeParameters: [], bases: [], implements: [] }
|
||||
}
|
||||
|
||||
/** impl 的自己的类型:impl Trait for Type 取 Type,impl Type 取 Type。 */
|
||||
function implType(node: Node): Node | undefined {
|
||||
return node.childForFieldName('type') ?? node.childForFieldName('trait') ?? undefined
|
||||
}
|
||||
|
||||
function shape(node: Node): { node: Node, header: { startIndex: number, endIndex: number, startRow: number, endRow: number }, children: CodeBlock[] } {
|
||||
const body = node.childForFieldName('body')
|
||||
return body ? { node, header: headerSpan(node, body.startIndex), children: [] } : { ...leafShape(node) }
|
||||
}
|
||||
|
||||
function parametersOf(list: Node | null): ParameterFacts[] {
|
||||
if (!list)
|
||||
return []
|
||||
return named(list).flatMap((child) => {
|
||||
if (child.type === 'self_parameter') {
|
||||
const name = [...child.children].reverse().find(token => token?.type === 'self')
|
||||
return name ? [{ name, optional: false, type: child }] : []
|
||||
}
|
||||
if (child.type !== 'parameter')
|
||||
return []
|
||||
const name = child.childForFieldName('pattern')
|
||||
const type = child.childForFieldName('type') ?? undefined
|
||||
return name ? [{ name, optional: false, type }] : []
|
||||
})
|
||||
}
|
||||
|
||||
function isAsync(node: Node): boolean {
|
||||
const modifiers = named(node).find(child => child.type === 'function_modifiers')
|
||||
return modifiers?.children.some(child => child?.type === 'async') ?? false
|
||||
}
|
||||
|
||||
function rustTokenRole(leaf: Node): TokenRole {
|
||||
if (leaf.type === 'line_comment' || leaf.type === 'block_comment' || inside(leaf, 'line_comment') || inside(leaf, 'block_comment'))
|
||||
return 'comment'
|
||||
if (inside(leaf, 'string_literal') || leaf.type === 'string_content')
|
||||
return 'string'
|
||||
if (leaf.type === 'integer_literal' || leaf.type === 'float_literal' || leaf.type === 'char_literal')
|
||||
return 'number'
|
||||
if (leaf.type === 'primitive_type' || leaf.type === 'type_identifier')
|
||||
return 'type'
|
||||
if (leaf.type === 'mutable_specifier')
|
||||
return 'keyword'
|
||||
if (!leaf.isNamed)
|
||||
return STORAGE.has(leaf.type) ? 'storage' : /^[a-z]+$/.test(leaf.type) ? 'keyword' : 'plain'
|
||||
if (leaf.type === 'identifier' || leaf.type === 'field_identifier' || leaf.type === 'self') {
|
||||
const parent = leaf.parent?.type
|
||||
return parent === 'function_item' || parent === 'function_signature_item' ? 'function' : 'variable'
|
||||
}
|
||||
return 'plain'
|
||||
}
|
||||
|
||||
function inside(node: Node, type: string): boolean {
|
||||
let current = node.parent
|
||||
while (current) {
|
||||
if (current.type === type)
|
||||
return true
|
||||
current = current.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -209,6 +209,9 @@ function ownDoc(block: CodeBlock): Node | undefined {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
case 'class':
|
||||
case 'trait':
|
||||
case 'impl':
|
||||
case 'module':
|
||||
case 'enum':
|
||||
case 'exception':
|
||||
case 'const':
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('库规则:启用', () => {
|
||||
expect(fromLibrary('sqlalchemy', 'sqlalchemy')).toBe(true)
|
||||
expect(fromLibrary('sqlalchemy.orm', 'sqlalchemy')).toBe(true)
|
||||
expect(fromLibrary('drizzle-orm/pg-core', 'drizzle-orm')).toBe(true)
|
||||
expect(fromLibrary('axum::routing', 'axum')).toBe(true)
|
||||
expect(fromLibrary('sqlalchemy_utils', 'sqlalchemy')).toBe(false)
|
||||
expect(fromLibrary('.sqlalchemy', 'sqlalchemy')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function moduleName(source: Node): string {
|
||||
|
||||
/** 模块是这个库或它的子模块:等于库名,或以「库名.」(Python 的点分模块名)「库名/」(JavaScript 的子路径)开头。 */
|
||||
export function fromLibrary(module: string, library: string): boolean {
|
||||
return module === library || module.startsWith(`${library}.`) || module.startsWith(`${library}/`)
|
||||
return module === library || module.startsWith(`${library}.`) || module.startsWith(`${library}/`) || module.startsWith(`${library}::`)
|
||||
}
|
||||
|
||||
/** 这份源码启用的规则,保持注册顺序:导入了规则关心的某个库,或规则不看导入(libraries 是空的)。 */
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
||||
import { attributeName, docSentence, named, oneLine, returnText, stringContent } from './rustTree'
|
||||
|
||||
/** 和 FastAPI 接口同一个蓝。 */
|
||||
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
||||
const BADGE: KindBadge = { label: '接口', color: COLOR, icon: 'globe' }
|
||||
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'])
|
||||
|
||||
/**
|
||||
* 处理函数上的 get / post 等路由属性,或 Router 上能看出来的 route。
|
||||
* 请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。
|
||||
* 文件里导入了 axum 才启用。
|
||||
*/
|
||||
export const axumRule: LibraryRule = {
|
||||
id: 'axum',
|
||||
label: 'axum 接口',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['axum'],
|
||||
claim: (block) => {
|
||||
if (block.kind === 'function') {
|
||||
const route = attributeRoute(block)
|
||||
return route ? card(block, route) : undefined
|
||||
}
|
||||
if (block.kind === 'statement') {
|
||||
const [route] = routesOn(block.node).filter(item => !handled(block.node, item.handler))
|
||||
return route ? routerCard(block.node, route) : undefined
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'axum.route': '接口' } } }),
|
||||
}
|
||||
|
||||
interface Route {
|
||||
method: string
|
||||
path: string
|
||||
handler?: string
|
||||
}
|
||||
|
||||
function attributeRoute(block: FunctionBlock): Route | undefined {
|
||||
for (const attr of block.facts.decorators) {
|
||||
const method = attributeName(attr)?.split('::').pop()
|
||||
if (!method || !METHODS.has(method))
|
||||
continue
|
||||
const path = attributeString(attr)
|
||||
if (path)
|
||||
return { method: method.toUpperCase(), path }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function card(block: FunctionBlock, route: Route): RuleCard {
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const params = pathParams(route.path, block.node)
|
||||
const rows: PropertyRow[] = [
|
||||
nameRow(block.facts.name),
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||
requestRow(route, params),
|
||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
||||
]
|
||||
return {
|
||||
kind: 'axum.route',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一文件里的处理函数已经标了路由属性时,Router 上这句不再单独成卡。 */
|
||||
function handled(node: Node, handler: string | undefined): boolean {
|
||||
if (!handler)
|
||||
return false
|
||||
const root = node.tree.rootNode
|
||||
for (const item of named(root)) {
|
||||
if (item.type !== 'function_item' || item.childForFieldName('name')?.text !== handler)
|
||||
continue
|
||||
return itemAttributesHasRoute(item)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function routerCard(node: Node, route: Route & { handlerNode?: Node }): RuleCard {
|
||||
const fn = route.handler ? functionNamed(node, route.handler) : undefined
|
||||
const params = fn ? pathParams(route.path, fn) : []
|
||||
const doc = fn ? docSentence(leadingDoc(fn)) : undefined
|
||||
return {
|
||||
kind: 'axum.route',
|
||||
badge: BADGE,
|
||||
rows: [
|
||||
{ label: '名称', value: route.handler ? [plain(route.handler)] : [hint('路由')] },
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
requestRow(route, params),
|
||||
{ label: '返回', value: [plain(fn ? returnText(fn) : '—')] },
|
||||
],
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
function requestRow(route: Route, params: string[]): PropertyRow {
|
||||
const items: PropertyItem[] = [
|
||||
{ cells: [[plain('方法')], [keyword(route.method)]] },
|
||||
{ cells: [[plain('路径')], [{ text: route.path, role: 'string' }]] },
|
||||
...(params.length > 0 ? [{ cells: [[plain('路径参数')], params.map(text => plain(text)).flatMap((part, index) => index === 0 ? [part] : [plain(', '), part])] }] : []),
|
||||
]
|
||||
return {
|
||||
label: '请求',
|
||||
value: [keyword(route.method), plain(` ${route.path}`)],
|
||||
columns: ['项', '值'],
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
function routesOn(node: Node, found: (Route & { handlerNode?: Node })[] = []): (Route & { handlerNode?: Node })[] {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function')
|
||||
const field = callee?.type === 'field_expression' ? callee.childForFieldName('field')?.text : undefined
|
||||
if (field === 'route') {
|
||||
const args = named(node.childForFieldName('arguments'))
|
||||
const pathNode = args.find(arg => arg.type === 'string_literal')
|
||||
const call = args.find(arg => arg.type === 'call_expression')
|
||||
const method = call?.childForFieldName('function')?.text.split('::').pop()
|
||||
const handlerNode = named(call?.childForFieldName('arguments'))[0]
|
||||
const path = stringContent(pathNode)
|
||||
if (path && method && METHODS.has(method)) {
|
||||
found.push({
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
...(handlerNode?.type === 'identifier' && { handler: handlerNode.text, handlerNode }),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of named(node))
|
||||
routesOn(child, found)
|
||||
return found
|
||||
}
|
||||
|
||||
/** 路径里的 {名字} 对上参数。Path(id): Path<Uuid> 写成 id: Uuid。 */
|
||||
function pathParams(path: string, fn: Node): string[] {
|
||||
const names = [...path.matchAll(/\{(\w+)\}/g)].map(match => match[1] ?? '')
|
||||
return names.flatMap((name) => {
|
||||
const param = parameterNamed(fn, name)
|
||||
if (!param)
|
||||
return [`${name}`]
|
||||
const type = innerType(param.childForFieldName('type'))
|
||||
return [type ? `${name}: ${type}` : name]
|
||||
})
|
||||
}
|
||||
|
||||
function parameterNamed(fn: Node, name: string): Node | undefined {
|
||||
const list = fn.childForFieldName('parameters')
|
||||
return named(list).find((param) => {
|
||||
if (param.type !== 'parameter' && param.type !== 'self_parameter')
|
||||
return false
|
||||
const pattern = param.childForFieldName('pattern') ?? param
|
||||
return namedDeep(pattern).some(node => (node.type === 'identifier' || node.type === 'self') && node.text === name)
|
||||
})
|
||||
}
|
||||
|
||||
function innerType(type: Node | null): string | undefined {
|
||||
if (!type)
|
||||
return undefined
|
||||
if (type.type === 'generic_type' && type.childForFieldName('type')?.text === 'Path') {
|
||||
const arg = named(type.childForFieldName('type_arguments'))[0]
|
||||
if (arg)
|
||||
return oneLine(arg.text)
|
||||
}
|
||||
return oneLine(type.text)
|
||||
}
|
||||
|
||||
function itemAttributesHasRoute(fn: Node): boolean {
|
||||
let prev = fn.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item') {
|
||||
const method = attributeName(prev)?.split('::').pop()
|
||||
if (method && METHODS.has(method))
|
||||
return true
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function functionNamed(node: Node, name: string): Node | undefined {
|
||||
return named(node.tree.rootNode).find(item => item.type === 'function_item' && item.childForFieldName('name')?.text === name)
|
||||
}
|
||||
|
||||
function leadingDoc(fn: Node): Node | undefined {
|
||||
let prev = fn.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item')
|
||||
prev = prev.previousNamedSibling
|
||||
return prev && (prev.type === 'line_comment' || prev.type === 'block_comment') ? prev : undefined
|
||||
}
|
||||
|
||||
function attributeString(attributeItem: Node): string | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
const tree = attribute?.children.find(child => child?.type === 'token_tree')
|
||||
return stringContent(tree?.children.find(child => child?.type === 'string_literal') ?? undefined)
|
||||
}
|
||||
|
||||
function namedDeep(node: Node): Node[] {
|
||||
return [node, ...named(node).flatMap(namedDeep)]
|
||||
}
|
||||
|
||||
function nameRow(name: Node | undefined): PropertyRow {
|
||||
return {
|
||||
label: '名称',
|
||||
value: name ? [{ text: name.text, role: 'function', offset: name.startIndex }] : [hint('匿名')],
|
||||
...(name && { defines: range(name) }),
|
||||
}
|
||||
}
|
||||
|
||||
function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function plain(text: string): Inline {
|
||||
return { text, role: 'plain' }
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function range(node: Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
@@ -64,7 +64,7 @@ describe('react 库规则:认领', () => {
|
||||
expect(propertyLines(properties(view)).slice(0, 4)).toEqual([
|
||||
'名称: DesktopTitlebar ⟦具名导出|⟧ ⟦客户端组件|⟧',
|
||||
'说明: 窗口顶部 40px. 整条连同里面的文字都是拖拽区 (data-tauri-drag-region="deep"), 按钮除外. 侧栏开关只在侧栏顶部.',
|
||||
'属性: 1 个',
|
||||
'构造参数: 1 个',
|
||||
' platform | DesktopShell["platform"] | — | 是',
|
||||
])
|
||||
})
|
||||
@@ -83,13 +83,13 @@ describe('react 库规则:认领', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('react 库规则:属性', () => {
|
||||
describe('react 库规则:构造参数', () => {
|
||||
it('luffy 的 LegalDocPage:来自 LegalDocPageProps(名字能跳到定义),字段带说明时多一列说明', async () => {
|
||||
const view = await card(LEGAL, 'LegalDocPage')
|
||||
const row = properties(view).find(item => item.label === '属性')
|
||||
const row = properties(view).find(item => item.label === '构造参数')
|
||||
expect(row?.columns).toEqual(['名称', '类型', '默认值', '必填', '说明'])
|
||||
expect(rowLines(view, '属性')).toEqual([
|
||||
'属性: 1 个 · 来自 LegalDocPageProps',
|
||||
expect(rowLines(view, '构造参数')).toEqual([
|
||||
'构造参数: 1 个 · 来自 LegalDocPageProps',
|
||||
' doc | "terms" | "privacy" | — | 是 | terms = 用户服务协议; privacy = 隐私政策',
|
||||
])
|
||||
const from = row && !('markdown' in row.value) ? row.value.at(-1) : undefined
|
||||
@@ -98,16 +98,16 @@ describe('react 库规则:属性', () => {
|
||||
})
|
||||
|
||||
it('luffy 的 ShareButton:回调标「回调」、类型写成函数签名;StepLabel 的 children 标「子元素」', async () => {
|
||||
expect(rowLines(await card(SHARE, 'ShareButton'), '属性')).toEqual([
|
||||
'属性: 5 个',
|
||||
expect(rowLines(await card(SHARE, 'ShareButton'), '构造参数')).toEqual([
|
||||
'构造参数: 5 个',
|
||||
' open | 布尔 | — | 是 | ',
|
||||
' onOpenChange ⟦回调|⟧ | (next: 布尔) => 无 | — | 是 | ',
|
||||
' deployUrl | 字符串 | 空 | — | 是 | 已发布的持久 URL; null = 尚未发布 (点击后由父组件驱动发布)。',
|
||||
' sessionId | 字符串 | — | 是 | ',
|
||||
' title | 字符串 | — | 是 | ',
|
||||
])
|
||||
expect(rowLines(await card(POSTER, 'StepLabel'), '属性')).toEqual([
|
||||
'属性: 2 个',
|
||||
expect(rowLines(await card(POSTER, 'StepLabel'), '构造参数')).toEqual([
|
||||
'构造参数: 2 个',
|
||||
' n | 数值 | — | 是',
|
||||
' children ⟦子元素|⟧ | ReactNode | — | 是',
|
||||
])
|
||||
@@ -118,8 +118,8 @@ describe('react 库规则:属性', () => {
|
||||
'interface Props { size?: number; onPick(v: string): void; label: string }',
|
||||
'export const A = ({ size = 16, label: text, ...rest }: Props) => <i />;',
|
||||
].join('\n')
|
||||
expect(rowLines(await card(source, 'A'), '属性')).toEqual([
|
||||
'属性: 4 个 · 来自 Props',
|
||||
expect(rowLines(await card(source, 'A'), '构造参数')).toEqual([
|
||||
'构造参数: 4 个 · 来自 Props',
|
||||
' size | 数值 | 16 | 否',
|
||||
' onPick ⟦回调|⟧ | (v: 字符串) => 无 | — | 是',
|
||||
' label(组件里叫 text) | 字符串 | — | 是',
|
||||
@@ -133,8 +133,8 @@ describe('react 库规则:属性', () => {
|
||||
'export const Card = ({ title, onClose = noop }: CardProps) => <div />;',
|
||||
'export function Empty() { return <i /> }',
|
||||
].join('\n')
|
||||
expect(rowLines(await card(source, 'Card'), '属性')).toEqual([
|
||||
'属性: 2 个 · 来自 CardProps',
|
||||
expect(rowLines(await card(source, 'Card'), '构造参数')).toEqual([
|
||||
'构造参数: 2 个 · 来自 CardProps',
|
||||
' title | 未标注 | — | —',
|
||||
' onClose | 未标注 | noop | 否',
|
||||
])
|
||||
@@ -143,13 +143,13 @@ describe('react 库规则:属性', () => {
|
||||
{ name: 'onClose', type: '() => void', optional: true, callback: true },
|
||||
{ name: 'size', type: 'number', optional: true, callback: false },
|
||||
])
|
||||
expect(rowLines(await card(source, 'Card', known({ CardProps: fields })), '属性')).toEqual([
|
||||
'属性: 3 个 · 来自 CardProps',
|
||||
expect(rowLines(await card(source, 'Card', known({ CardProps: fields })), '构造参数')).toEqual([
|
||||
'构造参数: 3 个 · 来自 CardProps',
|
||||
' title | string | — | 是 | 标题',
|
||||
' onClose ⟦回调|⟧ | () => void | noop | 否 | ',
|
||||
' size | number | — | 否 | ',
|
||||
])
|
||||
expect(rowLines(await card(source, 'Empty'), '属性')).toEqual(['属性: 无'])
|
||||
expect(rowLines(await card(source, 'Empty'), '构造参数')).toEqual(['构造参数: 无'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -257,11 +257,37 @@ describe('react 库规则:分区', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('单独打开一段源码', () => {
|
||||
it('读取的数据挂的是这次调用,改它的里 JSX 那一处是整个属性', async () => {
|
||||
const source = [
|
||||
'export function Panel() {',
|
||||
' const pathname =',
|
||||
' usePathname()',
|
||||
' const [open, setOpen] = useState(false)',
|
||||
' return (',
|
||||
' <Dialog',
|
||||
' onOpenChange={',
|
||||
' setOpen',
|
||||
' }',
|
||||
' />',
|
||||
' )',
|
||||
'}',
|
||||
].join('\n')
|
||||
const view = await card(source, 'Panel')
|
||||
const read = properties(view).find(row => row.label === '读取的数据')?.items?.[0]
|
||||
expect(read?.edits?.map(edit => edit.snippet.code)).toEqual(['usePathname()'])
|
||||
const state = properties(view).find(row => row.label === '状态')?.items?.[0]
|
||||
expect(state?.edits?.map(edit => edit.snippet.code)).toEqual(['onOpenChange={\n setOpen\n}'])
|
||||
expect(view.code).toContain('function Panel')
|
||||
expect(read?.edits?.[0].snippet.code).not.toContain('function Panel')
|
||||
})
|
||||
})
|
||||
|
||||
describe('react 库规则:渲染大纲', () => {
|
||||
it('luffy 的 DesktopTitlebar:整张卡片讲完了组件,没有留下的子块;渲染大纲每行一个元素,原生标签、组件、样式小标、属性、插值', async () => {
|
||||
const view = await card(TITLEBAR, 'DesktopTitlebar')
|
||||
expect(view.children).toEqual([])
|
||||
expect(properties(view).map(row => row.label)).toEqual(['名称', '说明', '属性', '读取的数据', '派生值', '渲染'])
|
||||
expect(properties(view).map(row => row.label)).toEqual(['名称', '说明', '构造参数', '读取的数据', '派生值', '渲染'])
|
||||
expect(rowLines(view, '渲染')).toEqual([
|
||||
'渲染: 1 处返回',
|
||||
' div ⟦样式 8 个类|⟧ data-tauri-drag-region="deep"',
|
||||
@@ -388,7 +414,7 @@ describe('react 库规则:代码差异', () => {
|
||||
|
||||
const componentChange = diff.blocks[component.anchor]
|
||||
expect(componentChange).toMatchObject({ change: 'mod', own: true })
|
||||
const propsIndex = properties(component).findIndex(row => row.label === '属性')
|
||||
const propsIndex = properties(component).findIndex(row => row.label === '构造参数')
|
||||
expect(Object.keys(componentChange.marks?.rows?.changed ?? {})).toEqual([String(propsIndex)])
|
||||
expect(componentChange.marks?.rows?.changed[propsIndex].items?.changed[1]).toEqual({ added: true })
|
||||
expect(componentChange.marks?.rows?.removed).toEqual([])
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, RuleSlot, SegmentGroup, ThemeColor, TokenRole, WorkspaceIndex } from '../contract'
|
||||
import type { CellEdit, CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, RuleSlot, SegmentGroup, ThemeColor, TokenRole, WorkspaceIndex } from '../contract'
|
||||
import type { BindingFacts, BodyFacts, ClassFacts, ComponentFacts, DerivedFacts, EffectFacts, FieldFacts, FieldText, HandlerFacts, OutlineNode, Place, PropsFacts, ReadFacts } from './reactTree'
|
||||
import { argumentsOf, attributeName, attributeValue, bodyOf, clientDirective, commentsBefore, commentText, componentAt, componentOf, componentsIn, declaredTypes, enclosingComponent, isComponentTag, isFunction, named, outlineOf, parametersOf, placesOf, propsOf, sameNode, statementsOf, stringContent, unwrap } from './reactTree'
|
||||
import { hint, joined, name, plain, range } from './segments'
|
||||
|
||||
/**
|
||||
* React 库规则:TSX / JSX 里的 React 函数组件(见 reactTree.ts 的 componentAt)整块换成「React 组件」卡片,不再是普通函数卡片的输入参数 / 输出参数。
|
||||
* 卡片:名称(后面跟导出方式、客户端组件、记忆化、转发 ref 这些小标签)/ 说明 / 属性(解构的每个属性一行,类型取属性类型里对应的字段;
|
||||
* 卡片:名称(后面跟导出方式、客户端组件、记忆化、转发 ref 这些小标签)/ 说明 / 构造参数(解构的每个属性一行,类型取属性类型里对应的字段;
|
||||
* 属性类型是接口、类型别名时写来自哪个类型,导入的类型经工作区索引查它的字段),再按钩子分区,没有就不画:
|
||||
* 状态(useState / useReducer:名称 / 初始值 / 改它的,渲染时按条件重置状态的 if 也写进改它的)、引用(useRef)、
|
||||
* 读取的数据(别的钩子:变量 / 来自 / 意思)、派生值(组件体里别的常量、useMemo / useCallback:名称 / 怎么算 / 什么时候算)、
|
||||
@@ -123,7 +123,7 @@ const ZH: Words = {
|
||||
labels: {
|
||||
name: '名称',
|
||||
doc: '说明',
|
||||
props: '属性',
|
||||
props: '构造参数',
|
||||
state: '状态',
|
||||
refs: '引用',
|
||||
reads: '读取的数据',
|
||||
@@ -254,7 +254,7 @@ function docRows(component: ComponentFacts, { context, words }: Env): PropertyRo
|
||||
return doc ? [{ label: words.labels.doc, value: doc }] : []
|
||||
}
|
||||
|
||||
// ─── 属性 ─────────────────────────────────────────────────────────────────────
|
||||
// ─── 构造参数 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 工作区里各文件顶层声明的接口、类型别名:名字 → 各字段的文字(JSON,见 FieldText),组件的属性类型是导入的时候查它。 */
|
||||
const PROP_TYPES_INDEX: WorkspaceIndex = {
|
||||
@@ -357,7 +357,7 @@ function propDoc(entry: PropEntry): Inline[] {
|
||||
return text ? [{ text: text.replace(/\s*\n\s*/g, ' '), role: 'comment' }] : []
|
||||
}
|
||||
|
||||
/** 属性:解构的每个属性一行(名称 / 类型 / 默认值 / 必填,有属性带说明时多一列说明);属性类型是一个名字时「属性」后面写来自哪里。 */
|
||||
/** 构造参数:解构的每个属性一行(名称 / 类型 / 默认值 / 必填,有属性带说明时多一列说明);属性类型是一个名字时「构造参数」后面写来自哪里。 */
|
||||
function propsRow(component: ComponentFacts, env: Env): PropertyRow {
|
||||
const { context, words } = env
|
||||
const props = propsOf(component)
|
||||
@@ -487,16 +487,54 @@ function commentLine(comments: readonly Node[]): string | undefined {
|
||||
// ─── 状态、引用、读取的数据、派生值 ──────────────────────────────────────────
|
||||
|
||||
/** 状态:名称 / 初始值 / 改它的(改状态的函数,括号里写在哪儿改:副作用、处理函数、渲染时按条件重置……)。 */
|
||||
/** 这一处改状态的源码:JSX 取包着值的整个属性,处理函数取函数,派生值取那个值,另一处读取取那次调用,重置取条件。纯渲染没有节点。 */
|
||||
function placeNode(place: Place, body: BodyFacts): Node | undefined {
|
||||
switch (place.kind) {
|
||||
case 'jsx':
|
||||
return place.attribute.parent?.type === 'jsx_attribute' ? place.attribute.parent : undefined
|
||||
case 'handler':
|
||||
return body.handlers.find(handler => sameNode(handler.name, place.name))?.fn
|
||||
case 'derived':
|
||||
return body.derived.find(item => sameNode(item.name, place.name))?.value
|
||||
case 'read':
|
||||
return body.reads.find(read => sameNode(read.target, place.target))?.call
|
||||
case 'reset':
|
||||
return place.condition
|
||||
case 'effect':
|
||||
return body.effects[place.index]?.statement
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 「改它的」这一格:函数名,括号里每一处各挂自己的源码。 */
|
||||
function changesOf(analysis: Analysis, setter: Node, env: Env): { cell: Inline[], edits: CellEdit[] } {
|
||||
const places = placesOf(setter, analysis.component, analysis.body)
|
||||
const parts = places.map(place => placeText(place, analysis, env))
|
||||
const cell = [name(setter, 'function'), ...inParens(parts, env)]
|
||||
const edits: CellEdit[] = []
|
||||
let at = 2
|
||||
parts.forEach((part, index) => {
|
||||
const node = placeNode(places[index], analysis.body)
|
||||
if (node)
|
||||
edits.push({ cell: 2, from: at, to: at + part.length, snippet: env.context.snippet(node) })
|
||||
at += part.length + (index < parts.length - 1 ? 1 : 0)
|
||||
})
|
||||
return { cell, edits }
|
||||
}
|
||||
|
||||
function stateRow(analysis: Analysis, env: Env): PropertyRow {
|
||||
const { component, body } = analysis
|
||||
const { body } = analysis
|
||||
const { context, words } = env
|
||||
const items = body.states.map((state): PropertyItem => {
|
||||
const initial = state.initial ? context.translate(state.initial) : [hint(words.nothing)]
|
||||
const reducer = state.reducer ? [hint(words.parens[0]), ...context.translate(state.reducer), hint(words.parens[1])] : []
|
||||
const changes = state.setter
|
||||
? [name(state.setter, 'function'), ...inParens(placesOf(state.setter, component, body).map(place => placeText(place, analysis, env)), env)]
|
||||
: [hint(words.nothing)]
|
||||
return { cells: [binding(state.value, 'variable', env), [...initial, ...reducer], changes], defines: range(state.value) }
|
||||
const changes = state.setter ? changesOf(analysis, state.setter, env) : { cell: [hint(words.nothing)], edits: [] }
|
||||
return {
|
||||
cells: [binding(state.value, 'variable', env), [...initial, ...reducer], changes.cell],
|
||||
...(changes.edits.length > 0 && { edits: changes.edits }),
|
||||
defines: range(state.value),
|
||||
}
|
||||
})
|
||||
return { label: words.labels.state, value: [hint(words.count(items.length))], items, columns: words.heads.state }
|
||||
}
|
||||
@@ -601,10 +639,14 @@ function readMeaning(read: ReadFacts, env: Env): Inline[] {
|
||||
/** 读取的数据:变量 / 来自(钩子调用照常翻)/ 意思。 */
|
||||
function readRow(analysis: Analysis, env: Env): PropertyRow {
|
||||
const { context, words } = env
|
||||
const items = analysis.body.reads.map((read): PropertyItem => ({
|
||||
cells: [binding(read.target, 'variable', env), context.translate(read.call), readMeaning(read, env)],
|
||||
defines: range(read.target),
|
||||
}))
|
||||
const items = analysis.body.reads.map((read): PropertyItem => {
|
||||
const from = context.translate(read.call)
|
||||
return {
|
||||
cells: [binding(read.target, 'variable', env), from, readMeaning(read, env)],
|
||||
edits: [{ cell: 1, from: 0, to: from.length, snippet: context.snippet(read.call) }],
|
||||
defines: range(read.target),
|
||||
}
|
||||
})
|
||||
return { label: words.labels.reads, value: [hint(words.count(items.length))], items, columns: words.heads.reads }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CodeBlock, Node } from '../contract'
|
||||
|
||||
/**
|
||||
* 读 React 函数组件的语法树(React 库规则用,见 react.ts):认组件,读出它的属性、组件体里的各种钩子、处理函数和渲染的 JSX。
|
||||
* 读 React 函数组件的语法树(React 库规则用,见 react.ts):认组件,读出它的构造参数、组件体里的各种钩子、处理函数和渲染的 JSX。
|
||||
* 只读语法树、不翻译,读出来的节点都指向源码;怎么排成卡片、用什么词归 react.ts。
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ImplBlock, KindBadge, LibraryRule, RuleCard, ThemeColor } from '../contract'
|
||||
import { implRows, plainSay } from './rustRows'
|
||||
|
||||
/** 和函数卡同一个黄,实现块看起来仍是函数那一挂。 */
|
||||
const COLOR: ThemeColor = { dark: '#dcdcaa', light: '#795e26' }
|
||||
const BADGE: KindBadge = { label: '实现', color: COLOR, icon: 'square-function' }
|
||||
|
||||
/**
|
||||
* impl 的方法表:点方法名展开那张函数卡。
|
||||
* 子块放进属性表某一行,和 React 事件处理同一条路。不看导入,Rust 文件都启用。
|
||||
*/
|
||||
export const rustImplRule: LibraryRule = {
|
||||
id: 'rust',
|
||||
label: 'Rust 实现',
|
||||
languageIds: ['rust'],
|
||||
libraries: [],
|
||||
claim: block => block.kind === 'impl' ? implCard(block) : undefined,
|
||||
attributeNames: () => ({
|
||||
attributes: {},
|
||||
values: { kind: { 'rust.impl': '实现' } },
|
||||
}),
|
||||
}
|
||||
|
||||
function implCard(block: ImplBlock): RuleCard {
|
||||
const rows = implRows(block, plainSay())
|
||||
const row = rows.findIndex(item => item.label === '方法')
|
||||
const methods = block.children.filter(child => child.kind === 'function')
|
||||
return {
|
||||
kind: 'rust.impl',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: { startIndex: block.facts.name.startIndex, endIndex: block.facts.name.endIndex } }),
|
||||
absorbs: [],
|
||||
...(row >= 0 && { slots: methods.map((child, item) => ({ block: child, row, item })) }),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { blockMarks } from '../diff/viewDiff'
|
||||
import { createRustAdapter } from '../languages/rust'
|
||||
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { rustZhTranslator } from '../translators/rustZh'
|
||||
import { axumRule } from './axum'
|
||||
import { rustImplRule } from './rustImpl'
|
||||
import { serdeRule } from './serde'
|
||||
import { sqlxRule } from './sqlx'
|
||||
import { tauriRule } from './tauri'
|
||||
import { thiserrorRule } from './thiserror'
|
||||
|
||||
const adapter = createRustAdapter(WASM_DIR)
|
||||
const rules = [rustImplRule, serdeRule, thiserrorRule, axumRule, sqlxRule, tauriRule]
|
||||
|
||||
function views(source: string): Promise<BlockView[]> {
|
||||
return withBlocks(adapter, 'rust', source, blocks =>
|
||||
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
|
||||
rules,
|
||||
report: (message) => {
|
||||
throw new Error(message)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
describe('serde 与 thiserror', () => {
|
||||
it('可序列化的结构体、错误枚举', async () => {
|
||||
expect(viewLines(await views(readFixture('rustLibs.rs')))).toEqual([
|
||||
'import 1',
|
||||
' 来源: serde',
|
||||
' 导入项: 2 个',
|
||||
' Deserialize | —',
|
||||
' Serialize | —',
|
||||
'serde.struct 3',
|
||||
' 名称: AgentRow ⟦可序列化|⟧',
|
||||
' 说明: 桌面端列表里的一行代理。',
|
||||
' 可见性: 公开',
|
||||
' 选项: 字段名改驼峰',
|
||||
' 字段: 3 个',
|
||||
' id | Uuid | 公开 | 改名 agent_id',
|
||||
' name | String | 公开 | —',
|
||||
' secret | String | 本模块 | 不输出',
|
||||
'import 14',
|
||||
' 来源: thiserror',
|
||||
' 导入项: 1 个',
|
||||
' Error | —',
|
||||
'thiserror.error 16',
|
||||
' 名称: AgentError',
|
||||
' 说明: 读写代理时出的错。',
|
||||
' 变体: 2 个',
|
||||
' NotFound | 这个 id 没有代理 | agent {id} 不存在',
|
||||
' Upstream | 调用上游失败 | 上游错误 ⟦透传来源|⟧',
|
||||
'import 27',
|
||||
' 来源: axum::routing',
|
||||
' 导入项: 1 个',
|
||||
' get | —',
|
||||
'axum.route 29',
|
||||
' 名称: get_agent',
|
||||
' 说明: 按 id 取一个代理。',
|
||||
' 异步: 是',
|
||||
' 请求: GET /agents/{id}',
|
||||
' 方法 | GET',
|
||||
' 路径 | /agents/{id}',
|
||||
' 路径参数 | id: Uuid',
|
||||
' 返回: Result<Json<AgentRow>, StatusCode>',
|
||||
'axum.route 35',
|
||||
' 名称: health',
|
||||
' 请求: GET /health',
|
||||
' 方法 | GET',
|
||||
' 路径 | /health',
|
||||
' 返回: —',
|
||||
'import 37',
|
||||
' 来源: sqlx',
|
||||
' 导入项: 1 个',
|
||||
' query_as | —',
|
||||
'sqlx.query 40',
|
||||
' 说明: 按 id 取一行。',
|
||||
' SQL: SELECT id, name FROM agents WHERE id = $1',
|
||||
' 参数: $1 = id',
|
||||
' 返回类型: AgentRow',
|
||||
'import 42',
|
||||
' 来源: tauri',
|
||||
' 导入项: 1 个',
|
||||
' State | —',
|
||||
'tauri.command 44',
|
||||
' 名称: rename_agent ⟦桌面命令|⟧',
|
||||
' 说明: 让窗口把这一行改名。',
|
||||
' 异步: 是',
|
||||
' 参数: 2 个',
|
||||
' state | State<App> | 窗口状态',
|
||||
' name | String | 调用方传入',
|
||||
' 返回: Result<String, String>',
|
||||
])
|
||||
})
|
||||
|
||||
it('没导入 serde 时,带 derive 的结构体仍是结构体', async () => {
|
||||
const source = '#[derive(Serialize)]\npub struct AgentRow {\n name: String,\n}\n'
|
||||
const [card] = await views(source)
|
||||
expect(card?.kind).toBe('class')
|
||||
})
|
||||
|
||||
it('改 axum 的路径,路径那一格标成改过', async () => {
|
||||
const before = 'use axum::routing::get;\n#[get("/agents/{id}")]\nasync fn get_agent(Path(id): Path<Uuid>) -> Result<Json<AgentRow>, StatusCode> {}\n'
|
||||
const after = before.replace('/agents/{id}', '/people/{id}')
|
||||
const oldCard = (await views(before)).find(block => block.kind === 'axum.route')
|
||||
const newCard = (await views(after)).find(block => block.kind === 'axum.route')
|
||||
const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined
|
||||
const row = newCard && 'properties' in newCard ? newCard.properties.findIndex(item => item.label === '请求') : -1
|
||||
const parts = marks?.rows?.changed[row]?.items?.changed[1]?.cells?.[1] ?? []
|
||||
const text = (change: 'add' | 'del') => parts
|
||||
.filter(part => part.change === change)
|
||||
.flatMap(part => part.segments)
|
||||
.map(segment => 'text' in segment ? segment.text : '')
|
||||
.join('')
|
||||
expect(text('del')).toContain('/agents/{id}')
|
||||
expect(text('add')).toContain('/people/{id}')
|
||||
})
|
||||
|
||||
it('skip_serializing 也是不输出', async () => {
|
||||
const source = 'use serde::Serialize;\n#[derive(Serialize)]\nstruct Row {\n #[serde(skip_serializing)]\n token: String,\n}\n'
|
||||
const lines = viewLines(await views(source))
|
||||
expect(lines).toContain(' token | String | 本模块 | 不输出')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, TokenRole, TraitBlock } from '../contract'
|
||||
import type { RustVisibility } from './rustTree'
|
||||
import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
|
||||
|
||||
/** 把类型、名字画成原文。翻译插件传入时,名字带源码下标。 */
|
||||
export interface RustSay {
|
||||
name: (node: import('../contract').Node, role: TokenRole) => Inline[]
|
||||
source: (node: import('../contract').Node, role?: TokenRole) => Inline[]
|
||||
}
|
||||
|
||||
const VISIBILITY: Record<RustVisibility, string> = {
|
||||
public: '公开',
|
||||
module: '本模块',
|
||||
super: '父模块',
|
||||
crate: '本 crate',
|
||||
}
|
||||
|
||||
/** 块上的可见性:属性盖过 facts,没写是本模块。 */
|
||||
export function visibilityWord(block: CodeBlock): string {
|
||||
const overridden = block.attributes?.access
|
||||
if (overridden && overridden in VISIBILITY)
|
||||
return VISIBILITY[overridden as RustVisibility]
|
||||
if ('facts' in block && 'access' in block.facts && block.facts.access === 'public')
|
||||
return VISIBILITY.public
|
||||
return VISIBILITY.module
|
||||
}
|
||||
|
||||
export function importRows(block: ImportBlock, say: RustSay): PropertyRow[] {
|
||||
const [source] = block.facts.sources
|
||||
return [
|
||||
{ label: '来源', value: source ? say.source(source, 'type') : [hint('—')] },
|
||||
{
|
||||
label: '导入项',
|
||||
value: [hint(`${block.facts.items.length} 个`)],
|
||||
columns: ['名字', '别名'],
|
||||
items: block.facts.items.map((item) => {
|
||||
if (item.kind === 'namespace')
|
||||
return { cells: [[hint('*')], [hint('—')]] }
|
||||
if (item.kind !== 'named')
|
||||
return { cells: [[hint('—')], [hint('—')]] }
|
||||
return {
|
||||
cells: [say.name(item.name, 'type'), item.alias ? say.name(item.alias, 'type') : [hint('—')]],
|
||||
defines: range(item.name),
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] {
|
||||
const fields = structFields(block.node)
|
||||
return [
|
||||
nameRow(block.facts.name, say, 'type'),
|
||||
...docRow(block.facts.doc),
|
||||
visibilityRow(block),
|
||||
...tableRow('字段', ['字段', '类型', '可见性'], fields.map(field => ({
|
||||
cells: [
|
||||
say.name(field.name, 'variable'),
|
||||
say.source(field.type, 'type'),
|
||||
[keyword(VISIBILITY[field.visibility])],
|
||||
],
|
||||
defines: range(field.name),
|
||||
}))),
|
||||
]
|
||||
}
|
||||
|
||||
export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] {
|
||||
return [
|
||||
nameRow(block.facts.name, say, 'variable'),
|
||||
...docRow(block.facts.doc),
|
||||
visibilityRow(block),
|
||||
]
|
||||
}
|
||||
|
||||
export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
|
||||
return [
|
||||
nameRow(block.facts.name, say, 'type'),
|
||||
...docRow(block.facts.doc),
|
||||
...tableRow('变体', ['变体', '携带的数据'], block.facts.members.map(member => ({
|
||||
cells: [say.name(member.name, 'type'), [hint(variantPayload(member.value) ?? '—')]],
|
||||
defines: range(member.name),
|
||||
}))),
|
||||
]
|
||||
}
|
||||
|
||||
export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] {
|
||||
const arms = matchArms(block.node)
|
||||
return [
|
||||
nameRow(block.facts.name, say, 'function'),
|
||||
...docRow(block.facts.doc),
|
||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||
visibilityRow(block),
|
||||
...tableRow(isMethod(block.node) ? '参数' : '输入参数', ['名称', '类型'], block.facts.parameters.map(param => ({
|
||||
cells: [say.name(param.name, 'variable'), param.type ? say.source(param.type, 'type') : [hint('—')]],
|
||||
defines: range(param.name),
|
||||
}))),
|
||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
||||
...tableRow('分情况', ['分支', '做什么'], arms.map(arm => ({
|
||||
cells: [[plain(arm.pattern)], [hint(arm.does)]],
|
||||
})), '支'),
|
||||
]
|
||||
}
|
||||
|
||||
export function traitRows(block: TraitBlock, say: RustSay): PropertyRow[] {
|
||||
const methods = bodyFunctions(block.node)
|
||||
return [
|
||||
nameRow(block.facts.name, say, 'type'),
|
||||
...docRow(block.facts.doc),
|
||||
...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => {
|
||||
const name = method.childForFieldName('name')
|
||||
return {
|
||||
cells: [
|
||||
name ? say.name(name, 'function') : [hint('匿名')],
|
||||
[plain(signatureParams(method))],
|
||||
[plain(returnText(method))],
|
||||
],
|
||||
...(name && { defines: range(name) }),
|
||||
}
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
export function implRows(block: ImplBlock, say: RustSay): PropertyRow[] {
|
||||
const methods = block.children.filter((child): child is FunctionBlock => child.kind === 'function')
|
||||
const title: Inline[] = block.facts.name
|
||||
? [...say.name(block.facts.name, 'type'), plain(' 的方法')]
|
||||
: [hint('实现')]
|
||||
return [
|
||||
{ label: '名称', value: title, ...(block.facts.name && { defines: range(block.facts.name) }) },
|
||||
...docRow(block.facts.doc),
|
||||
...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => {
|
||||
const name = method.facts.name
|
||||
return {
|
||||
cells: [
|
||||
name ? say.name(name, 'function') : [hint('匿名')],
|
||||
[plain(signatureParams(method.node))],
|
||||
[plain(returnText(method.node))],
|
||||
],
|
||||
...(name && { defines: range(name) }),
|
||||
}
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
function nameRow(name: import('../contract').Node | undefined, say: RustSay, role: TokenRole): PropertyRow {
|
||||
return {
|
||||
label: '名称',
|
||||
value: name ? say.name(name, role) : [hint('匿名')],
|
||||
...(name && { defines: range(name) }),
|
||||
}
|
||||
}
|
||||
|
||||
function docRow(doc: import('../contract').Node | undefined): PropertyRow[] {
|
||||
const sentence = docSentence(doc)
|
||||
return sentence ? [{ label: '说明', value: { markdown: sentence } }] : []
|
||||
}
|
||||
|
||||
function visibilityRow(block: CodeBlock): PropertyRow {
|
||||
return { label: '可见性', value: [keyword(visibilityWord(block))] }
|
||||
}
|
||||
|
||||
/** 没有行就不占一行。countUnit 默认「个」,分情况用「支」。 */
|
||||
function tableRow(label: string, columns: string[], items: PropertyItem[], countUnit = '个'): PropertyRow[] {
|
||||
if (items.length === 0)
|
||||
return []
|
||||
return [{ label, value: [hint(`${items.length} ${countUnit}`)], columns, items }]
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function plain(text: string): Inline {
|
||||
return { text, role: 'plain' }
|
||||
}
|
||||
|
||||
function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function range(node: import('../contract').Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
/** 名字、类型照抄源码,名字带上下标,点了能跳到定义。 */
|
||||
export function plainSay(): RustSay {
|
||||
return {
|
||||
name: (node, role) => [{ text: node.text, role, offset: node.startIndex }],
|
||||
source: (node, role = 'type') => [{ text: oneLine(node.text), role, offset: node.startIndex }],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { Access, BlockAttributes, Node } from '../contract'
|
||||
|
||||
/** 具名子节点,空的跳过。 */
|
||||
export function named(node: Node | null | undefined): Node[] {
|
||||
return node?.namedChildren.filter((child): child is Node => child !== null) ?? []
|
||||
}
|
||||
|
||||
export type RustVisibility = 'public' | 'module' | 'super' | 'crate'
|
||||
export function firstSentence(text: string): string {
|
||||
const trimmed = text.trim()
|
||||
const sentence = trimmed.match(/^[\s\S]*?[。!?.!?]/)
|
||||
return (sentence ? sentence[0] : trimmed.split('\n')[0] ?? '').trim()
|
||||
}
|
||||
|
||||
/** 文档注释去掉记号后的第一句;普通注释是整段去掉记号。 */
|
||||
export function commentPlain(node: Node): string {
|
||||
const doc = node.childForFieldName('doc')
|
||||
const raw = doc ? doc.text : node.text.replace(/^\/\/[/!]?\s?/, '').replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '')
|
||||
return raw.replace(/\n/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** 说明行用的第一句。没有文字时没有。 */
|
||||
export function docSentence(node: Node | undefined): string | undefined {
|
||||
if (!node)
|
||||
return undefined
|
||||
const sentence = firstSentence(commentPlain(node))
|
||||
return sentence || undefined
|
||||
}
|
||||
|
||||
/** 空白收成一个空格,方便属性表里的类型、签名。 */
|
||||
export function oneLine(text: string): string {
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** 节点上的可见性。pub(in …) 不在这四档里,按本 crate 算(只本包能用)。 */
|
||||
export function rustVisibility(node: Node): RustVisibility {
|
||||
const vis = named(node).find(child => child.type === 'visibility_modifier')
|
||||
if (!vis)
|
||||
return 'module'
|
||||
const words = new Set(vis.children.map(child => child?.type))
|
||||
if (words.has('super'))
|
||||
return 'super'
|
||||
if (words.has('crate') || words.has('in'))
|
||||
return 'crate'
|
||||
return 'public'
|
||||
}
|
||||
|
||||
/** 公开写在 facts.access 上;其余写在块的 access 属性上,盖过 facts。 */
|
||||
export function visibilityFacts(node: Node): { access?: Access, attributes?: BlockAttributes } {
|
||||
const visibility = rustVisibility(node)
|
||||
return visibility === 'public' ? { access: 'public' } : { attributes: { access: visibility } }
|
||||
}
|
||||
|
||||
/** 参数表括号里的原文,空白收成一行。 */
|
||||
export function signatureParams(fn: Node): string {
|
||||
const params = fn.childForFieldName('parameters')
|
||||
if (!params)
|
||||
return ''
|
||||
return oneLine(params.text.replace(/^\(/, '').replace(/\)$/, ''))
|
||||
}
|
||||
|
||||
/** 返回类型原文;没写是 ()。 */
|
||||
export function returnText(fn: Node): string {
|
||||
const returned = fn.childForFieldName('return_type')
|
||||
return returned ? oneLine(returned.text) : '()'
|
||||
}
|
||||
|
||||
/** 函数体里直接写的 match,每一支的模式和「做什么」。 */
|
||||
export function matchArms(fn: Node): { pattern: string, does: string }[] {
|
||||
const body = fn.childForFieldName('body')
|
||||
if (!body)
|
||||
return []
|
||||
const arms: { pattern: string, does: string }[] = []
|
||||
for (const statement of named(body)) {
|
||||
const expr = statement.type === 'expression_statement' ? named(statement)[0] : statement
|
||||
if (expr?.type !== 'match_expression')
|
||||
continue
|
||||
const block = expr.childForFieldName('body')
|
||||
for (const arm of named(block).filter(child => child.type === 'match_arm')) {
|
||||
const pattern = arm.childForFieldName('pattern')
|
||||
if (pattern)
|
||||
arms.push({ pattern: oneLine(pattern.text), does: armDoes(arm) })
|
||||
}
|
||||
}
|
||||
return arms
|
||||
}
|
||||
|
||||
/** 变体携带的数据:没带是空的,元组变体是类型,结构变体是字段原文。body 是变体的 body。 */
|
||||
export function variantPayload(body: Node | undefined): string | undefined {
|
||||
if (!body)
|
||||
return undefined
|
||||
if (body.type === 'ordered_field_declaration_list') {
|
||||
const types = named(body).filter(child => child.type !== 'attribute_item').map(child => oneLine(child.text))
|
||||
return types.join(', ') || undefined
|
||||
}
|
||||
return oneLine(body.text)
|
||||
}
|
||||
|
||||
/** 写在这条前面、紧挨着的属性(#[…]),从外往里。 */
|
||||
export function leadingAttributes(node: Node): Node[] {
|
||||
const attrs: Node[] = []
|
||||
let prev = node.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item') {
|
||||
attrs.unshift(prev)
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
/** 属性的名字:get、serde、axum::post、tauri::command。 */
|
||||
export function attributeName(attributeItem: Node): string | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
const head = attribute ? named(attribute)[0] : undefined
|
||||
return head?.text
|
||||
}
|
||||
|
||||
/** 这条自己的属性,含写在它前面的。 */
|
||||
export function itemAttributes(node: Node): Node[] {
|
||||
return [...leadingAttributes(node), ...named(node).filter(child => child.type === 'attribute_item')]
|
||||
}
|
||||
|
||||
/** derive(...) 里列出的名字。 */
|
||||
export function deriveNames(node: Node): string[] {
|
||||
const names: string[] = []
|
||||
for (const item of itemAttributes(node)) {
|
||||
if (attributeName(item) !== 'derive')
|
||||
continue
|
||||
const attribute = named(item).find(child => child.type === 'attribute')
|
||||
const tree = attribute?.children.find(child => child?.type === 'token_tree')
|
||||
if (!tree)
|
||||
continue
|
||||
for (const name of named(tree)) {
|
||||
if (name.type === 'identifier')
|
||||
names.push(name.text)
|
||||
else if (name.type === 'scoped_identifier')
|
||||
names.push(name.childForFieldName('name')?.text ?? name.text)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/** 字符串字面量里的原文,不含引号。 */
|
||||
export function stringContent(node: Node | undefined): string | undefined {
|
||||
if (!node)
|
||||
return undefined
|
||||
if (node.type === 'string_content')
|
||||
return node.text
|
||||
const content = node.children.find(child => child?.type === 'string_content')
|
||||
return content?.text
|
||||
}
|
||||
|
||||
/** 宏调用的名字和括号里的具名实参。 */
|
||||
export function macroInvocation(node: Node): { name: string, args: Node[] } | undefined {
|
||||
if (node.type !== 'macro_invocation')
|
||||
return undefined
|
||||
const macro = node.childForFieldName('macro')
|
||||
const args = node.children.find(child => child?.type === 'token_tree')
|
||||
if (!macro || !args)
|
||||
return undefined
|
||||
return { name: macro.text, args: named(args) }
|
||||
}
|
||||
|
||||
/** 函数写在 impl 或 trait 里面,参数表叫「参数」而不是「输入参数」。 */
|
||||
export function isMethod(node: Node): boolean {
|
||||
const parent = node.parent
|
||||
return parent?.type === 'declaration_list' && (parent.parent?.type === 'impl_item' || parent.parent?.type === 'trait_item')
|
||||
}
|
||||
|
||||
/** 结构体的字段:名字、类型、可见性。元组结构体没有名字,不进这张表。 */
|
||||
export function structFields(node: Node): { name: Node, type: Node, visibility: RustVisibility }[] {
|
||||
const body = node.childForFieldName('body')
|
||||
if (body?.type !== 'field_declaration_list')
|
||||
return []
|
||||
return named(body).flatMap((field) => {
|
||||
if (field.type !== 'field_declaration')
|
||||
return []
|
||||
const name = field.childForFieldName('name')
|
||||
const type = field.childForFieldName('type')
|
||||
return name && type ? [{ name, type, visibility: rustVisibility(field) }] : []
|
||||
})
|
||||
}
|
||||
|
||||
/** trait 或 impl 主体里的函数(含只写签名的)。 */
|
||||
export function bodyFunctions(node: Node): Node[] {
|
||||
const body = node.childForFieldName('body')
|
||||
return named(body).filter(child => child.type === 'function_item' || child.type === 'function_signature_item')
|
||||
}
|
||||
function armDoes(arm: Node): string {
|
||||
const comment = firstComment(arm)
|
||||
if (comment)
|
||||
return firstSentence(commentPlain(comment)) || '—'
|
||||
const value = arm.childForFieldName('value')
|
||||
if (!value)
|
||||
return '—'
|
||||
if (value.type === 'return_expression') {
|
||||
const returned = named(value)[0]
|
||||
return returned ? `返回 ${oneLine(returned.text)}` : '返回'
|
||||
}
|
||||
if (value.type === 'block') {
|
||||
const inner = firstComment(value)
|
||||
if (inner)
|
||||
return firstSentence(commentPlain(inner)) || '—'
|
||||
const statement = named(value)[0]
|
||||
return statement ? oneLine(statement.text) : '—'
|
||||
}
|
||||
return oneLine(value.text)
|
||||
}
|
||||
|
||||
function firstComment(node: Node): Node | undefined {
|
||||
if (node.type === 'line_comment' || node.type === 'block_comment')
|
||||
return node
|
||||
for (const child of node.children) {
|
||||
if (!child)
|
||||
continue
|
||||
const found = firstComment(child)
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { ClassBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor, TokenRole } from '../contract'
|
||||
import type { rustVisibility } from './rustTree'
|
||||
import { visibilityWord } from './rustRows'
|
||||
import { attributeName, deriveNames, docSentence, itemAttributes, leadingAttributes, named, oneLine, stringContent, structFields } from './rustTree'
|
||||
|
||||
/** 和结构体同一个青,序列化只是名称后的标签,不另换边框色。 */
|
||||
const COLOR: ThemeColor = { dark: '#4ec9b0', light: '#267f99' }
|
||||
const BADGE: KindBadge = { label: '可序列化', color: COLOR, icon: 'braces' }
|
||||
const MARKS = new Set(['Serialize', 'Deserialize'])
|
||||
|
||||
/**
|
||||
* 带 Serialize / Deserialize 的结构体。字段多一列「序列化」。
|
||||
* 文件里导入了 serde 才启用。
|
||||
*/
|
||||
export const serdeRule: LibraryRule = {
|
||||
id: 'serde',
|
||||
label: 'serde 序列化',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['serde'],
|
||||
claim: block => block.kind === 'class' && block.node.type === 'struct_item' && marked(block) ? card(block) : undefined,
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'serde.struct': '可序列化' } } }),
|
||||
}
|
||||
|
||||
function marked(block: ClassBlock): boolean {
|
||||
return deriveNames(block.node).some(name => MARKS.has(name))
|
||||
}
|
||||
|
||||
function card(block: ClassBlock): RuleCard {
|
||||
const fields = structFields(block.node)
|
||||
const camel = renameAll(block.node) === 'camelCase'
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const rows: PropertyRow[] = [
|
||||
{
|
||||
label: '名称',
|
||||
value: block.facts.name ? [...copied(block.facts.name, 'type'), plain(' '), tag('可序列化')] : [tag('可序列化')],
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
},
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
{ label: '可见性', value: [{ text: visibilityWord(block), role: 'keyword' as const }] },
|
||||
...(camel ? [{ label: '选项', value: [keyword('字段名改驼峰')] }] : []),
|
||||
...(fields.length > 0
|
||||
? [{
|
||||
label: '字段',
|
||||
value: [hint(`${fields.length} 个`)],
|
||||
columns: ['字段', '类型', '可见性', '序列化'],
|
||||
items: fields.map(field => ({
|
||||
cells: [
|
||||
copied(field.name, 'variable'),
|
||||
copied(field.type, 'type'),
|
||||
[keyword(visibilityWordOf(field.visibility))],
|
||||
[serdeCell(field.name)],
|
||||
],
|
||||
defines: range(field.name),
|
||||
})),
|
||||
}]
|
||||
: []),
|
||||
]
|
||||
return {
|
||||
kind: 'serde.struct',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
function visibilityWordOf(visibility: ReturnType<typeof rustVisibility>): string {
|
||||
switch (visibility) {
|
||||
case 'public': return '公开'
|
||||
case 'super': return '父模块'
|
||||
case 'crate': return '本 crate'
|
||||
default: return '本模块'
|
||||
}
|
||||
}
|
||||
|
||||
function serdeCell(fieldName: Node): Inline {
|
||||
const field = fieldName.parent
|
||||
if (!field)
|
||||
return hint('—')
|
||||
let rename: string | undefined
|
||||
let skip = false
|
||||
for (const attr of leadingAttributes(field)) {
|
||||
if (attributeName(attr) !== 'serde')
|
||||
continue
|
||||
for (const piece of named(tokenTree(attr))) {
|
||||
if (piece.type === 'identifier' && (piece.text === 'skip' || piece.text === 'skip_serializing'))
|
||||
skip = true
|
||||
if (piece.type === 'identifier' && piece.text === 'rename')
|
||||
rename = stringContent(firstString(tokenTree(attr))) ?? rename
|
||||
}
|
||||
}
|
||||
if (skip)
|
||||
return hint('不输出')
|
||||
if (rename)
|
||||
return { text: `改名 ${rename}`, role: 'string' }
|
||||
return hint('—')
|
||||
}
|
||||
|
||||
function renameAll(node: Node): string | undefined {
|
||||
for (const attr of itemAttributes(node)) {
|
||||
if (attributeName(attr) !== 'serde')
|
||||
continue
|
||||
const names = named(tokenTree(attr))
|
||||
if (!names.some(piece => piece.type === 'identifier' && piece.text === 'rename_all'))
|
||||
continue
|
||||
const value = stringContent(firstString(tokenTree(attr)))
|
||||
if (value)
|
||||
return value
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function tokenTree(attributeItem: Node): Node | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
return attribute?.children.find(child => child?.type === 'token_tree') ?? undefined
|
||||
}
|
||||
|
||||
function firstString(tree: Node | undefined): Node | undefined {
|
||||
return tree?.children.find(child => child?.type === 'string_literal') ?? undefined
|
||||
}
|
||||
|
||||
function copied(node: Node, role: TokenRole): Inline[] {
|
||||
return [{ text: oneLine(node.text), role, offset: node.startIndex }]
|
||||
}
|
||||
|
||||
function tag(label: string): SegmentGroup {
|
||||
return { style: 'tag', label, segments: [], color: COLOR }
|
||||
}
|
||||
|
||||
function plain(text: string): Inline {
|
||||
return { text, role: 'plain' }
|
||||
}
|
||||
|
||||
function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function range(node: Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
||||
import { commentPlain, firstSentence, macroInvocation, named, oneLine, stringContent } from './rustTree'
|
||||
|
||||
/** 查询卡用字符串那种橙。 */
|
||||
const COLOR: ThemeColor = { dark: '#ce9178', light: '#a31515' }
|
||||
const BADGE: KindBadge = { label: '查询', color: COLOR, icon: 'database' }
|
||||
const MACROS = new Set(['query', 'query_as', 'query_scalar'])
|
||||
|
||||
/**
|
||||
* sqlx::query! / query_as! 这类宏。一句说明、等宽 SQL、参数、返回类型。
|
||||
* 文件里导入了 sqlx 才启用。
|
||||
*/
|
||||
export const sqlxRule: LibraryRule = {
|
||||
id: 'sqlx',
|
||||
label: 'sqlx 查询',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['sqlx'],
|
||||
claim: (block) => {
|
||||
if (block.kind === 'comment')
|
||||
return nextQuery(block.node) ? { omit: true } : undefined
|
||||
const macro = block.kind === 'statement' ? findQuery(block.node) : undefined
|
||||
return macro ? card(block, macro) : undefined
|
||||
},
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'sqlx.query': '查询' } } }),
|
||||
}
|
||||
|
||||
function card(block: CodeBlock, macro: Node): RuleCard {
|
||||
const call = macroInvocation(macro)
|
||||
const args = call?.args ?? []
|
||||
const typed = call?.name.split('::').pop() !== 'query'
|
||||
const sqlNode = args[typed ? 1 : 0]
|
||||
const sql = stringContent(sqlNode) ?? ''
|
||||
const params = args.slice(typed ? 2 : 1)
|
||||
const returned = typed ? args[0] : undefined
|
||||
const note = precedingNote(block.node)
|
||||
const rows: PropertyRow[] = [
|
||||
...(note ? [{ label: '说明', value: { markdown: note } }] : []),
|
||||
{ label: 'SQL', value: [{ text: sql, role: 'string', ...(sqlNode && { offset: sqlNode.startIndex }) }] },
|
||||
...(params.length > 0
|
||||
? [{ label: '参数', value: [{ text: params.map((param, index) => `$${index + 1} = ${oneLine(param.text)}`).join('、'), role: 'plain' as const }] }]
|
||||
: []),
|
||||
{ label: '返回类型', value: returned ? [{ text: oneLine(returned.text), role: 'type', offset: returned.startIndex }] : [hint('—')] },
|
||||
]
|
||||
return { kind: 'sqlx.query', badge: BADGE, rows, absorbs: [] }
|
||||
}
|
||||
|
||||
function findQuery(node: Node): Node | undefined {
|
||||
if (node.type === 'macro_invocation') {
|
||||
const last = node.childForFieldName('macro')?.text.split('::').pop()
|
||||
if (last && MACROS.has(last))
|
||||
return node
|
||||
}
|
||||
for (const child of named(node)) {
|
||||
const found = findQuery(child)
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function nextQuery(comment: Node): Node | undefined {
|
||||
let next = comment.nextNamedSibling
|
||||
while (next?.type === 'attribute_item')
|
||||
next = next.nextNamedSibling
|
||||
return next ? findQuery(next) : undefined
|
||||
}
|
||||
|
||||
function precedingNote(node: Node): string | undefined {
|
||||
let prev = node.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item')
|
||||
prev = prev.previousNamedSibling
|
||||
if (!prev || (prev.type !== 'line_comment' && prev.type !== 'block_comment'))
|
||||
return undefined
|
||||
const sentence = firstSentence(commentPlain(prev))
|
||||
return sentence || undefined
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor } from '../contract'
|
||||
import { attributeName, docSentence, oneLine, returnText } from './rustTree'
|
||||
|
||||
/** 桌面命令的边框用这个橙。 */
|
||||
const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' }
|
||||
const BADGE: KindBadge = { label: '桌面命令', color: COLOR, icon: 'square-function' }
|
||||
|
||||
/**
|
||||
* #[tauri::command] 的函数。名称后标「桌面命令」。
|
||||
* 参数多一列来源:State<…> 是窗口状态,其余是调用方传入。
|
||||
* 文件里导入了 tauri 才启用。
|
||||
*/
|
||||
export const tauriRule: LibraryRule = {
|
||||
id: 'tauri',
|
||||
label: 'tauri 桌面命令',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['tauri'],
|
||||
claim: block => block.kind === 'function' && commanded(block) ? card(block) : undefined,
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令' } } }),
|
||||
}
|
||||
|
||||
function commanded(block: FunctionBlock): boolean {
|
||||
return block.facts.decorators.some(attr => attributeName(attr) === 'tauri::command')
|
||||
}
|
||||
|
||||
function card(block: FunctionBlock): RuleCard {
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const rows: PropertyRow[] = [
|
||||
{
|
||||
label: '名称',
|
||||
value: block.facts.name
|
||||
? [{ text: block.facts.name.text, role: 'function', offset: block.facts.name.startIndex }, plain(' '), tag('桌面命令')]
|
||||
: [tag('桌面命令')],
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
},
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||
...(block.facts.parameters.length > 0
|
||||
? [{
|
||||
label: '参数',
|
||||
value: [hint(`${block.facts.parameters.length} 个`)],
|
||||
columns: ['名称', '类型', '来源'],
|
||||
items: block.facts.parameters.map(param => ({
|
||||
cells: [
|
||||
[variable(param.name)],
|
||||
[param.type ? typed(param.type) : hint('—')],
|
||||
[hint(isWindowState(param.type) ? '窗口状态' : '调用方传入')],
|
||||
],
|
||||
defines: range(param.name),
|
||||
})),
|
||||
}]
|
||||
: []),
|
||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
||||
]
|
||||
return {
|
||||
kind: 'tauri.command',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowState(type: Node | undefined): boolean {
|
||||
if (!type)
|
||||
return false
|
||||
if (type.type === 'generic_type')
|
||||
return type.childForFieldName('type')?.text === 'State'
|
||||
return type.text.startsWith('State<')
|
||||
}
|
||||
|
||||
function variable(node: Node): Inline {
|
||||
return { text: node.text, role: 'variable', offset: node.startIndex }
|
||||
}
|
||||
|
||||
function typed(node: Node): Inline {
|
||||
return { text: oneLine(node.text), role: 'type', offset: node.startIndex }
|
||||
}
|
||||
|
||||
function tag(label: string): SegmentGroup {
|
||||
return { style: 'tag', label, segments: [], color: COLOR }
|
||||
}
|
||||
|
||||
function plain(text: string): Inline {
|
||||
return { text, role: 'plain' }
|
||||
}
|
||||
|
||||
function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function range(node: Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { EnumBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor } from '../contract'
|
||||
import { attributeName, commentPlain, deriveNames, docSentence, firstSentence, named, stringContent, variantPayload } from './rustTree'
|
||||
|
||||
/** 错误卡用异常那种红。 */
|
||||
const COLOR: ThemeColor = { dark: '#f14c4c', light: '#cd3131' }
|
||||
const BADGE: KindBadge = { label: '错误', color: COLOR, icon: 'triangle-alert' }
|
||||
|
||||
/**
|
||||
* derive Error 的枚举。变体表是:变体、何时、展示。
|
||||
* 文件里导入了 thiserror 才启用。
|
||||
*/
|
||||
export const thiserrorRule: LibraryRule = {
|
||||
id: 'thiserror',
|
||||
label: 'thiserror 错误',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['thiserror'],
|
||||
claim: block => block.kind === 'enum' && deriveNames(block.node).includes('Error') ? card(block) : undefined,
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'thiserror.error': '错误' } } }),
|
||||
}
|
||||
|
||||
function card(block: EnumBlock): RuleCard {
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const rows: PropertyRow[] = [
|
||||
{
|
||||
label: '名称',
|
||||
value: [{ text: block.facts.name.text, role: 'type', offset: block.facts.name.startIndex }],
|
||||
defines: range(block.facts.name),
|
||||
},
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
{
|
||||
label: '变体',
|
||||
value: [hint(`${block.facts.members.length} 个`)],
|
||||
columns: ['变体', '何时', '展示'],
|
||||
items: block.facts.members.map((member) => {
|
||||
const variant = member.name.parent?.type === 'enum_variant' ? member.name.parent : member.name
|
||||
const display = errorText(variant)
|
||||
const cells: Inline[][] = [
|
||||
[{ text: member.name.text, role: 'type', offset: member.name.startIndex }],
|
||||
[hint(whenText(variant) ?? '—')],
|
||||
display
|
||||
? [plain(display), ...(passesSource(variant) ? [plain(' '), tag('透传来源')] : [])]
|
||||
: [hint(variantPayload(member.value) ?? '—')],
|
||||
]
|
||||
return { cells, defines: range(member.name) }
|
||||
}),
|
||||
},
|
||||
]
|
||||
return {
|
||||
kind: 'thiserror.error',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
defines: range(block.facts.name),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 变体上面的文档注释,属性可以隔在中间。第一句是「何时」。 */
|
||||
function whenText(variant: Node): string | undefined {
|
||||
const docs: Node[] = []
|
||||
let prev = variant.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item')
|
||||
prev = prev.previousNamedSibling
|
||||
while (prev && (prev.type === 'line_comment' || prev.type === 'block_comment')) {
|
||||
docs.unshift(prev)
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
if (docs.length === 0)
|
||||
return undefined
|
||||
const sentence = firstSentence(docs.map(commentPlain).join(' '))
|
||||
return sentence || undefined
|
||||
}
|
||||
|
||||
function errorText(variant: Node): string | undefined {
|
||||
let prev = variant.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item') {
|
||||
if (attributeName(prev) === 'error') {
|
||||
const tree = tokenTree(prev)
|
||||
const literal = tree?.children.find(child => child?.type === 'string_literal')
|
||||
const text = stringContent(literal ?? undefined)
|
||||
if (text)
|
||||
return text
|
||||
}
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function passesSource(variant: Node): boolean {
|
||||
const body = variant.childForFieldName('body')
|
||||
return named(body).some(child => child.type === 'attribute_item' && (attributeName(child) === 'from' || attributeName(child) === 'source'))
|
||||
}
|
||||
|
||||
function tokenTree(attributeItem: Node): Node | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
return attribute?.children.find(child => child?.type === 'token_tree') ?? undefined
|
||||
}
|
||||
|
||||
function tag(label: string): SegmentGroup {
|
||||
return { style: 'tag', label, segments: [], color: COLOR }
|
||||
}
|
||||
|
||||
function plain(text: string): Inline {
|
||||
return { text, role: 'plain' }
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function range(node: Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { FactBlock, Node, PropertyRow, SourceDocument, Translator } from '../contract'
|
||||
import { enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
|
||||
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
|
||||
|
||||
const say = plainSay()
|
||||
|
||||
const template = createZhTemplateTranslator({
|
||||
id: 'rust.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['rust'],
|
||||
keywords: {
|
||||
fn: '函数',
|
||||
let: '令',
|
||||
mut: '可变',
|
||||
pub: '公开',
|
||||
struct: '结构体',
|
||||
enum: '枚举',
|
||||
impl: '实现',
|
||||
trait: '特质',
|
||||
use: '导入',
|
||||
mod: '模块',
|
||||
match: '匹配',
|
||||
if: '如果',
|
||||
else: '否则',
|
||||
for: '遍历',
|
||||
while: '当',
|
||||
loop: '循环',
|
||||
return: '返回',
|
||||
async: '异步',
|
||||
await: '等待',
|
||||
break: '跳出',
|
||||
continue: '继续',
|
||||
where: '其中',
|
||||
type: '类型',
|
||||
const: '常量',
|
||||
static: '静态',
|
||||
unsafe: '不安全',
|
||||
as: '作为',
|
||||
in: '在',
|
||||
ref: '引用',
|
||||
move: '移动',
|
||||
dyn: '动态',
|
||||
crate: '本 crate',
|
||||
super: '父模块',
|
||||
self: '自己',
|
||||
Self: '本类型',
|
||||
},
|
||||
literals: { true: '真', false: '假' },
|
||||
builtinTypes: {},
|
||||
isTypePosition: () => false,
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
stringNodes: ['string_literal'],
|
||||
commentText,
|
||||
nodeTemplates: {},
|
||||
})
|
||||
|
||||
/**
|
||||
* Rust 的中文直译。属性表的用词按代码阅读的 Rust 原型:结构体、可见性、变体、分情况。
|
||||
* 关键字和注释仍走中文直译那一套。常量和变量的行不另写一套,走模板里现成的那套。
|
||||
*/
|
||||
export const rustZhTranslator: Translator = {
|
||||
...template,
|
||||
attributeNames: {
|
||||
attributes: { ...ZH_ATTRIBUTE_NAMES.attributes, access: '可见性' },
|
||||
values: {
|
||||
...ZH_ATTRIBUTE_NAMES.values,
|
||||
kind: { ...KIND_ZH, class: '结构体' },
|
||||
access: {
|
||||
...ZH_ATTRIBUTE_NAMES.values.access,
|
||||
public: '公开',
|
||||
module: '本模块',
|
||||
super: '父模块',
|
||||
crate: '本 crate',
|
||||
},
|
||||
},
|
||||
},
|
||||
describe,
|
||||
}
|
||||
|
||||
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||||
switch (block.kind) {
|
||||
case 'import':
|
||||
return importRows(block, say)
|
||||
case 'class':
|
||||
return structRows(block, say)
|
||||
case 'enum':
|
||||
return enumRows(block, say)
|
||||
case 'function':
|
||||
return functionRows(block, say)
|
||||
case 'trait':
|
||||
return traitRows(block, say)
|
||||
case 'impl':
|
||||
return implRows(block, say)
|
||||
case 'module':
|
||||
return moduleRows(block, say)
|
||||
case 'const':
|
||||
case 'variable':
|
||||
return template.describe(block, document)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function commentText(node: Node, text: string): string {
|
||||
const doc = node.childForFieldName('doc')
|
||||
if (doc)
|
||||
return doc.text.trim()
|
||||
return text.replace(/^\/\/[/!]?\s?/, '').replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '').trim()
|
||||
}
|
||||
@@ -208,10 +208,13 @@ function slotChild(node: Node, slot: Exclude<TemplatePart, string>): Node | null
|
||||
}
|
||||
|
||||
const ATTRIBUTE_ZH: Record<CommonAttribute, string> = { kind: '种类', access: '访问级别', async: '异步', abstract: '抽象', typeOnly: '仅类型' }
|
||||
const KIND_ZH: Record<BlockKind, string> = {
|
||||
export const KIND_ZH: Record<BlockKind, string> = {
|
||||
import: '导入',
|
||||
function: '函数',
|
||||
class: '类',
|
||||
trait: '特质',
|
||||
impl: '实现',
|
||||
module: '模块',
|
||||
enum: '枚举',
|
||||
exception: '异常',
|
||||
type: '类型',
|
||||
@@ -249,7 +252,7 @@ const ARGUMENT_COLUMNS = ['参数', '值']
|
||||
const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' }
|
||||
|
||||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||||
const ZH_ATTRIBUTE_NAMES: AttributeNames = {
|
||||
export const ZH_ATTRIBUTE_NAMES: AttributeNames = {
|
||||
attributes: ATTRIBUTE_ZH,
|
||||
values: { kind: KIND_ZH, access: ACCESS_ZH, async: YES_NO, abstract: YES_NO, typeOnly: YES_NO },
|
||||
}
|
||||
@@ -704,6 +707,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
case 'function':
|
||||
return functionRows(block.facts, phrases)
|
||||
case 'class':
|
||||
case 'trait':
|
||||
case 'impl':
|
||||
case 'module':
|
||||
return classRows(block.facts, phrases)
|
||||
case 'enum':
|
||||
return enumRows(block.facts, phrases)
|
||||
|
||||
@@ -11,10 +11,11 @@ import { BlockLevel } from './BlockTree'
|
||||
import { DiffBar, NewFileBanner } from './DiffBar'
|
||||
import { changeStops, diffTree, foldUnchanged, openedByDiff } from './diffView'
|
||||
import { acceptNames, DisplayNameContext, documentNames } from './displayNames'
|
||||
import { coveredByEditing, startEditing } from './editing'
|
||||
import { coveredByEditing, snippetRefs, startEditing } from './editing'
|
||||
import { applyFilter, excludedBy, keptWhileEditing } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
import { OverviewRuler } from './OverviewRuler'
|
||||
import { SnippetEditContext } from './PropertyGrid'
|
||||
import { asksOf, historyOf, PATH_ASK_ANCHOR } from './qa'
|
||||
import { useChangeNav } from './useChangeNav'
|
||||
import { useDefinitionLinks } from './useDefinitionLinks'
|
||||
@@ -67,6 +68,7 @@ export function App(): ReactElement | null {
|
||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds, openedByDiff(changes)), [doc, view.folds, changes])
|
||||
const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep])
|
||||
const covered = useMemo(() => coveredByEditing(doc?.blocks ?? [], new Set(view.editing)), [doc, view.editing])
|
||||
const snippetIds = useMemo(() => new Map(snippetRefs(doc?.blocks ?? []).map(ref => [ref.edit, ref.id])), [doc])
|
||||
/** 「只看改动」开着(整个工作区共用,扩展端记着);本地不改,改了发给扩展端、等它发回来 */
|
||||
const [onlyChanges, setOnlyChanges] = useState(false)
|
||||
/** 「只看改动」时点开过的几段没变的块(按第一块的 id),筛选一变就重新折 */
|
||||
@@ -176,6 +178,7 @@ export function App(): ReactElement | null {
|
||||
return (
|
||||
<HighlightContext value={highlight}>
|
||||
<DisplayNameContext value={names?.names ?? NO_NAMES}>
|
||||
<SnippetEditContext value={{ ids: snippetIds, editing: tree.editing, onToggle: tree.onToggleEdit, version: doc.version, focusId, onFocused }}>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
@@ -236,6 +239,7 @@ export function App(): ReactElement | null {
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
</SnippetEditContext>
|
||||
</DisplayNameContext>
|
||||
</HighlightContext>
|
||||
)
|
||||
|
||||
@@ -270,7 +270,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
||||
{editing
|
||||
? (
|
||||
<CodeEditor
|
||||
block={block}
|
||||
source={block}
|
||||
version={tree.version}
|
||||
autoFocus={tree.focusId === block.id}
|
||||
onAutoFocused={tree.onFocused}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react'
|
||||
import type { BlockView } from './types'
|
||||
import type { Segment } from './types'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { recolor } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
@@ -7,9 +7,17 @@ import { postMessage } from './vscode'
|
||||
|
||||
const EDIT_DEBOUNCE_MS = 200
|
||||
|
||||
/** 编辑框要的那一段源码:整块,或格子里单独打开的一段,字段一样。 */
|
||||
export interface EditableSource {
|
||||
startLine: number
|
||||
endLine: number
|
||||
code: string
|
||||
codeTokens: Segment[]
|
||||
}
|
||||
|
||||
interface CodeEditorProps {
|
||||
block: BlockView
|
||||
/** 文档版本:每来一份新文档都要对一次输入框内容,哪怕这一块的代码没变 */
|
||||
source: EditableSource
|
||||
/** 文档版本:每来一份新文档都要对一次输入框内容,哪怕这一段的代码没变 */
|
||||
version: number
|
||||
autoFocus: boolean
|
||||
onAutoFocused: () => void
|
||||
@@ -17,11 +25,11 @@ interface CodeEditorProps {
|
||||
}
|
||||
|
||||
/** 一块源码的编辑状态:着色层显示,透明输入框接收输入,改动防抖后整行替换回文件。 */
|
||||
export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
|
||||
const [text, setText] = useState(block.code)
|
||||
export function CodeEditor({ source, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
|
||||
const [text, setText] = useState(source.code)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const blockRef = useRef(block)
|
||||
blockRef.current = block
|
||||
const sourceRef = useRef(source)
|
||||
sourceRef.current = source
|
||||
const textRef = useRef(text)
|
||||
textRef.current = text
|
||||
/** 本地改过、还没在新文档里看到的文字;有它时新文档不能覆盖有焦点的输入框 */
|
||||
@@ -37,7 +45,7 @@ export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }:
|
||||
if (!unsent.current)
|
||||
return
|
||||
unsent.current = false
|
||||
const { startLine, endLine } = blockRef.current
|
||||
const { startLine, endLine } = sourceRef.current
|
||||
const value = textRef.current
|
||||
postMessage({
|
||||
type: 'edit',
|
||||
@@ -51,16 +59,16 @@ export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }:
|
||||
useEffect(() => {
|
||||
const el = textareaRef.current
|
||||
const focused = el !== null && document.activeElement === el
|
||||
if (focused && unconfirmed.current !== null && unconfirmed.current !== block.code)
|
||||
if (focused && unconfirmed.current !== null && unconfirmed.current !== source.code)
|
||||
return
|
||||
unconfirmed.current = null
|
||||
sentLineCount.current = null
|
||||
if (block.code === textRef.current)
|
||||
if (source.code === textRef.current)
|
||||
return
|
||||
if (focused)
|
||||
restoreSelection.current = [el.selectionStart, el.selectionEnd]
|
||||
setText(block.code)
|
||||
}, [version, block.code])
|
||||
setText(source.code)
|
||||
}, [version, source.code])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = textareaRef.current
|
||||
@@ -121,7 +129,7 @@ export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }:
|
||||
onBlur={flush}
|
||||
/>
|
||||
<pre className="cr-code" aria-hidden="true">
|
||||
<SegmentText segments={recolor(block.codeTokens, block.code, text)} />
|
||||
<SegmentText segments={recolor(source.codeTokens, source.code, text)} />
|
||||
{text === '' || text.endsWith('\n') ? ' ' : ''}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
import type { BlockView, Inline, InlinePart, ItemDiff, ItemsDiff, KindBadge, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import type { BlockView, CellEdit, Inline, InlinePart, ItemDiff, ItemsDiff, KindBadge, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
|
||||
import { ChevronDown, ChevronRight, Eye, Pencil } from 'lucide-react'
|
||||
import { createContext, Fragment, useContext } from 'react'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { DiffInline, DiffText } from './DiffText'
|
||||
import { inlineText } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
@@ -56,6 +57,83 @@ function CellText({ cell, parts }: { cell: Inline[], parts: InlinePart[] | undef
|
||||
return parts ? <DiffInline parts={parts} /> : <SegmentText segments={cell} />
|
||||
}
|
||||
|
||||
/** 格子里单独打开的那段源码:和块共用同一套铅笔、同一个编辑框。 */
|
||||
export interface SnippetEditState {
|
||||
ids: ReadonlyMap<CellEdit, string>
|
||||
editing: ReadonlySet<string>
|
||||
onToggle: (id: string) => void
|
||||
version: number
|
||||
focusId: string | null
|
||||
onFocused: () => void
|
||||
}
|
||||
|
||||
export const SnippetEditContext = createContext<SnippetEditState | null>(null)
|
||||
|
||||
function EditToggle({ editing, onToggle }: { editing: boolean, onToggle: () => void }): ReactElement {
|
||||
const label = editing ? '预览' : '编辑'
|
||||
const Icon = editing ? Eye : Pencil
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="cr-tool inline-flex h-5 w-6 items-center justify-center rounded align-middle"
|
||||
data-active={editing ? '' : undefined}
|
||||
data-tooltip={label}
|
||||
aria-label={label}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon size={14} strokeWidth={1.8} aria-hidden />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function SnippetSpan({ edit, segments }: { edit: CellEdit, segments: Inline[] }): ReactElement {
|
||||
const ctx = useContext(SnippetEditContext)
|
||||
const id = ctx?.ids.get(edit)
|
||||
if (!ctx || id === undefined)
|
||||
return <SegmentText segments={segments} />
|
||||
const editing = ctx.editing.has(id)
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-baseline gap-0.5">
|
||||
{editing
|
||||
? (
|
||||
<span className="block w-full">
|
||||
<CodeEditor
|
||||
source={edit.snippet}
|
||||
version={ctx.version}
|
||||
autoFocus={ctx.focusId === id}
|
||||
onAutoFocused={ctx.onFocused}
|
||||
onExit={() => ctx.onToggle(id)}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
: <SegmentText segments={segments} />}
|
||||
<EditToggle editing={editing} onToggle={() => ctx.onToggle(id)} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function CellView({ cell, index, edits, parts }: {
|
||||
cell: Inline[]
|
||||
index: number
|
||||
edits: CellEdit[] | undefined
|
||||
parts: InlinePart[] | undefined
|
||||
}): ReactElement {
|
||||
const mine = (edits ?? []).filter(edit => edit.cell === index).sort((a, b) => a.from - b.from)
|
||||
if (mine.length === 0)
|
||||
return <CellText cell={cell} parts={parts} />
|
||||
const chunks: ReactElement[] = []
|
||||
let at = 0
|
||||
mine.forEach((edit, n) => {
|
||||
if (edit.from > at)
|
||||
chunks.push(<SegmentText key={`t${n}`} segments={cell.slice(at, edit.from)} />)
|
||||
chunks.push(<SnippetSpan key={`s${n}`} edit={edit} segments={cell.slice(edit.from, edit.to)} />)
|
||||
at = edit.to
|
||||
})
|
||||
if (at < cell.length)
|
||||
chunks.push(<SegmentText key="tail" segments={cell.slice(at)} />)
|
||||
return <>{chunks}</>
|
||||
}
|
||||
|
||||
/**
|
||||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||||
@@ -115,9 +193,9 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
|
||||
{item.cells.map((cell, i) => i === 0 && nested
|
||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
|
||||
: (
|
||||
<p key={i} className="cr-code min-w-0">
|
||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||
</p>
|
||||
<div key={i} className="cr-code min-w-0">
|
||||
<CellView cell={cell} index={i} edits={item.edits} parts={mark?.cells?.[i]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
|
||||
@@ -177,10 +255,10 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
{toggle && i === 0
|
||||
? (
|
||||
<button type="button" className="cr-item-toggle" aria-expanded={toggle.open} aria-label={`${toggle.open ? '收起' : '展开'}${inlineText(cell)}`} onClick={toggle.onToggle}>
|
||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||
<CellView cell={cell} index={i} edits={item.edits} parts={mark?.cells?.[i]} />
|
||||
</button>
|
||||
)
|
||||
: <CellText cell={cell} parts={mark?.cells?.[i]} />}
|
||||
: <CellView cell={cell} index={i} edits={item.edits} parts={mark?.cells?.[i]} />}
|
||||
</>
|
||||
)
|
||||
const tint = toggle?.open && toggle.badge
|
||||
@@ -191,11 +269,11 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
{cells.map((cell, i) => item.table && i === cells.length - 1
|
||||
? (
|
||||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||||
<p className="cr-code">{text(cell, i)}</p>
|
||||
<div className="cr-code">{text(cell, i)}</div>
|
||||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
|
||||
</div>
|
||||
)
|
||||
: <p key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</p>)}
|
||||
: <div key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</div>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,11 +68,13 @@ body.vscode-high-contrast-light {
|
||||
:is([data-kind="import"], [data-kind="copy"]) { --kind: var(--cr-kind-import); }
|
||||
[data-kind="function"] { --kind: var(--cr-kind-function); }
|
||||
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
|
||||
[data-kind="trait"] { --kind: var(--cr-storage); }
|
||||
[data-kind="impl"] { --kind: var(--cr-kind-function); }
|
||||
[data-kind="enum"] { --kind: var(--cr-kind-enum); }
|
||||
[data-kind="exception"] { --kind: var(--cr-kind-exception); }
|
||||
:is([data-kind="field"], [data-kind="const"], [data-kind="variable"], [data-kind="label"]) { --kind: var(--cr-kind-data); }
|
||||
:is([data-kind="if"], [data-kind="else"], [data-kind="case"]) { --kind: var(--cr-kind-branch); }
|
||||
[data-kind="stage"] { --kind: var(--cr-kind-stage); }
|
||||
:is([data-kind="stage"], [data-kind="module"]) { --kind: var(--cr-kind-stage); }
|
||||
[data-kind="loop"] { --kind: var(--cr-kind-loop); }
|
||||
:is([data-kind="try"], [data-kind="catch"], [data-kind="finally"], [data-kind="health"]) { --kind: var(--cr-kind-error); }
|
||||
:is([data-kind="return"], [data-kind="start"]) { --kind: var(--cr-kind-return); }
|
||||
|
||||
@@ -63,4 +63,24 @@ describe('同一行不会同时在两处能改', () => {
|
||||
const plain = revealPlan(BLOCKS, 15, 999)
|
||||
expect(plain && uncoveredPlan(plain, covered)).toEqual(plain)
|
||||
})
|
||||
|
||||
it('格子里单独打开的一段,和盖住它的块、跟它重叠的另一段不能同时编辑', () => {
|
||||
const snippet = (startLine: number, endLine: number) => ({ startLine, endLine, code: 'x', codeTokens: [] })
|
||||
const card = block('1', 'react.component', [0, 20])
|
||||
card.properties = [{
|
||||
label: '状态',
|
||||
value: [],
|
||||
items: [{
|
||||
cells: [[{ text: 'a', role: 'plain' }], [{ text: 'b', role: 'plain' }]],
|
||||
edits: [
|
||||
{ cell: 0, from: 0, to: 1, snippet: snippet(4, 6) },
|
||||
{ cell: 1, from: 0, to: 1, snippet: snippet(6, 7) },
|
||||
],
|
||||
}],
|
||||
}]
|
||||
const blocks = [card, block('2', 'function', [30, 32])]
|
||||
expect(startEditing(blocks, ['1~0', '2'], '1')).toEqual(['2', '1'])
|
||||
expect(startEditing(blocks, ['1', '2'], '1~0')).toEqual(['2', '1~0'])
|
||||
expect(startEditing(blocks, ['1~0', '2'], '1~1')).toEqual(['2', '1~1'])
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { BlockView } from './types'
|
||||
import type { BlockView, CellEdit, PropertyItem } from './types'
|
||||
|
||||
/**
|
||||
* 同一时刻能编辑的几段源码不重叠,同一行不会同时在两处能改:
|
||||
* ① 块的编辑范围是它的完整源码,子孙都在它的编辑框里:块编辑时它的子孙都不画;
|
||||
* ② 点「编辑」时,编辑范围和这块重叠的别的块(祖先、子孙,`} else {` 这样共用一行的兄弟块)退出编辑。
|
||||
* ② 点「编辑」时,编辑范围和这块重叠的别的块(祖先、子孙,`} else {` 这样共用一行的兄弟块)退出编辑;
|
||||
* ③ 属性表格子里单独打开的那一段(见 CellEdit)行号落在组件块里面,和块同一条规矩:重叠就不能同时编辑。
|
||||
* 组件正在编辑时预览不画,里面这段的编辑器也不画。
|
||||
*/
|
||||
|
||||
type Lines = Pick<BlockView, 'startLine' | 'endLine'>
|
||||
@@ -41,12 +43,51 @@ function* allBlocks(blocks: readonly BlockView[]): Generator<BlockView> {
|
||||
}
|
||||
|
||||
/** 点了 id 这块的「编辑」之后正在编辑的块:加上它,编辑范围和它重叠的别的块退出编辑。 */
|
||||
export interface SnippetRef {
|
||||
id: string
|
||||
edit: CellEdit
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
|
||||
/** 各块属性表里能单独打开的源码,按画出来的顺序编号。id 是「块 id~序号」。 */
|
||||
export function snippetRefs(blocks: readonly BlockView[]): SnippetRef[] {
|
||||
const refs: SnippetRef[] = []
|
||||
const take = (blockId: string, items: PropertyItem[] | undefined, n: number): number => {
|
||||
for (const item of items ?? []) {
|
||||
for (const edit of item.edits ?? []) {
|
||||
refs.push({ id: `${blockId}~${n}`, edit, startLine: edit.snippet.startLine, endLine: edit.snippet.endLine })
|
||||
n += 1
|
||||
}
|
||||
n = take(blockId, item.items, n)
|
||||
n = take(blockId, item.table?.items, n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
for (const block of allBlocks(blocks)) {
|
||||
let n = 0
|
||||
if ('properties' in block) {
|
||||
for (const row of block.properties)
|
||||
n = take(block.id, row.items, n)
|
||||
}
|
||||
else if (block.table) {
|
||||
take(block.id, block.table.items, 0)
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
/** 点了 id 这段的「编辑」之后正在编辑的几段:加上它,行重叠的别的段(块,或格子里的一段源码)退出编辑。 */
|
||||
export function startEditing(blocks: readonly BlockView[], editing: readonly string[], id: string): string[] {
|
||||
const byId = new Map([...allBlocks(blocks)].map(block => [block.id, block]))
|
||||
const byId = new Map<string, Lines>()
|
||||
for (const block of allBlocks(blocks))
|
||||
byId.set(block.id, block)
|
||||
for (const ref of snippetRefs(blocks))
|
||||
byId.set(ref.id, ref)
|
||||
const target = byId.get(id)
|
||||
const others = editing.filter((other) => {
|
||||
const block = byId.get(other)
|
||||
return other !== id && !(target && block && overlaps(target, block))
|
||||
const span = byId.get(other)
|
||||
return other !== id && !(target && span && overlaps(target, span))
|
||||
})
|
||||
return [...others, id]
|
||||
}
|
||||
@@ -21,6 +21,9 @@ describe('块的种类图标', () => {
|
||||
import: 'import',
|
||||
function: 'square-function',
|
||||
class: 'box',
|
||||
trait: 'network',
|
||||
impl: 'square-function',
|
||||
module: 'layers',
|
||||
type: 'braces',
|
||||
enum: 'list-ordered',
|
||||
exception: 'triangle-alert',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export type BlockKind
|
||||
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
|
||||
| 'trait' | 'impl' | 'module'
|
||||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||||
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
|
||||
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
|
||||
@@ -65,11 +66,30 @@ export interface SourceRange { startIndex: number, endIndex: number }
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 能单独打开编辑的一段源码:整行切好、着色好,和块的 code / codeTokens 同一套。网页不自己切 */
|
||||
export interface SourceSnippet {
|
||||
/** 起止行,0 起,含头含尾 */
|
||||
startLine: number
|
||||
endLine: number
|
||||
code: string
|
||||
codeTokens: Segment[]
|
||||
}
|
||||
|
||||
/** 属性表一个子行的某一格里,能单独打开的一段源码。cell 是第几格,from / to 是这一格文字里这一处(含头不含尾) */
|
||||
export interface CellEdit {
|
||||
cell: number
|
||||
from: number
|
||||
to: number
|
||||
snippet: SourceSnippet
|
||||
}
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值 */
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: Inline[][]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
||||
items?: PropertyItem[]
|
||||
/** 这一行里能单独打开的几段源码。打开后只出现那一段。没有能打开的就不写 */
|
||||
edits?: CellEdit[]
|
||||
/** 排在这一行最后一格里、那格文字下面的表(实参的值里跨几行写的调用的实参表),表里的行又可以带 table;总是展开 */
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user