✨ feat(prototype): 代码阅读原型加调用与构造:跨多行的调用把实参摊成表(参数 / 值),查定义分新建和调用;文件注释的侧栏做成共用组件
This commit is contained in:
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
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user