✨ feat(prototype): 代码阅读推导式行内框原型
This commit is contained in:
1 parent
09f05511ff
commit
f79666175b
3 files changed
+98
-17
No files matched your search
@@ -23,6 +23,7 @@
|
||||
| `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 / 遍历 · 推导式` | luffy 邀请码工具的 `"".join(secrets.choice(ALPHABET) for _ in range(length))`,外加带条件的列表、字典推导 | 写在表达式里的遍历原地套一个带循环图标和结果类型(生成器 / 列表 / 字典)的小框,框里按读的顺序重排:「遍历 range(length) 的每个 _,仅当 …,取 …」 |
|
||||
| `Prototype / 筛选 · 默认` | `envLock.ts` | 预览顶部固定一条筛选栏:「筛选」+ 当前文件出现过的属性各一个下拉框(种类、访问级别、异步、仅类型)+ 右边「共 33 块」 |
|
||||
| `Prototype / 筛选 · 按访问级别` | 同上 | 访问级别下拉菜单打开,只勾「公共」:两个私有常量和私有函数 `chmodAll`(连同函数体)藏起来;条件、返回这些没有访问级别的块照常显示;右边「已隐藏 3 块 · 清除」 |
|
||||
| `Prototype / 筛选 · 按种类` | 同上 | 种类下拉菜单打开,去掉「注释」「导入」:6 块藏起来 |
|
||||
@@ -54,6 +55,7 @@
|
||||
- `Reader / ModeToggle`:编辑器右上角的「编辑 / 预览」。
|
||||
- `Explorer / TreeRow`:侧栏文件树的一行,文件夹展开 / 收起 / 展开且悬停,文件默认 / 选中 / 有改动 / 有改动且选中 / 悬停,共 8 个。名字后面的注释层默认藏起来,有注释的实例里打开;悬停状态行尾带「添加注释」「编辑注释」两个按钮,实例里按有没有注释显示其中一个。
|
||||
- `Explorer / Header`:侧栏顶部。
|
||||
- `Reader / IterationBlock`:`Sample=generator | list | dict`,三种推导式的返回块。
|
||||
- `Reader / FilterBar`:`Active=none | access | kind`,预览顶部的筛选栏,打开的下拉菜单是勾选列表,每个值后面是块数。
|
||||
- `Window / QuickInput`:VS Code 顶部弹出的单行输入框,标题和内容在实例里改。
|
||||
- `Window / TitleBar`、`Window / ActivityBar`、`Window / StatusBar`、`Editor / TitleBar`、`Editor / Breadcrumb`:VS Code 窗口外壳。
|
||||
@@ -99,6 +101,7 @@ node --test test.js
|
||||
- [ ] 译文里的关键字、变量名、函数名颜色和编辑器里对应的代码一致,关键字前后都有空格
|
||||
- [ ] 有子块的块左边有折叠箭头,没有子块的没有,标签仍然对齐
|
||||
- [ ] 属性表:函数、导入、常量、变量的属性名一列对齐;导入项每项一行,左边竖线;输入参数下每个参数一行,名称 / 类型 / 默认值三列对齐,左边竖线从第一个参数连到最后一个;长类型(`Annotated[...]`)换行不挤掉默认值列
|
||||
- [ ] 遍历:小框和前后文字在同一行里对齐、太长时整体换行;循环图标和「循环」块同色
|
||||
- [ ] 筛选:筛选栏固定在预览顶部、和代码块左右对齐;下拉菜单盖在代码块上面;藏掉的块不留空位,左边层级竖线跟着变短
|
||||
- [ ] 文件注释:悬停行的按钮贴在行尾、不挤掉名字;注释灰字紧跟名字、长了截断;顶部输入框居中,看得出是 VS Code 自带的样子
|
||||
- [ ] 演示:块的编辑 / 预览、右上角编辑 / 预览、侧栏切文件都能点通
|
||||
+78
-16
@@ -811,6 +811,7 @@ const LUCIDE = {
|
||||
"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" }]],
|
||||
"repeat": [["path", { d: "m17 2 4 4-4 4" }], ["path", { d: "M3 11v-1a4 4 0 0 1 4-4h14" }], ["path", { d: "m7 22-4-4 4-4" }], ["path", { d: "M21 13v1a4 4 0 0 1-4 4H3" }]],
|
||||
"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" }]],
|
||||
@@ -1084,7 +1085,7 @@ function blockLevel(ctx, parent, lib, fileKey, nodes, depth, placed, name = `Lev
|
||||
/** 一段着色文字:片段拼成一个文本节点,按片段上色,中文部分换中文字体。 */
|
||||
function richText(ctx, parent, name, segments, o = {}) {
|
||||
const node = text(ctx, parent, name, segments.map((segment) => segment.text).join("") || " ",
|
||||
{ font: ctx.fonts.mono, size: 12.5, lineHeight: 20, color: o.color || COLORS.fg, width: o.width });
|
||||
{ font: ctx.fonts.mono, size: 12.5, lineHeight: 20, color: o.color || COLORS.fg, width: o.width, hug: o.hug });
|
||||
let start = 0;
|
||||
for (const segment of segments) {
|
||||
const end = start + segment.text.length;
|
||||
@@ -1188,27 +1189,79 @@ function variableRows(ctx, content, fileKey, item, topLevel) {
|
||||
* 各自的行见 GRID_KINDS;编辑状态显示这一块的源码。行数因块而异,所以每块一个组件集。
|
||||
*/
|
||||
function gridCard(ctx, fileKey, item, state, topLevel) {
|
||||
const spec = BLOCK_KINDS[item.kind];
|
||||
const editing = state === "edit";
|
||||
const node = frame(ctx, `State=${state}`, { component: true, dir: "HORIZONTAL", width: TREE_W, py: 8, gap: 10, radius: 8,
|
||||
fill: editing ? COLORS.side : spec.color, fillOpacity: editing ? 1 : 0.07,
|
||||
stroke: editing ? COLORS.focus : spec.color, strokeOpacity: editing ? 1 : 0.35 });
|
||||
const { node, content, toggle } = cardShell(ctx, `State=${state}`, BLOCK_KINDS[item.kind], { editing, kids: Boolean(item.kids) });
|
||||
node.setSharedPluginData(NAMESPACE, "block", `${fileKey}:${lineRange(item.at)[0]}`);
|
||||
if (editing) richText(ctx, content, "Code", codeSegments(blockCode(fileKey, item.at)), { color: COLORS.code });
|
||||
const title = editing ? "" : GRID_KINDS[item.kind].rows(ctx, content, fileKey, item, topLevel);
|
||||
return { node, toggle, title };
|
||||
}
|
||||
|
||||
/** 块的外壳(组件):折叠箭头位、种类标签、内容区、右边的编辑 / 预览切换。内容由调用方往 content 里放。 */
|
||||
function cardShell(ctx, name, spec, o = {}) {
|
||||
const node = frame(ctx, name, { component: true, dir: "HORIZONTAL", width: TREE_W, py: 8, gap: 10, radius: 8,
|
||||
fill: o.editing ? COLORS.side : spec.color, fillOpacity: o.editing ? 1 : 0.07,
|
||||
stroke: o.editing ? COLORS.focus : spec.color, strokeOpacity: o.editing ? 1 : 0.35 });
|
||||
node.paddingLeft = 8;
|
||||
node.paddingRight = 10;
|
||||
node.setSharedPluginData(NAMESPACE, "block", `${fileKey}:${lineRange(item.at)[0]}`);
|
||||
const fold = append(node, frame(ctx, "Fold", { dir: "HORIZONTAL", width: 14, height: 20, align: "CENTER", justify: "CENTER" }));
|
||||
if (item.kids) append(fold, icon(ctx, "chevron-down", COLORS.desc, 14, { name: "Fold icon" }));
|
||||
if (o.kids) append(fold, icon(ctx, "chevron-down", COLORS.desc, 14, { name: "Fold icon" }));
|
||||
const badge = append(node, frame(ctx, "Badge", { dir: "HORIZONTAL", width: 44, height: 20, radius: 4, align: "CENTER", justify: "CENTER",
|
||||
fill: spec.color, fillOpacity: 0.18 }));
|
||||
text(ctx, badge, "Kind", spec.label, { hug: true, medium: true, size: 11, lineHeight: 14, color: spec.color });
|
||||
const content = append(node, frame(ctx, "Content", { width: 10, gap: 3 }), true);
|
||||
if (editing) richText(ctx, content, "Code", codeSegments(blockCode(fileKey, item.at)), { color: COLORS.code });
|
||||
const title = editing ? "" : GRID_KINDS[item.kind].rows(ctx, content, fileKey, item, topLevel);
|
||||
const toggle = append(node, frame(ctx, "Toggle", { dir: "HORIZONTAL", px: 8, py: 2, gap: 4, radius: 4, align: "CENTER",
|
||||
fill: editing ? COLORS.toggleOn : null }));
|
||||
append(toggle, icon(ctx, editing ? "eye" : "pencil", COLORS.desc, 13));
|
||||
text(ctx, toggle, "Label", editing ? "预览" : "编辑", { hug: true, size: 12, lineHeight: 16, color: COLORS.desc });
|
||||
return { node, toggle, title };
|
||||
fill: o.editing ? COLORS.toggleOn : null }));
|
||||
append(toggle, icon(ctx, o.editing ? "eye" : "pencil", COLORS.desc, 13));
|
||||
text(ctx, toggle, "Label", o.editing ? "预览" : "编辑", { hug: true, size: 12, lineHeight: 16, color: COLORS.desc });
|
||||
return { node, content, toggle };
|
||||
}
|
||||
|
||||
// ─── 推导式(写在表达式里的遍历)──────────────────────────────────────────────
|
||||
//
|
||||
// 示例:luffy 邀请码工具里的生成器,外加带条件的列表、字典两种写法。
|
||||
// 原地套一个带循环图标和结果类型的小框,框里按读的顺序重排:来源、变量、条件,最后是每项取什么。
|
||||
const ITERATION_SAMPLES = [
|
||||
{ key: "generator", code: 'return "".join(secrets.choice(ALPHABET) for _ in range(length))',
|
||||
before: 'return "".join(', after: ")", type: "生成器", fors: [["_", "range(length)"]], conds: [], element: "secrets.choice(ALPHABET)" },
|
||||
{ key: "list", code: "return [u.id for u in users if u.is_active]",
|
||||
before: "return ", after: "", type: "列表", fors: [["u", "users"]], conds: ["u.is_active"], element: "u.id" },
|
||||
{ key: "dict", code: "return {u.id: u for u in users}",
|
||||
before: "return ", after: "", type: "字典", fors: [["u", "users"]], conds: [], element: "u.id: u" },
|
||||
];
|
||||
|
||||
const pySegments = (code) => segmentsOf(translateCode(code, "python"));
|
||||
const keyword = (word) => ({ text: word, color: COLORS.tokenControl });
|
||||
|
||||
/** 「遍历 range(length) 的每个 _,仅当 …,取 …」:来源在前、变量在后,按读的顺序。 */
|
||||
function iterationSegments(sample) {
|
||||
const source = sample.fors.flatMap(([vars, from], index) =>
|
||||
[...(index ? [{ text: "、" }] : []), ...pySegments(from), { text: " " }, keyword("的每个"), { text: " " }, ...pySegments(vars)]);
|
||||
return [keyword("遍历"), { text: " " }, ...source,
|
||||
...sample.conds.flatMap((cond) => [{ text: "," }, keyword("仅当"), { text: " " }, ...pySegments(cond)]),
|
||||
{ text: "," }, keyword("取"), { text: " " }, ...pySegments(sample.element)];
|
||||
}
|
||||
|
||||
/** 推导式的小框:循环图标 + 结果类型(生成器 / 列表 / 字典 / 集合)+ 重排后的译文,颜色跟「循环」块一致。 */
|
||||
function iterationTag(ctx, parent, sample) {
|
||||
const loop = BLOCK_KINDS.loop.color;
|
||||
const tag = append(parent, frame(ctx, "Iteration", { dir: "HORIZONTAL", px: 6, py: 1, gap: 4, radius: 4, align: "CENTER",
|
||||
fill: loop, fillOpacity: 0.14, stroke: loop, strokeOpacity: 0.45 }));
|
||||
append(tag, icon(ctx, "repeat", loop, 12));
|
||||
text(ctx, tag, "Type", sample.type, { hug: true, medium: true, size: 11, lineHeight: 16, color: loop });
|
||||
richText(ctx, tag, "Text", iterationSegments(sample), { hug: true });
|
||||
return tag;
|
||||
}
|
||||
|
||||
/** Reader / IterationBlock — Sample=…:一条带推导式的返回语句。 */
|
||||
function iterationBlock(ctx, sample) {
|
||||
const { node, content } = cardShell(ctx, `Sample=${sample.key}`, BLOCK_KINDS.return);
|
||||
const line = append(content, frame(ctx, "Line", { dir: "HORIZONTAL", width: 10, align: "CENTER" }), true);
|
||||
line.layoutWrap = "WRAP";
|
||||
richText(ctx, line, "Before", pySegments(sample.before), { hug: true });
|
||||
iterationTag(ctx, line, sample);
|
||||
if (sample.after) richText(ctx, line, "After", pySegments(sample.after), { hug: true });
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Reader / BlockTree — File=<文件>:预览模式下编辑区里的整棵代码块树 */
|
||||
@@ -1401,6 +1454,8 @@ 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.iteration = make("Reader / IterationBlock", ITERATION_SAMPLES.map((sample) => sample.key), 3,
|
||||
(key) => iterationBlock(ctx, ITERATION_SAMPLES.find((sample) => sample.key === key)));
|
||||
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("/")));
|
||||
@@ -1619,6 +1674,13 @@ 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 iteration = frame(ctx, "Prototype / 遍历 · 推导式", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 8, fill: COLORS.editor });
|
||||
page.appendChild(iteration);
|
||||
for (const sample of ITERATION_SAMPLES) {
|
||||
const instance = lib.iteration.get(sample.key).createInstance();
|
||||
instance.name = `Block / ${sample.key}`;
|
||||
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"] },
|
||||
@@ -1665,7 +1727,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(notes).map((board) => board.screen), ...Object.values(filters).map((board) => board.screen), iteration];
|
||||
let y = 0;
|
||||
for (const screen of screens) {
|
||||
screen.x = 0;
|
||||
@@ -1683,7 +1745,7 @@ async function build(api, stage = () => {}) {
|
||||
{ nodeId: filters.none.screen.id, name: "筛选" }];
|
||||
page.selection = [start];
|
||||
api.viewport.scrollAndZoomIntoView(page.selection);
|
||||
return { page, components, boards, narrow, notes, filters, screens };
|
||||
return { page, components, boards, narrow, notes, filters, iteration, screens };
|
||||
}
|
||||
|
||||
async function run(api) {
|
||||
@@ -1702,6 +1764,6 @@ async function run(api) {
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
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 };
|
||||
module.exports = { 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);
|
||||
+17
-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 { 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 { 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]; }
|
||||
@@ -27,6 +27,7 @@ const EXPECTED_LINKS = 106;
|
||||
const EXPECTED_SETS = [
|
||||
["Reader / ModeToggle", 2],
|
||||
["Reader / CodeBlock", 18],
|
||||
["Reader / IterationBlock", 3],
|
||||
["Reader / FilterBar", 3],
|
||||
["Explorer / TreeRow", 8],
|
||||
["Window / QuickInput", 1],
|
||||
@@ -365,6 +366,21 @@ test("filter bar lists the facets of the file and hides filtered blocks with the
|
||||
assert.equal(action(named(filters.kind.filter, "Clear")).destinationId, filters.none.screen.id);
|
||||
});
|
||||
|
||||
test("comprehensions read in order: source, variable, condition, then the element", async () => {
|
||||
assert.deepEqual(ITERATION_SAMPLES.map((sample) => iterationSegments(sample).map((segment) => segment.text).join("")), [
|
||||
"遍历 range(length) 的每个 _,取 secrets.choice(ALPHABET)",
|
||||
"遍历 users 的每个 u,仅当 u.is_active,取 u.id",
|
||||
"遍历 users 的每个 u,取 u.id: u",
|
||||
]);
|
||||
const api = fakeFigma();
|
||||
const { iteration } = await build(api);
|
||||
const block = iteration.findOne((node) => node.name === "Block / generator");
|
||||
assert.equal(named(block, "Before").characters, '返回 "".join(');
|
||||
assert.equal(named(named(block, "Iteration"), "Type").characters, "生成器");
|
||||
assert.equal(named(named(block, "Iteration"), "Text").characters, "遍历 range(length) 的每个 _,取 secrets.choice(ALPHABET)");
|
||||
assert.equal(named(block, "After").characters, ")");
|
||||
});
|
||||
|
||||
test("boards and sets sit on the page without overlapping", async () => {
|
||||
const api = fakeFigma();
|
||||
const result = await build(api);
|
||||
|
||||
Reference in new issue
Block a user