✨ feat(prototype): 代码阅读预览筛选栏原型
This commit is contained in:
1 parent
27c788d040
commit
4eaf2ae11d
3 files changed
+183
-15
No files matched your search
@@ -23,6 +23,9 @@
|
||||
| `Prototype / issueLock.ts · 编辑 / 预览` | `vscode/src/panel/issueLock.ts` | 中文注释块、多行返回语句块 |
|
||||
| `Prototype / routes.py · 编辑 / 预览` | luffy-agent-platform 的 `backend/src/luffy_agent/user/routes.py` | Python:带装饰器、默认值、返回类型的 FastAPI 路由函数,属性表;`is not None` → 不是 空 |
|
||||
| `Prototype / envLock.ts · 预览(编辑区变窄)` | — | 窗口只有 1000 宽时块跟着变窄,长句换行 |
|
||||
| `Prototype / 筛选 · 默认` | `envLock.ts` | 预览顶部固定一条筛选栏:「筛选」+ 当前文件出现过的属性各一个下拉框(种类、访问级别、异步、仅类型)+ 右边「共 33 块」 |
|
||||
| `Prototype / 筛选 · 按访问级别` | 同上 | 访问级别下拉菜单打开,只勾「公共」:两个私有常量和私有函数 `chmodAll`(连同函数体)藏起来;条件、返回这些没有访问级别的块照常显示;右边「已隐藏 3 块 · 清除」 |
|
||||
| `Prototype / 筛选 · 按种类` | 同上 | 种类下拉菜单打开,去掉「注释」「导入」:6 块藏起来 |
|
||||
| `Prototype / 文件注释 · 悬停出按钮` | luffy 的 `campaign` 目录 | 鼠标停在侧栏某一行,行尾出现一排操作按钮,第一个「添加注释」,按钮下面是提示 |
|
||||
| `Prototype / 文件注释 · 填写` | 同上 | 点「添加注释」后 VS Code 顶部弹出单行输入框,标题带上目录 / 文件名;回车保存,Esc 取消,清空后保存即删除 |
|
||||
| `Prototype / 文件注释 · 已保存` | 同上 | 有注释的目录 / 文件在名字后面灰字显示注释,太长截断;鼠标停在有注释的行上,按钮换成「编辑注释」,悬停提示里是路径和注释全文 |
|
||||
@@ -51,6 +54,7 @@
|
||||
- `Reader / ModeToggle`:编辑器右上角的「编辑 / 预览」。
|
||||
- `Explorer / TreeRow`:侧栏文件树的一行,文件夹展开 / 收起 / 展开且悬停,文件默认 / 选中 / 有改动 / 有改动且选中 / 悬停,共 8 个。名字后面的注释层默认藏起来,有注释的实例里打开;悬停状态行尾带「添加注释」「编辑注释」两个按钮,实例里按有没有注释显示其中一个。
|
||||
- `Explorer / Header`:侧栏顶部。
|
||||
- `Reader / FilterBar`:`Active=none | access | kind`,预览顶部的筛选栏,打开的下拉菜单是勾选列表,每个值后面是块数。
|
||||
- `Window / QuickInput`:VS Code 顶部弹出的单行输入框,标题和内容在实例里改。
|
||||
- `Window / TitleBar`、`Window / ActivityBar`、`Window / StatusBar`、`Editor / TitleBar`、`Editor / Breadcrumb`:VS Code 窗口外壳。
|
||||
|
||||
@@ -70,6 +74,7 @@
|
||||
- 块右边「编辑」→ 这一块换成源码;「预览」→ 换回译文。
|
||||
- 编辑器右上角「编辑」→ 整个文件回到 VS Code 编辑器;「预览」→ 回到代码块树。
|
||||
- 侧栏点 `envLock.ts` / `issueLock.ts` → 在编辑器里打开那个文件。
|
||||
- 从「筛选」起点开始:点「访问级别」「种类」→ 打开各自的下拉菜单并按勾选筛过;点「清除」→ 回到不筛。
|
||||
- 从「文件注释」起点开始:点 `campaign` 行尾的「添加注释」→ 顶部输入框;点输入框 → 保存后的侧栏。
|
||||
- 编辑区和侧栏可以上下滚动。
|
||||
|
||||
@@ -94,5 +99,6 @@ node --test test.js
|
||||
- [ ] 译文里的关键字、变量名、函数名颜色和编辑器里对应的代码一致,关键字前后都有空格
|
||||
- [ ] 有子块的块左边有折叠箭头,没有子块的没有,标签仍然对齐
|
||||
- [ ] 属性表:函数、导入、常量、变量的属性名一列对齐;导入项每项一行,左边竖线;输入参数下每个参数一行,名称 / 类型 / 默认值三列对齐,左边竖线从第一个参数连到最后一个;长类型(`Annotated[...]`)换行不挤掉默认值列
|
||||
- [ ] 筛选:筛选栏固定在预览顶部、和代码块左右对齐;下拉菜单盖在代码块上面;藏掉的块不留空位,左边层级竖线跟着变短
|
||||
- [ ] 文件注释:悬停行的按钮贴在行尾、不挤掉名字;注释灰字紧跟名字、长了截断;顶部输入框居中,看得出是 VS Code 自带的样子
|
||||
- [ ] 演示:块的编辑 / 预览、右上角编辑 / 预览、侧栏切文件都能点通
|
||||
+142
-10
@@ -18,6 +18,9 @@ const SIDEBAR_W = 280;
|
||||
const EDITOR_W = WINDOW_W - ACTIVITY_W - SIDEBAR_W - 1;
|
||||
const TREE_W = 880;
|
||||
const QUICK_INPUT_W = 600;
|
||||
const FILTER_BAR_H = 36;
|
||||
const FILTER_LABEL_W = 52;
|
||||
const CHIP_W = 150;
|
||||
const ROW_H = 22;
|
||||
const SIDEBAR_HEADER_H = 34;
|
||||
const VIEW_PAD = 24;
|
||||
@@ -41,7 +44,8 @@ const COLORS = {
|
||||
hover: "#2a2d2e", // list.hoverBackground / toolbar.hoverBackground
|
||||
widget: "#202020", // editorHoverWidget.background / quickInput.background
|
||||
widgetBorder: "#454545", // editorHoverWidget.border / widget.border
|
||||
input: "#313131", // input.background
|
||||
input: "#313131", // input.background / dropdown.background
|
||||
link: "#4daafc", // textLink.foreground
|
||||
toggleOn: "#37373d",
|
||||
gitModified: "#e2c08d", // gitDecoration.modifiedResourceForeground
|
||||
code: "#d4d4d4",
|
||||
@@ -805,6 +809,8 @@ function dot(ctx, parent, name, color, size) {
|
||||
const LUCIDE = {
|
||||
"chevron-right": [["path", { d: "m9 18 6-6-6-6" }]],
|
||||
"chevron-down": [["path", { d: "m6 9 6 6 6-6" }]],
|
||||
"list-filter": [["path", { d: "M3 6h18" }], ["path", { d: "M7 12h10" }], ["path", { d: "M10 18h4" }]],
|
||||
"check": [["path", { d: "M20 6 9 17l-5-5" }]],
|
||||
"chevrons-down-up": [["path", { d: "m7 20 5-5 5 5" }], ["path", { d: "m7 4 5 5 5-5" }]],
|
||||
"triangle-alert": [["path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }], ["path", { d: "M12 9v4" }], ["path", { d: "M12 17h.01" }]],
|
||||
"circle-x": [["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "m15 9-6 6" }], ["path", { d: "m9 9 6 6" }]],
|
||||
@@ -891,6 +897,102 @@ function setText(root, name, characters) {
|
||||
named(root, name).characters = characters;
|
||||
}
|
||||
|
||||
// ─── 筛选 ────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 每块带几条「属性 = 值」,由语言插件从语法树换算成各语言统一的属性,翻译插件给属性和值起显示名;
|
||||
// 原型直接用显示名。筛选栏按当前文件里出现过的属性各给一个下拉框,选项是实际出现的值和数量。
|
||||
// 同一属性里勾的值满足一个就显示,不同属性要同时满足;块没有某个属性就不受它影响;块藏起来,子块一起藏。
|
||||
const FACET_ORDER = ["种类", "访问级别", "异步", "仅类型"];
|
||||
const FILTER_FILE = "envLock";
|
||||
|
||||
function blockAttributes(fileKey, item, depth) {
|
||||
const attributes = [["种类", BLOCK_KINDS[item.kind].label]];
|
||||
if (item.kind === "function") {
|
||||
const signature = parseSignature(fileKey, item);
|
||||
if (depth === 0) attributes.push(["访问级别", signature.access]);
|
||||
attributes.push(["异步", signature.async ? "是" : "否"]);
|
||||
}
|
||||
if ((item.kind === "const" || item.kind === "variable") && depth === 0) attributes.push(["访问级别", parseVariable(fileKey, item, true).access]);
|
||||
if (item.kind === "import" && FILES[fileKey].lang !== "python") attributes.push(["仅类型", parseImport(fileKey, item).typeOnly ? "是" : "否"]);
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/** 当前文件里每个属性出现过的值和块数,按 FACET_ORDER 排。 */
|
||||
function facetCounts(fileKey) {
|
||||
const facets = new Map(FACET_ORDER.map((facet) => [facet, new Map()]));
|
||||
walkTree(CODE_TREE[fileKey], (item, depth) => {
|
||||
for (const [facet, value] of blockAttributes(fileKey, item, depth)) facets.get(facet).set(value, (facets.get(facet).get(value) || 0) + 1);
|
||||
});
|
||||
return new Map([...facets].filter(([, values]) => values.size));
|
||||
}
|
||||
|
||||
/** 被筛掉的块(只记最外层那块,子块跟着藏)。allowed:属性 → 勾选的值。 */
|
||||
function filteredOut(fileKey, allowed) {
|
||||
const hidden = [];
|
||||
const visit = (nodes, depth) => {
|
||||
for (const item of nodes) {
|
||||
const out = blockAttributes(fileKey, item, depth).some(([facet, value]) => allowed[facet] && !allowed[facet].includes(value));
|
||||
if (out) hidden.push(item);
|
||||
else if (item.kids) visit(item.kids, depth + 1);
|
||||
}
|
||||
};
|
||||
visit(CODE_TREE[fileKey], 0);
|
||||
return hidden;
|
||||
}
|
||||
|
||||
/** 演示用的三种筛选:不筛;访问级别只看公共;种类里去掉注释和导入。 */
|
||||
function filterPresets() {
|
||||
const kinds = [...facetCounts(FILTER_FILE).get("种类").keys()];
|
||||
return {
|
||||
none: { allowed: {} },
|
||||
access: { facet: "访问级别", allowed: { 访问级别: ["公共"] }, chip: "访问级别:公共" },
|
||||
kind: { facet: "种类", allowed: { 种类: kinds.filter((kind) => !["注释", "导入"].includes(kind)) }, chip: `种类:已选 ${kinds.length - 2} 种` },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reader / FilterBar — Active=none | access | kind:预览顶部固定的筛选栏。
|
||||
* 左边「筛选」,中间每个属性一个下拉框(固定宽度),右边汇总和「清除」;打开的下拉菜单是勾选列表,带数量。
|
||||
*/
|
||||
function filterBar(ctx, active) {
|
||||
const facets = facetCounts(FILTER_FILE);
|
||||
const preset = filterPresets()[active];
|
||||
let total = 0;
|
||||
walkTree(CODE_TREE[FILTER_FILE], () => { total += 1; });
|
||||
const node = frame(ctx, `Active=${active}`, { component: true, dir: "HORIZONTAL", width: TREE_W, height: FILTER_BAR_H, gap: 8, align: "CENTER" });
|
||||
const label = append(node, frame(ctx, "Label", { dir: "HORIZONTAL", width: FILTER_LABEL_W, height: 20, gap: 4, align: "CENTER" }));
|
||||
append(label, icon(ctx, "list-filter", COLORS.desc, 14));
|
||||
text(ctx, label, "Text", "筛选", { hug: true, size: 12, lineHeight: 16, color: COLORS.desc });
|
||||
for (const facet of facets.keys()) {
|
||||
const on = facet === preset.facet;
|
||||
const chip = append(node, frame(ctx, `Chip / ${facet}`, { dir: "HORIZONTAL", width: CHIP_W, height: 24, px: 8, gap: 4, radius: 4,
|
||||
align: "CENTER", fill: COLORS.input, stroke: on ? COLORS.focus : COLORS.borderStrong }));
|
||||
text(ctx, chip, "Text", on ? preset.chip : facet, { size: 12, lineHeight: 16, singleLine: true, color: on ? COLORS.fgStrong : COLORS.fg });
|
||||
append(chip, icon(ctx, "chevron-down", COLORS.desc, 12));
|
||||
}
|
||||
append(node, frame(ctx, "Spacer", { width: 10, height: 1 }), true);
|
||||
const hidden = filteredOut(FILTER_FILE, preset.allowed).length;
|
||||
text(ctx, node, "Summary", active === "none" ? `共 ${total} 块` : `已隐藏 ${hidden} 块`, { hug: true, size: 12, lineHeight: 16, color: COLORS.desc });
|
||||
if (active !== "none") text(ctx, node, "Clear", "清除", { hug: true, size: 12, lineHeight: 16, color: COLORS.link });
|
||||
if (preset.facet) {
|
||||
const menu = frame(ctx, "Menu", { width: 200, py: 4, radius: 6, fill: COLORS.widget, stroke: COLORS.widgetBorder });
|
||||
for (const [value, count] of facets.get(preset.facet)) {
|
||||
const checked = preset.allowed[preset.facet].includes(value);
|
||||
const row = filled(ctx, menu, `Option / ${value}`, { dir: "HORIZONTAL", px: 10, py: 4, gap: 8, align: "CENTER" });
|
||||
const box = append(row, frame(ctx, "Checkbox", { dir: "HORIZONTAL", width: 14, height: 14, radius: 3, align: "CENTER", justify: "CENTER",
|
||||
fill: checked ? COLORS.focus : null, stroke: checked ? COLORS.focus : COLORS.dim }));
|
||||
if (checked) append(box, icon(ctx, "check", "#ffffff", 12, { weight: 2.4 }));
|
||||
text(ctx, row, "Value", value, { size: 12, lineHeight: 16, color: COLORS.fg });
|
||||
text(ctx, row, "Count", String(count), { hug: true, size: 12, lineHeight: 16, color: COLORS.dim });
|
||||
}
|
||||
node.appendChild(menu);
|
||||
menu.layoutPositioning = "ABSOLUTE";
|
||||
menu.x = FILTER_LABEL_W + 8 + [...facets.keys()].indexOf(preset.facet) * (CHIP_W + 8);
|
||||
menu.y = FILTER_BAR_H - 4;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// ─── Components: 代码块 ──────────────────────────────────────────────────────
|
||||
|
||||
/** 哪些种类的块画成属性表,以及各自的行。 */
|
||||
@@ -938,8 +1040,8 @@ function codeBlock(ctx, kind, state) {
|
||||
}
|
||||
|
||||
/** 一层代码块:左边一条竖线标出这一层,右边是这一层的块,块里嵌套的下一层紧跟在块下面。 */
|
||||
function blockLevel(ctx, parent, lib, fileKey, nodes, depth, placed) {
|
||||
const level = filled(ctx, parent, `Level ${depth}`, { dir: "HORIZONTAL", gap: 8 });
|
||||
function blockLevel(ctx, parent, lib, fileKey, nodes, depth, placed, name = `Level ${depth}`) {
|
||||
const level = filled(ctx, parent, name, { dir: "HORIZONTAL", gap: 8 });
|
||||
const guide = append(level, frame(ctx, "Guide", { width: 12, align: "CENTER" }));
|
||||
guide.layoutSizingVertical = "FILL";
|
||||
const line = ctx.api.createRectangle();
|
||||
@@ -957,7 +1059,7 @@ function blockLevel(ctx, parent, lib, fileKey, nodes, depth, placed) {
|
||||
card.name = `Block / L${line}`;
|
||||
append(list, card, true);
|
||||
placed.push({ item, instance: card, depth });
|
||||
if (item.kids) blockLevel(ctx, list, lib, fileKey, item.kids, depth + 1, placed);
|
||||
if (item.kids) blockLevel(ctx, list, lib, fileKey, item.kids, depth + 1, placed, `Children / L${line}`);
|
||||
continue;
|
||||
}
|
||||
const instance = lib.block.get(`${item.kind}/${state}`).node.createInstance();
|
||||
@@ -974,7 +1076,7 @@ function blockLevel(ctx, parent, lib, fileKey, nodes, depth, placed) {
|
||||
useCjkFont(ctx, code);
|
||||
if (!item.kids) named(instance, "Fold icon").visible = false;
|
||||
placed.push({ item, instance, depth });
|
||||
if (item.kids) blockLevel(ctx, list, lib, fileKey, item.kids, depth + 1, placed);
|
||||
if (item.kids) blockLevel(ctx, list, lib, fileKey, item.kids, depth + 1, placed, `Children / L${line}`);
|
||||
}
|
||||
return level;
|
||||
}
|
||||
@@ -1299,6 +1401,7 @@ function createComponents(ctx, page) {
|
||||
lib.toggle = make("Reader / ModeToggle", MODES, 2, (mode) => modeToggle(ctx, mode));
|
||||
lib.block = make("Reader / CodeBlock", BLOCK_VARIANTS.map(([kind, state]) => `${kind}/${state}`), 2,
|
||||
(entry) => codeBlock(ctx, ...entry.split("/")), (item) => item.node);
|
||||
lib.filterBar = make("Reader / FilterBar", ["none", "access", "kind"], 3, (active) => filterBar(ctx, active));
|
||||
lib.row = make("Explorer / TreeRow", ROW_VARIANTS.map(([type, state]) => `${type}/${state}`), 2,
|
||||
(entry) => treeRow(ctx, ...entry.split("/")));
|
||||
lib.quickInput = make("Window / QuickInput", ["typing"], 1, () => quickInput(ctx));
|
||||
@@ -1433,12 +1536,32 @@ function windowBoard(ctx, page, lib, name, o) {
|
||||
const crumbs = lib.breadcrumb.get(o.file).createInstance();
|
||||
crumbs.name = "Breadcrumb";
|
||||
append(editor, crumbs, true);
|
||||
let filter;
|
||||
if (o.filter) {
|
||||
const row = filled(ctx, editor, "Filter row", { align: "CENTER", px: VIEW_PAD, fill: COLORS.editor });
|
||||
row.strokes = paint(COLORS.border);
|
||||
row.strokeBottomWeight = 1;
|
||||
row.strokeTopWeight = row.strokeLeftWeight = row.strokeRightWeight = 0;
|
||||
filter = lib.filterBar.get(o.filter).createInstance();
|
||||
filter.name = "Filter bar";
|
||||
append(row, filter);
|
||||
// 打开的下拉菜单要盖住下面的代码块:先排的子项画在上面
|
||||
editor.itemReverseZIndex = true;
|
||||
}
|
||||
const view = o.mode === "edit" ? codeView(ctx, editor, o.file) : previewView(ctx, editor, lib, o.file, editorWidth);
|
||||
if (o.filter) {
|
||||
const tree = view.children[0];
|
||||
for (const item of filteredOut(o.file, filterPresets()[o.filter].allowed)) {
|
||||
const line = lineRange(item.at)[0];
|
||||
tree.findOne((node) => node.name === `Block / L${line}`).visible = false;
|
||||
if (item.kids) tree.findOne((node) => node.name === `Children / L${line}`).visible = false;
|
||||
}
|
||||
}
|
||||
const status = lib.status.get(o.mode).createInstance();
|
||||
if (o.mode === "edit") setText(status, "Item / TypeScript", FILES[o.file].lang === "python" ? "Python" : "TypeScript");
|
||||
status.name = "Status bar";
|
||||
append(screen, status, true);
|
||||
const board = { screen, rows: side.rows, titleBar, view };
|
||||
const board = { screen, rows: side.rows, titleBar, view, filter };
|
||||
if (o.tooltip) board.tooltip = tooltip(ctx, screen, o.tooltip, WORKSPACES[FILES[o.file].workspace].rows);
|
||||
if (o.quickInput) {
|
||||
const input = lib.quickInput.get("typing").createInstance();
|
||||
@@ -1494,6 +1617,8 @@ async function build(api, stage = () => {}) {
|
||||
};
|
||||
}
|
||||
const narrow = windowBoard(ctx, page, lib, "envLock.ts · 预览(编辑区变窄)", { file: "envLock", mode: "preview", width: WINDOW_NARROW });
|
||||
const filters = Object.fromEntries(Object.entries({ none: "默认", access: "按访问级别", kind: "按种类" }).map(([key, label]) =>
|
||||
[key, windowBoard(ctx, page, lib, `筛选 · ${label}`, { file: FILTER_FILE, mode: "preview", filter: key })]));
|
||||
const notes = {
|
||||
hover: windowBoard(ctx, page, lib, "文件注释 · 悬停出按钮", { file: "routes", mode: "preview",
|
||||
hover: NOTE_DRAFT.row, notes: { "routes.py": NOTES["routes.py"] },
|
||||
@@ -1532,9 +1657,15 @@ async function build(api, stage = () => {}) {
|
||||
await connect(named(notes.hover.rows.get("hover"), "Action / 添加注释"), notes.typing.screen, "NAVIGATE");
|
||||
await connect(named(notes.typing.quickInput, "Field"), notes.saved.screen, "NAVIGATE");
|
||||
|
||||
// 筛选:默认栏里点「访问级别」「种类」打开各自的下拉菜单;「清除」回到不筛
|
||||
await connect(named(filters.none.filter, "Chip / 访问级别"), filters.access.screen, "NAVIGATE");
|
||||
await connect(named(filters.none.filter, "Chip / 种类"), filters.kind.screen, "NAVIGATE");
|
||||
await connect(named(filters.access.filter, "Clear"), filters.none.screen, "NAVIGATE");
|
||||
await connect(named(filters.kind.filter, "Clear"), filters.none.screen, "NAVIGATE");
|
||||
|
||||
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(notes).map((board) => board.screen), ...Object.values(filters).map((board) => board.screen)];
|
||||
let y = 0;
|
||||
for (const screen of screens) {
|
||||
screen.x = 0;
|
||||
@@ -1548,10 +1679,11 @@ async function build(api, stage = () => {}) {
|
||||
setY += set.height + 64;
|
||||
}
|
||||
const start = boards.envLock.preview.screen;
|
||||
page.flowStartingPoints = [{ nodeId: start.id, name: "预览 envLock.ts" }, { nodeId: notes.hover.screen.id, name: "文件注释" }];
|
||||
page.flowStartingPoints = [{ nodeId: start.id, name: "预览 envLock.ts" }, { nodeId: notes.hover.screen.id, name: "文件注释" },
|
||||
{ nodeId: filters.none.screen.id, name: "筛选" }];
|
||||
page.selection = [start];
|
||||
api.viewport.scrollAndZoomIntoView(page.selection);
|
||||
return { page, components, boards, narrow, notes, screens };
|
||||
return { page, components, boards, narrow, notes, filters, screens };
|
||||
}
|
||||
|
||||
async function run(api) {
|
||||
@@ -1570,6 +1702,6 @@ async function run(api) {
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = { 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 = { 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);
|
||||
+35
-5
@@ -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 { 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 { 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]; }
|
||||
@@ -20,12 +20,14 @@ function countBlocks(nodes) {
|
||||
// 属性表:8 个函数、14 条导入、2 个常量、12 个变量,共 36 块各 2 条 = 72
|
||||
// 窗口:3 个文件 × 编辑 / 预览各 1 个切换 = 6;superwork 的 4 个窗口里侧栏点另一个示例文件 = 4
|
||||
// (luffy 只有一个示例文件,侧栏没有可跳的)
|
||||
// 文件注释:悬停出的「添加注释」→ 填写,输入框回车 → 已保存 = 2;合计 102
|
||||
const EXPECTED_LINKS = 102;
|
||||
// 文件注释:悬停出的「添加注释」→ 填写,输入框回车 → 已保存 = 2
|
||||
// 筛选:默认栏的「访问级别」「种类」各打开一个下拉菜单,两张筛过的画板「清除」回默认 = 4;合计 106
|
||||
const EXPECTED_LINKS = 106;
|
||||
|
||||
const EXPECTED_SETS = [
|
||||
["Reader / ModeToggle", 2],
|
||||
["Reader / CodeBlock", 18],
|
||||
["Reader / FilterBar", 3],
|
||||
["Explorer / TreeRow", 8],
|
||||
["Window / QuickInput", 1],
|
||||
["Explorer / Header", 1],
|
||||
@@ -335,6 +337,34 @@ test("file notes: hover shows the action, the quick input takes the note, the tr
|
||||
assert.equal(named(notes.saved.screen, "Line 2").characters, NOTES["order_bonus.py"]);
|
||||
});
|
||||
|
||||
test("filter bar lists the facets of the file and hides filtered blocks with their children", async () => {
|
||||
const facets = facetCounts(FILTER_FILE);
|
||||
assert.deepEqual([...facets.keys()], ["种类", "访问级别", "异步", "仅类型"]);
|
||||
assert.deepEqual(Object.fromEntries(facets.get("访问级别")), { 私有: 3, 公共: 3 });
|
||||
// 块没有某个属性就不受它影响:按访问级别筛时条件、返回这些块照常显示
|
||||
const access = filteredOut(FILTER_FILE, filterPresets().access.allowed);
|
||||
assert.deepEqual(access.map((item) => item.kind), ["const", "const", "function"]);
|
||||
|
||||
const api = fakeFigma();
|
||||
const { filters } = await build(api);
|
||||
assert.equal(named(filters.none.filter, "Summary").characters, "共 33 块");
|
||||
assert.equal(named(filters.access.filter, "Summary").characters, "已隐藏 3 块");
|
||||
assert.equal(named(filters.kind.filter, "Summary").characters, "已隐藏 6 块");
|
||||
assert.equal(named(filters.access.filter, "Menu").layoutPositioning, "ABSOLUTE");
|
||||
assert.equal(named(named(filters.access.filter, "Option / 公共"), "Checkbox").children.length, 1, "公共 is checked");
|
||||
assert.equal(named(named(filters.access.filter, "Option / 私有"), "Checkbox").children.length, 0, "私有 is unchecked");
|
||||
|
||||
const shown = (board, line) => board.view.findOne((node) => node.name === `Block / L${line}`).visible;
|
||||
assert.equal(shown(filters.none, 38), true);
|
||||
assert.equal(shown(filters.access, 38), false);
|
||||
assert.equal(filters.access.view.findOne((node) => node.name === "Children / L38").visible, false, "children hide with their block");
|
||||
assert.equal(shown(filters.access, 23), true);
|
||||
assert.equal(shown(filters.kind, 12), false);
|
||||
|
||||
assert.equal(action(named(filters.none.filter, "Chip / 访问级别")).destinationId, filters.access.screen.id);
|
||||
assert.equal(action(named(filters.kind.filter, "Clear")).destinationId, filters.none.screen.id);
|
||||
});
|
||||
|
||||
test("boards and sets sit on the page without overlapping", async () => {
|
||||
const api = fakeFigma();
|
||||
const result = await build(api);
|
||||
@@ -357,8 +387,8 @@ test("the plugin stays small enough for Figma to build quickly", async () => {
|
||||
if (node.type !== "INSTANCE") node.children.forEach(walk);
|
||||
};
|
||||
result.page.children.forEach(walk);
|
||||
// 卡死那次是 5000 多个图层;现在 3 个示例文件、36 张属性表约 2200 个
|
||||
assert.ok(owned < 2500, `page owns ${owned} layers outside instances`);
|
||||
// 卡死那次是 5000 多个图层,而且每次读宽度都逼 Figma 重排;现在 16 张画板、36 张属性表约 2600 个
|
||||
assert.ok(owned < 3000, `page owns ${owned} layers outside instances`);
|
||||
});
|
||||
|
||||
test("running twice numbers the new page and keeps the old one intact", async () => {
|
||||
|
||||
Reference in new issue
Block a user