✨ feat(prototype): 代码阅读原型加调用与构造:跨多行的调用把实参摊成表(参数 / 值),查定义分新建和调用;文件注释的侧栏做成共用组件

This commit is contained in:
cruldra committed 2026-09-27 21:42:18 +08:00
1 parent ab018e5902
commit ebf228cdc7
3 files changed
+189 -30

No files matched your search

@@ -27,6 +27,7 @@
| `Prototype / 枚举` | luffy 的 `referral/models.py` 里的 `CommissionStatus` | 枚举卡片:名称 / 访问级别 / 继承,成员收成一张表(成员 / 值 / 说明),说明取行尾注释;Python 用库规则(导入了 enum 才启用),TypeScript 的 enum 由语言插件报结构,画成同一张卡片 |
| `Prototype / 库规则 · Celery` | luffy 的 `backend/src/luffy_agent/celery_app.py` | 库规则(导入了 celery 才启用)两张卡片:「Celery 应用」名称 / 应用名 / 消息队列 / 结果存储;「定时任务」应用 + 任务表(任务 / 频率 / 说明),频率换算成「每 5 分钟」「每天」,说明取行尾注释,`**` 展开的另起一行 |
| `Prototype / 库规则 · FastAPI 接口` | luffy 的 `user/routes.py` 里的 `router` 和 `update_user` | 路由器(`APIRouter(...)`)和接口是两张卡片。「路由器」卡片:名称 / 前缀 / 标签(写了依赖、默认响应再加行),接口表列这个文件里挂在它上面的全部接口(方法 / 完整路径 / 函数 / 说明),函数名 Cmd+单击跳到接口卡片。库规则(导入了 fastapi 才启用)把挂在路由对象上的函数整块换成「接口」卡片,函数体照常是子块:名称 / 说明 / 路由器 / 标签 / 异步 / 依赖(`Depends(...)` 注入的参数),然后两张表。请求表一项一行:URL(路由对象的 prefix + 装饰器里的路径)/ 方法 / 路径参数 / 查询参数 / 请求体,没有的项不列。响应表每个状态码一行(状态码 / 内容 / 说明):成功那行来自 `status_code` 和 `response_model`,`responses={…}` 里写的各占一行,函数体里直接抛的异常按异常类上的 `status_code` 归行、说明列出抛出时的消息,有参数时 FastAPI 自动带的 422 淡色。挂到应用上时再叠的前缀(`/api/v1`)在别的文件里,不算 |
| `Prototype / 调用 · 构造`(`routes.py · 预览` 的块树里也是这样) | routes.py 的 `return UserDetailResponse(...)`、`user = await user_service.update_user(...)`、`avatar_url = await user_service.upload_avatar(...)` | 跨了多行写的调用把实参摊成一张表(参数 / 值,没名字的写「第 N 个」):返回里写「返回 新建 UserDetailResponse · 11 个参数」,变量的「初始值」写「等待 调用 user_service.update_user · 10 个参数」。被调用的名字是类就是「新建」,是函数就是「调用」(插件查定义,原型按首字母大写);一行写完的调用照旧一行 |
| `Prototype / 块种类图标` | — | 每种块一个图标、颜色、名称的图例,库规则带的种类后面淡色写规则名;最上面一块「返回」停着鼠标、图标下面弹出名称提示 |
| `Prototype / 遍历 · 推导式` | luffy 邀请码工具的 `"".join(secrets.choice(ALPHABET) for _ in range(length))`,外加带条件的列表、字典推导 | 写在表达式里的遍历原地套一个带循环图标和结果类型(生成器 / 列表 / 字典)的小框,框里按读的顺序重排:「遍历 range(length) 的每个 _,仅当 …,取 …」 |
| `Prototype / 侧栏 · 复制菜单` | 同上 | 悬停行行尾第一个按钮「复制」(带小箭头),点开一组复制方式:文件名 / 文件名(不带后缀)/ 相对路径 / 绝对路径,右边淡色预览会复制出什么;文件夹没有「不带后缀」 |
@@ -69,6 +70,7 @@
- `Reader / TableCard`:SQLAlchemy 规则认出的数据表卡片。
- `Reader / EnumCard`:枚举卡片。
- `Reader / CeleryAppCard`、`Reader / BeatCard`:Celery 规则的两张卡片。
- `Reader / CallBlock`:返回、语句里跨多行的调用,同样的调用共用一个;`Reader / ArgRow`:`Part=header | row`,实参表的一行,表格一行一个实例。
- `Reader / RouterCard`、`Reader / RouteCard`:FastAPI 规则的路由器卡片和接口卡片。
- `Reader / IterationBlock`:`Sample=generator | list | dict`,三种推导式的返回块。
- `Reader / FilterChip`:`State=default | active`,筛选栏里一个属性的下拉框,各筛选栏共用。
+153 -29
View File
@@ -157,6 +157,11 @@ const NOTES = {
"routes.py": "用户资料 / 头像接口",
};
const NOTE_HOVER = "order_bonus.py";
// 文件注释那几张画板的侧栏只有两种:填注释前后(鼠标停在 campaign,只有 routes.py 有注释)、已保存(鼠标停在 order_bonus.py,全部注释)
const NOTE_SIDEBARS = {
draft: { hover: NOTE_DRAFT.row, notes: { "routes.py": NOTES["routes.py"] } },
saved: { hover: NOTE_HOVER, notes: NOTES },
};
const EXT_LABEL = { ts: "TS", tsx: "TSX", json: "{}", md: "M↓", py: "PY", toml: "TM" };
@@ -985,6 +990,98 @@ function setText(root, name, characters) {
named(root, name).characters = characters;
}
// ─── 调用与构造 ──────────────────────────────────────────────────────────────
//
// 跨了多行写的调用(一般是实参多),实参摊成一张表(参数 / 值);一行写完的调用照旧一行译文。
// 被调用的名字查定义:是类就是「新建」(构造对象),是函数就是「调用」,查不到按调用算。Python 里构造对象写法上就是调用,只能靠定义分;
// TypeScript 的 new X(...) 本身就是新建。原型里按名字首字母大写当类。
// 调用在变量的初始值里,表放在「初始值」下面;在返回、语句里,块里先写一行「返回 新建 X」,下面是表。
// 没名字的实参按位置写「第 N 个」;值照常译,实参里嵌套的调用不再展开。
const CALL_ARG_KEY_W = 180;
/** 跨多行的调用拆出被调用的名字、有没有 await、各个实参;不是跨多行的调用返回 null。 */
function parseCall(code) {
if (!code.includes("\n")) return null;
const match = /^(await\s+)?([\w.]+)\(([\s\S]*)\)$/.exec(code.trim());
if (!match) return null;
const [, awaited, callee, inner] = match;
const cuts = [-1, ...topLevelIndexes(inner, ","), inner.length];
let position = 0;
const args = cuts.slice(1).map((end, index) => inner.slice(cuts[index] + 1, end).trim()).filter(Boolean).map((part) => {
const keyword = /^(\w+)\s*=\s*([\s\S]+)$/.exec(part);
if (keyword) return [keyword[1], keyword[2].replace(/\s+/g, " ")];
position += 1;
return [`第 ${position} 个`, part.replace(/\s+/g, " ")];
});
return { awaited: Boolean(awaited), callee, construct: /^[A-Z]/.test(callee.split(".").pop()), args };
}
/** 「等待 调用 user_service.update_user」「新建 UserDetailResponse」,后面淡色写几个参数。 */
function callHead(call) {
const parts = call.callee.split(".");
const last = parts.pop();
return [
...(call.awaited ? [{ text: "等待 ", color: COLORS.tokenControl }] : []),
{ text: call.construct ? "新建 " : "调用 ", color: COLORS.tokenStorage },
...parts.map((part) => ({ text: `${part}.`, color: COLORS.tokenVariable })),
{ text: last, color: call.construct ? COLORS.tokenType : COLORS.tokenFunction },
{ text: ` · ${call.args.length} 个参数`, color: COLORS.desc },
];
}
/** Reader / ArgRow — Part=header | row:实参表的一行,两格(参数 / 值),字在实例里改。表格一行一个实例,不用一格一格地画。 */
function argRowComponent(ctx, part) {
const header = part === "header";
const node = frame(ctx, `Part=${part}`, { component: true, dir: "HORIZONTAL", width: TREE_W - 120, fill: header ? COLORS.side : null });
if (!header) {
node.strokes = paint(COLORS.borderStrong);
node.strokeTopWeight = 1;
node.strokeBottomWeight = node.strokeLeftWeight = node.strokeRightWeight = 0;
}
const key = append(node, frame(ctx, "参数", { width: CALL_ARG_KEY_W, px: 8, py: 3 }));
key.layoutSizingVertical = "FILL";
key.strokes = paint(COLORS.borderStrong);
key.strokeRightWeight = 1;
key.strokeTopWeight = key.strokeBottomWeight = key.strokeLeftWeight = 0;
richText(ctx, key, "Name", [{ text: "参数", color: header ? COLORS.desc : COLORS.tokenVariable }]);
const value = filled(ctx, node, "值", { px: 8, py: 3 });
richText(ctx, value, "Value", [{ text: "值", color: COLORS.desc }]);
return node;
}
/** 实参表:表头 + 每个实参一行(Reader / ArgRow 的实例)。 */
function argsTable(ctx, parent, args) {
const wrap = filled(ctx, parent, "Args", {});
wrap.paddingLeft = 5;
const table = append(wrap, frame(ctx, "Table", { width: 10, radius: 4, stroke: COLORS.borderStrong }), true);
table.clipsContent = true;
const header = ctx.lib.argRow.get("header").createInstance();
header.name = "Header";
append(table, header, true);
for (const [name, value] of args) {
const row = ctx.lib.argRow.get("row").createInstance();
row.name = `Arg / ${name}`;
append(table, row, true);
const key = named(row, "Name");
writeText(ctx, key, name, ctx.fonts.mono);
key.fills = paint(name.startsWith("第 ") ? COLORS.desc : COLORS.tokenVariable);
const translated = translateCode(value, "python");
const cell = named(row, "Value");
writeText(ctx, cell, translated.text, ctx.fonts.mono);
cell.fills = paint(COLORS.fg);
paintRanges(cell, translated.ranges, COLORS.fg);
}
}
/** Reader / CallBlock — Call=<被调用的名字>:返回、语句里跨多行的调用,块里一行「返回 新建 X」,下面是实参表。 */
function callBlock(ctx, kind, call, name) {
const { node, content } = cardShell(ctx, name, BLOCK_KINDS[kind], { kids: false });
const lead = kind === "return" ? [{ text: "返回 ", color: COLORS.tokenControl }] : [];
richText(ctx, content, "Head", [...lead, ...callHead(call)]);
argsTable(ctx, content, call.args);
return { node };
}
// ─── 库规则:SQLAlchemy 数据表 ────────────────────────────────────────────────
//
// 库规则按库注册,文件导入了这个库才启用。SQLAlchemy 规则把模型类(父类链里有模型基类、字段用 mapped_column)
@@ -1515,8 +1612,9 @@ function blockLevel(ctx, parent, lib, fileKey, nodes, depth, placed, name = `Lev
for (const item of nodes) {
const state = item.edit ? "edit" : "preview";
const line = lineRange(item.at)[0];
if (lib.card.has(`${fileKey}:${line}`)) {
const card = lib.card.get(`${fileKey}:${line}`).get(state).node.createInstance();
const component = lib.card.has(`${fileKey}:${line}`) ? lib.card.get(`${fileKey}:${line}`).get(state) : lib.callBlockAt.get(`${fileKey}:${line}`);
if (component) {
const card = component.node.createInstance();
card.name = `Block / L${line}`;
append(list, card, true);
placed.push({ item, instance: card, depth });
@@ -1670,7 +1768,12 @@ function variableRows(ctx, content, fileKey, item, topLevel) {
propRow(ctx, content, "名称", [{ text: spec.name, color: constant ? COLORS.tokenConstant : COLORS.tokenVariable }]);
if (spec.access) propRow(ctx, content, "访问级别", [{ text: spec.access, color: COLORS.tokenControl }]);
if (spec.type) propRow(ctx, content, "类型", typeSegments(spec.type, lang));
propRow(ctx, content, item.kind === "const" ? "值" : "初始值", segmentsOf(translateCode(spec.value, lang)));
const call = lang === "python" ? parseCall(blockCode(fileKey, item.at).replace(/^\s*\w+\s*=\s*/, "")) : null;
if (call) {
propRow(ctx, content, "初始值", callHead(call), { twisty: true });
argsTable(ctx, content, call.args);
}
else propRow(ctx, content, item.kind === "const" ? "值" : "初始值", segmentsOf(translateCode(spec.value, lang)));
return spec.name;
}
@@ -1999,12 +2102,15 @@ function createComponents(ctx, page) {
lib.quickInput = make("Window / QuickInput", ["typing"], 1, () => quickInput(ctx));
lib.explorerHeader = make("Explorer / Header", ["default", "review"], 2, (state) => explorerHeader(ctx, state));
lib.sidebar = make("Explorer / Sidebar", fileKeys, 3, (fileKey) => sidebarComponent(ctx, lib, fileKey));
lib.notesSidebar = make("Explorer / NotesSidebar", Object.keys(NOTE_SIDEBARS), 2,
(key) => sidebarComponent(ctx, lib, "routes", NOTE_SIDEBARS[key], `Notes=${key}`));
lib.reviewSidebar = make("Explorer / ReviewSidebar", [REVIEW.file], 1, (fileKey) => sidebarComponent(ctx, lib, fileKey, { review: true }));
lib.windowTitle = make("Window / TitleBar", ["active"], 1, () => windowTitle(ctx));
lib.activity = make("Window / ActivityBar", ["reader"], 1, () => activityBar(ctx));
lib.status = make("Window / StatusBar", MODES, 1, (mode) => statusBar(ctx, mode));
lib.editorTitle = make("Editor / TitleBar", MODES, 2, (mode) => editorTitle(ctx, lib, mode));
lib.breadcrumb = make("Editor / Breadcrumb", fileKeys, 1, (fileKey) => breadcrumb(ctx, fileKey));
lib.argRow = make("Reader / ArgRow", ["header", "row"], 1, (part) => argRowComponent(ctx, part));
lib.card = new Map();
const setNames = new Set();
for (const [fileKey, nodes] of Object.entries(CODE_TREE)) {
@@ -2021,6 +2127,21 @@ function createComponents(ctx, page) {
});
});
}
// 同样的调用(比如两处一样的 return UserDetailResponse(...))共用一个组件
const calls = new Map();
for (const [fileKey, nodes] of Object.entries(CODE_TREE)) {
walkTree(nodes, (item) => {
if (!["return", "statement"].includes(item.kind)) return;
const code = blockCode(fileKey, item.at).replace(/^\s*return\s+/, "");
const call = parseCall(code);
if (call) calls.set(code, { kind: item.kind, call, at: [...(calls.has(code) ? calls.get(code).at : []), `${fileKey}:${lineRange(item.at)[0]}`] });
});
}
const callBlocks = make("Reader / CallBlock", [...calls.keys()], 1, (code) => {
const entry = calls.get(code);
return callBlock(ctx, entry.kind, entry.call, `Call=${entry.call.callee}`);
}, (item) => item.node);
lib.callBlockAt = new Map([...calls].flatMap(([code, entry]) => entry.at.map((key) => [key, callBlocks.get(code)])));
lib.tree = make("Reader / BlockTree", fileKeys, 2, (fileKey) => blockTree(ctx, lib, fileKey), (item) => item.node);
return { lib, sets };
}
@@ -2039,23 +2160,20 @@ function rowVariant(item, selected, hover) {
/**
* 侧栏:「代码阅读」视图,外观照搬 VS Code 资源管理器。
* 普通窗口放 Explorer / Sidebar 的实例(每个示例文件一个变体,窗口之间共用);
* 文件注释那几张要改行的悬停状态和注释,单独画。o.hover:鼠标停在哪一行;o.notes:各行的注释。
* o.review:审查工单时的侧栏(Explorer / ReviewSidebar),树里只有 PR 改过的文件。
* 窗口里放的都是侧栏组件的实例,窗口之间共用:Explorer / Sidebar(每个示例文件一个变体);
* o.review:审查工单时的侧栏(Explorer / ReviewSidebar);o.notes:文件注释那几张的侧栏(Explorer / NotesSidebar,见 NOTE_SIDEBARS)。
* rows:示例文件 → 那一行;有鼠标停着的行时另记一个 "hover"。
*/
function sidebar(ctx, parent, lib, selected, o = {}) {
if (!o.hover && !o.notes) {
const instance = (o.review ? lib.reviewSidebar : lib.sidebar).get(selected).createInstance();
instance.name = "Sidebar";
append(parent, instance);
instance.layoutSizingVertical = "FILL";
const rows = new Map(sidebarRows(selected, o).filter((item) => item.file)
.map((item) => [item.file, instance.findOne((node) => node.name === `Row / ${item.name}`)]));
return { pane: instance, rows };
}
const pane = append(parent, frame(ctx, "Sidebar", { width: SIDEBAR_W, fill: COLORS.side }));
pane.layoutSizingVertical = "FILL";
return { pane, rows: sidebarContent(ctx, pane, lib, selected, o) };
const set = o.notes ? lib.notesSidebar.get(o.notes) : (o.review ? lib.reviewSidebar : lib.sidebar).get(selected);
const instance = set.createInstance();
instance.name = "Sidebar";
append(parent, instance);
instance.layoutSizingVertical = "FILL";
const row = (name) => instance.findOne((node) => node.name === `Row / ${name}`);
const rows = new Map(sidebarRows(selected, o).filter((item) => item.file).map((item) => [item.file, row(item.name)]));
if (o.notes) rows.set("hover", row(NOTE_SIDEBARS[o.notes].hover));
return { pane: instance, rows };
}
function sidebarRows(selected, o) {
@@ -2066,8 +2184,8 @@ function sidebarRows(selected, o) {
* Explorer / Sidebar — File=<文件>:选中这个文件时的整个侧栏。
* Explorer / ReviewSidebar — File=<文件>:审查工单时的侧栏,标题栏写工单号,下面一行工单标题和 PR(视图的 message),树里只有 PR 改过的文件。
*/
function sidebarComponent(ctx, lib, selected, o = {}) {
const node = frame(ctx, `File=${selected}`, { component: true, width: SIDEBAR_W, height: MAIN_H, fill: COLORS.side });
function sidebarComponent(ctx, lib, selected, o = {}, name = `File=${selected}`) {
const node = frame(ctx, name, { component: true, width: SIDEBAR_W, height: MAIN_H, fill: COLORS.side });
sidebarContent(ctx, node, lib, selected, o);
return node;
}
@@ -2385,6 +2503,14 @@ async function build(api, stage = () => {}) {
const routeInstance = lib.route.get("preview").createInstance();
routeInstance.name = `Block / ${ROUTE_SAMPLE.name}`;
append(routeBoard, routeInstance, true);
// 调用与构造:块树里这几块在画板下方被截掉,单独摆出来看
const callBoard = frame(ctx, "Prototype / 调用 · 构造", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 8, fill: COLORS.editor });
page.appendChild(callBoard);
for (const [key, component] of [["routes:83", lib.card.get("routes:83").get("preview")], ["routes:45", lib.callBlockAt.get("routes:45")]]) {
const instance = component.node.createInstance();
instance.name = `Block / ${key}`;
append(callBoard, instance, true);
}
const kindBoard = kindLegend(ctx, page, lib);
const iteration = frame(ctx, "Prototype / 遍历 · 推导式", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 8, fill: COLORS.editor });
page.appendChild(iteration);
@@ -2394,15 +2520,13 @@ async function build(api, stage = () => {}) {
append(iteration, instance, true);
}
const notes = {
hover: windowBoard(ctx, page, lib, "文件注释 · 悬停出按钮", { file: "routes", mode: "preview",
hover: NOTE_DRAFT.row, notes: { "routes.py": NOTES["routes.py"] },
hover: windowBoard(ctx, page, lib, "文件注释 · 悬停出按钮", { file: "routes", mode: "preview", notes: "draft",
tooltip: { row: NOTE_DRAFT.row, align: "right", lines: ["添加注释"] } }),
typing: windowBoard(ctx, page, lib, "文件注释 · 填写", { file: "routes", mode: "preview",
hover: NOTE_DRAFT.row, notes: { "routes.py": NOTES["routes.py"] },
typing: windowBoard(ctx, page, lib, "文件注释 · 填写", { file: "routes", mode: "preview", notes: "draft",
quickInput: { title: `添加注释 · ${NOTE_DRAFT.row}`, value: NOTE_DRAFT.text } }),
saved: windowBoard(ctx, page, lib, "文件注释 · 已保存", { file: "routes", mode: "preview", hover: NOTE_HOVER, notes: NOTES,
saved: windowBoard(ctx, page, lib, "文件注释 · 已保存", { file: "routes", mode: "preview", notes: "saved",
tooltip: { row: NOTE_HOVER, lines: [`backend/src/luffy_agent/campaign/${NOTE_HOVER}`, NOTES[NOTE_HOVER]] } }),
copy: windowBoard(ctx, page, lib, "侧栏 · 复制菜单", { file: "routes", mode: "preview", hover: NOTE_HOVER, notes: NOTES,
copy: windowBoard(ctx, page, lib, "侧栏 · 复制菜单", { file: "routes", mode: "preview", notes: "saved",
copyMenu: { row: NOTE_HOVER, relative: `backend/src/luffy_agent/campaign/${NOTE_HOVER}`,
absolute: `/Users/cruldra/Sources/luffy-agent-platform/backend/src/luffy_agent/campaign/${NOTE_HOVER}` } }),
};
@@ -2453,7 +2577,7 @@ async function build(api, stage = () => {}) {
stage("排布画板");
const screens = [...Object.values(boards).flatMap((pair) => [pair.edit.screen, pair.preview.screen]), narrow.screen,
...Object.values(notes).map((board) => board.screen), ...Object.values(filters).map((board) => board.screen),
...Object.values(review).map((board) => board.screen), iteration, tableBoard, enumBoard, celeryBoard, routeBoard, kindBoard, askBoard];
...Object.values(review).map((board) => board.screen), iteration, tableBoard, enumBoard, celeryBoard, routeBoard, callBoard, kindBoard, askBoard];
let y = 0;
for (const screen of screens) {
screen.x = 0;
@@ -2472,7 +2596,7 @@ async function build(api, stage = () => {}) {
// 不选中任何画板(选中会把它在图层面板里层层展开),顶层全部收起
for (const node of page.children) node.expanded = false;
api.viewport.scrollAndZoomIntoView([start]);
return { clock, page, components, boards, narrow, notes, filters, review, iteration, tableBoard, enumBoard, celeryBoard, routeBoard, kindBoard, askBoard, screens };
return { clock, page, components, boards, narrow, notes, filters, review, iteration, tableBoard, enumBoard, celeryBoard, routeBoard, callBoard, kindBoard, askBoard, screens };
}
/** 各阶段用时,写进结束提示,慢了看得出慢在哪一步。 */
@@ -2506,6 +2630,6 @@ async function run(api) {
}
if (typeof module !== "undefined") {
module.exports = { ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
module.exports = { parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
}
if (typeof figma !== "undefined") void run(figma);
+34 -1
View File
@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const path = require("node:path");
const { ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { fakeFigma } = require("../../figma-lab/fake-figma");
function action(node) { return node.reactions[0].actions[0]; }
@@ -44,12 +44,14 @@ const EXPECTED_SETS = [
["Window / QuickInput", 1],
["Explorer / Header", 2],
["Explorer / Sidebar", 3],
["Explorer / NotesSidebar", 2],
["Explorer / ReviewSidebar", 1],
["Window / TitleBar", 1],
["Window / ActivityBar", 1],
["Window / StatusBar", 2],
["Editor / TitleBar", 2],
["Editor / Breadcrumb", 3],
["Reader / ArgRow", 2],
["Import / 'node:fs'", 2],
["Import / 'vscode'", 2],
["Const / ENV_PATTERN", 2],
@@ -86,6 +88,7 @@ const EXPECTED_SETS = [
["Function / upload_current_user_avatar", 2],
["Variable / payload", 2],
["Variable / avatar_url", 2],
["Reader / CallBlock", 1],
["Reader / BlockTree", 3],
];
@@ -279,6 +282,11 @@ test("build creates the variant sets, reuses them on boards and wires the flows"
assert.equal(instance.mainComponentId, lib.card.get(`${fileKey}:${lineRange(item.at)[0]}`).get(state).node.id);
continue;
}
const call = lib.callBlockAt.get(`${fileKey}:${lineRange(item.at)[0]}`);
if (call) {
assert.equal(instance.mainComponentId, call.node.id);
continue;
}
const variant = lib.block.get(`${item.kind}/${state}`).node;
assert.equal(instance.mainComponentId, variant.id);
const parsed = translateBlock(fileKey, item);
@@ -475,6 +483,31 @@ test("monospace text with Chinese is written in the CJK font first, then everyth
assert.equal(plain.rangeFonts.length, 0);
});
test("calls written over several lines spread their arguments into a table; one-line calls stay one line", async () => {
const update = parseCall(SOURCES.routes.slice(82, 94).join("\n").replace(/^\s*user = /, ""));
assert.equal(update.awaited, true);
assert.equal(update.construct, false);
assert.deepEqual(update.args.slice(0, 3), [["第 1 个", "db"], ["user_id", "user_id"], ["display_name", "body.display_name"]]);
assert.equal(update.args.length, 10);
assert.equal(parseCall("await user_service.get_user(db, user_id=user_id)"), null, "one line stays a line");
const api = fakeFigma();
const { components } = await build(api);
const { lib } = components;
// 两处一样的 return UserDetailResponse(...) 共用一个「新建」块
const construct = lib.callBlockAt.get("routes:45");
assert.equal(lib.callBlockAt.get("routes:96"), construct);
assert.equal(named(construct.node, "Head").characters, "返回 新建 UserDetailResponse · 11 个参数");
assert.equal(construct.node.findAll((node) => node.name.startsWith("Arg / ")).length, 11);
const placed = lib.tree.get("routes").placed.find(({ item }) => lineRange(item.at)[0] === 45);
assert.equal(placed.instance.mainComponentId, construct.node.id);
// 变量的初始值是跨多行的调用:「初始值」下面是实参表
const variable = lib.card.get("routes:83").get("preview").node;
assert.equal(named(named(variable, "Prop / 初始值"), "Value").characters, "等待 调用 user_service.update_user · 10 个参数");
assert.equal(named(named(variable, "Arg / 第 1 个"), "Value").characters, "db");
assert.ok(!named(lib.card.get("routes:43").get("preview").node, "Args"), "one-line call keeps its one-line value");
});
test("block kinds show as icons, the legend lists every kind with its name", async () => {
const api = fakeFigma();
const { kindBoard } = await build(api);