diff --git a/docs/prototype/code-reader/figma-plugin/README.md b/docs/prototype/code-reader/figma-plugin/README.md index 6bec7d7..0112373 100644 --- a/docs/prototype/code-reader/figma-plugin/README.md +++ b/docs/prototype/code-reader/figma-plugin/README.md @@ -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`,筛选栏里一个属性的下拉框,各筛选栏共用。 diff --git a/docs/prototype/code-reader/figma-plugin/code.js b/docs/prototype/code-reader/figma-plugin/code.js index dda2a1c..aca7bb7 100644 --- a/docs/prototype/code-reader/figma-plugin/code.js +++ b/docs/prototype/code-reader/figma-plugin/code.js @@ -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); diff --git a/docs/prototype/code-reader/figma-plugin/test.js b/docs/prototype/code-reader/figma-plugin/test.js index 828eafa..36f3721 100644 --- a/docs/prototype/code-reader/figma-plugin/test.js +++ b/docs/prototype/code-reader/figma-plugin/test.js @@ -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);