💄 style(vscode): 代码阅读的「按注释规范写注释」按钮统一排在卡片右上角那排最前面,不再画在「注释」一行上

This commit is contained in:
cruldra committed 2026-10-08 08:03:00 +08:00
1 parent c398a538d9
commit ebea984852
7 files changed
+52 -68

No files matched your search

+6 -6
View File
@@ -184,10 +184,10 @@
库规则的种类用规则给的图标(`KindBadge.icon`),跟在语言的种类图标后面:SQLAlchemy 数据表 `database`、数据类 `package`、Pydantic 数据模型 `braces`(和类型块同一个图标,颜色不同)、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`、React 组件 `atom`、状态 `toggle-right`、引用 `anchor`、数据源 `download`、派生值 `sigma`、副作用 `zap`、事件处理函数 `mouse-pointer-click`、渲染输出 `layout-template`、serde 可序列化 `braces`、thiserror 错误 `triangle-alert`、axum 接口 `globe`、sqlx 查询 `database`、tauri 桌面命令 `square-function`。数据类写了 `frozen=True` 再加一把锁(`lock`,悬停「不可修改」)。实现块再由规则 `rust.impl` 换成卡片,左边因此有两个「实现」图标。
这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置、编译属性共用一个,字段、标签共用一个——加库规则用的 14 个(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出的 8 个也在里面),lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,概念的 `icon` 只能写其中一个,写别的是类型错误。内置种类 → 图标名在 `contract.ts` 的 `KIND_ICONS`(宿主写进概念列表)。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。先是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。最前面是「按注释规范重写」(注释图标,块还没有注释时叫「按注释规范写注释」;不能写注释的块没有,见「重写注释」),然后是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
- 悬停提示(整个插件的网页只走这一条路,预览和看板共用同一个组件):带提示的元素只写 `data-tooltip="提示"`(名字照旧写在 `aria-label` 里,只有图标的按钮必须有 `aria-label`),页面根上挂一个 `TooltipHost`(`webview-ui/src/components/ui/tooltip.tsx`)统一弹,不用原生 `title`;提示里的换行照样换行(看板摘修复弹框里「迁移」列出的几个文件一行一个);注释、回答里 Markdown 链接的标题(`[文字](网址 "标题")`)也写成 `data-tooltip`。网页的 TSX 里写原生 `title` 属性是 lint 错误(`vscode/eslint.config.mjs`),只有 `iframe` 这种必须带标题的元素例外,组件自己叫 `title` 的属性不算。原因:macOS 上 VS Code 带的 Electron(1.139 是 Electron 43)只在鼠标压着带 `title` 的元素自身边缘时才弹原生提示,停在它里面的图标、文字上不弹(electron/electron#49843,Electron 38 起),种类图标 20px 的框几乎被 16px 的图形占满,基本弹不出来;样子也是系统的。样子照 VS Code 的悬停框:`--vscode-editorHoverWidget-background` 底、`--vscode-editorHoverWidget-border` 边框、`--vscode-editorHoverWidget-foreground` 字、`0 2px 8px --vscode-widget-shadow` 阴影,11px 字,圆角 4px,最宽 22rem;优先在元素上方,上方放不下翻到下方,水平对着元素中间,贴边留 8px,不接鼠标。鼠标停 500ms 才弹,在同一个元素里移动不重新计时,已经弹着时移到另一个带提示的元素上立刻换;移开、移出页面、按下鼠标、按任意键(含 Esc)、滚动就收,收了之后鼠标不离开这个元素不再弹。键盘把焦点移到带提示的元素上(`:focus-visible`)立刻弹,焦点离开就收;提示不是元素自己的名字(`aria-label` 或它的字)时用 `aria-describedby` 挂到元素上,读屏当描述念。
- 种类图标和底色按种类区分(颜色见「颜色」);文件开头的说明(种类「注释」)虚线框。
- 「注释」一行:讲这块的普通注释不单独成块,是这张卡片上的一行(见「注释挂到它讲的块上」)。属性表卡片(语言的、翻译插件排的、库规则的卡片都一样,宿主统一插,规则、翻译插件不用管)插在第一行(名称这类讲它是谁的一行)后面,有说明(值是 Markdown 的那一行)时插在说明后面,所以是「名称、说明、注释、别的」;一行译文的卡片(条件、循环、语句、调用、返回……)排在译文、摊开实参的表、说明的下面,同属性表的一行(左边「注释」、右边 Markdown)。名字是翻译插件给种类 comment 起的名字(中文直译「注释」)。几段注释(中间隔着空行、或前后两处)各是一段 Markdown,照源码顺序,空行隔开。按「注释的 Markdown 排版」显示(仓库路径链接、不带路径的引用、仓库图片都照常能点),标差异时同属性表的行逐词标。行上右边是「按注释规范重写」按钮(见「重写注释」)。
- 「注释」一行:讲这块的普通注释不单独成块,是这张卡片上的一行(见「注释挂到它讲的块上」)。属性表卡片(语言的、翻译插件排的、库规则的卡片都一样,宿主统一插,规则、翻译插件不用管)插在第一行(名称这类讲它是谁的一行)后面,有说明(值是 Markdown 的那一行)时插在说明后面,所以是「名称、说明、注释、别的」;一行译文的卡片(条件、循环、语句、调用、返回……)排在译文、摊开实参的表、说明的下面,同属性表的一行(左边「注释」、右边 Markdown)。名字是翻译插件给种类 comment 起的名字(中文直译「注释」)。几段注释(中间隔着空行、或前后两处)各是一段 Markdown,照源码顺序,空行隔开。按「注释的 Markdown 排版」显示(仓库路径链接、不带路径的引用、仓库图片都照常能点),标差异时同属性表的行逐词标。这一行上不放按钮,「按注释规范重写」排在卡片右上角那排按钮里(见「重写注释」)。
- 内容两种状态,每块单独切:
- 预览:函数、类、枚举、异常、导入、常量、变量、解释器、选项、解析指令、阶段、健康检查、启动块是属性表(见下节),文件开头的说明是 Markdown(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文里有跨几行写的调用时,摊开实参的表排在译文下面(见「调用与构造」);块带说明(Python 字段的属性 docstring)时,说明按 Markdown 再排在下面;块挂着注释时「注释」一行排在最下面。
- 编辑:这一块在源码里的完整代码(按语法树着色):挂在它上面的注释、头部连同各子孙块、收尾括号,可以直接改,改完按这整段写回;蓝色边框。按钮换成眼睛「预览」;Esc 也回到预览。
@@ -637,7 +637,7 @@
按写注释的技能(`/ecc:writing-code-comments`)给一块代码重写讲它的普通注释,没有就新写一段。
- 按钮(`MessageSquareText` 图标,只有图标,悬停 / 读屏「按注释规范重写」,块还没有注释时是「按注释规范写注释」):每块代码都有。卡片有「注释」一行时画在这一行右边,没有时排在右上角那排按钮里(「问 AI」左边);文件开头的说明卡片排在右上角。正在重写时不能再点。定义卡片浮窗里、删掉的块没有;嵌着的代码里的块(Dockerfile RUN 里的 shell)、语言没给行注释记号(`LanguageAdapter.lineComment`)又还没有注释的块没有(`BlockView.commentTarget` 没有)。
- 按钮(`MessageSquareText` 图标,只有图标,悬停 / 读屏「按注释规范重写」,块还没有注释时是「按注释规范写注释」):每块代码都有,不管卡片有没有「注释」一行都排在右上角那排按钮的最前面,依次是它、「问 AI」、「编辑」;文件开头的说明卡片也一样。正在重写时不能再点。定义卡片浮窗里、删掉的块没有;嵌着的代码里的块(Dockerfile RUN 里的 shell)、语言没给行注释记号(`LanguageAdapter.lineComment`)又还没有注释的块没有(`BlockView.commentTarget` 没有)。
- 改哪儿(`BlockView.commentTarget`,扩展端 `blockView.ts` 算):块前面挂着普通注释时换掉它们(从第一段开头到最后一段结尾,几段中间隔着的空行一起换掉,写成一段);文件开头的说明卡片换掉它自己;块还没有前面的注释时新写一段,插在块的第一行(连同它的说明、装饰器)上面。说明(docstring、JSDoc、`///`)不动;挂在块后面、子块交上来的注释不动。
- 问模型:同「问 AI」用 Claude Code 命令行、在这份文件所在的工作区文件夹里跑、用选的 profile、只读(`--tools Read,Grep,Glob`、`--allowedTools Read,Grep,Glob`、`--permission-mode dontAsk`、`--strict-mcp-config`:它能读那个文件,改不了,写回归扩展),从 stdin 发的只有一句:`/ecc:writing-code-comments <路径>:<起>-<止>`,路径相对 claude 的工作目录、用 `/` 分隔,行号 1 起,是这块代码的完整范围(连同挂在它上面的注释;文件开头的说明卡片是它自己那几行),别的不说(`commentRewrite.ts` 的 `commentRewritePrompt`)。文件有没保存的修改时不问(模型读的是磁盘上的文件),直接失败「先保存再重写注释」。
- 结果按 JSON Schema 交(`--json-schema '{"type":"object","properties":{"body":{"type":"string"}},"required":["body"],"additionalProperties":false}'`):模型调 Claude Code 的 `StructuredOutput` 工具交结果,result 行的 `structured_output.body` 是注释正文(不带注释符号)。不读模型的自由文字:只回了一段话、`structured_output` 没有或 `body` 是空的,都算失败,文件不动。
@@ -2038,7 +2038,7 @@ interface BlockViewBase {
concepts: BlockConcept[] // 认出来的概念,从左到右;webview 按图标名画,不自己用种类查
badge?: KindBadge // 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色;图标在 concepts 里。和类块一样默认折叠
slot?: { row: number, item: number } // 库规则挂在父卡片子行上的子块才有(RuleSlot):挂在父卡片第 row 行的第 item 个子行上,点它才在这一行下面展开(它的块 id 记成折叠着就是收着)
commentRow?: number // 「注释」一行排第几行,0 起:属性表卡片是 properties 的下标,一行译文的卡片是 rows 的下标;没有注释就没有。「重写注释」按钮画在这一行上
commentRow?: number // 「注释」一行排第几行,0 起:属性表卡片是 properties 的下标,一行译文的卡片是 rows 的下标;没有注释就没有
commentTarget?: CommentTarget // 「重写注释」写到哪(见「重写注释」);嵌着的代码里的块、语言没给行注释记号又还没有注释的块没有,不给按钮
children: BlockView[]
}
@@ -2247,7 +2247,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
- 块标签:拆法(`docTags.test.ts`:多行续写、空行结束、`@returns` / `@exception` 同义、JSDoc 的 `{类型}`、`[名字=默认值]`、横杠、标签前后的正文、没有标签时原样、别的标签和围栏代码块里的行留在正文、异常名写成引用);卡片(`translators/zhTemplate.test.ts` 的「说明里的块标签」):`fixtures/conversationBrief.py`(luffy 的 `render_conversation_brief`)说明里没有 `@param` / `@return`,输入参数表「说明」列四个参数的原文,输出参数后面跟 `@return`;`fixtures/localRuntimeGate.py`(luffy 的 `gate.py`)「异常」表两行,第一行是完整链接、第二行是不带路径的引用;对不上的 `@param` 排在说明下面、提示色、悬停「没有这个参数」;类的 `@param` 照写成表、异常类的对到构造参数(`*args` 去掉星号对)。React 组件(字段没注释时用 `@param`、`props.x`、讲整个参数的跟在构造参数后面、对不上的、异常表)和事件处理函数(`rules/react.test.ts`)、FastAPI 请求表的说明列和响应后面的 `@return`(`rules/fastapi.test.ts`)、Rust 函数和结构体(`languages/rust.test.ts`)、数据类字段(`rules/dataclass.test.ts`)。
- 注释挂到它讲的块上(各语言的 `languages/*.test.ts`「注释挂到它讲的块上」,`outline` 的 `notes` 是挂着的各段注释在哪):Python、TypeScript、Rust、Shell、Dockerfile 各一份,覆盖隔着空行的注释挂到后面那块、主体最后的挂到前面那块、主体里只有注释时挂到父块、文件最后的挂到最后一块、文件开头隔着空行的是文件的说明而紧挨着第一块代码的挂到它上面、`#!` / 写编码的 / `//!` / `@file` / 模块 docstring 是文件的、说明和注释并存、函数体里语句前面的注释;luffy 的 `fixtures/acquisitionRepository.py` 导入后面的注释挂到类上(`blockView.test.ts`:类卡片「名称、注释、访问级别」、编辑范围连着注释、写回原样);TypeScript 的「名称、说明、注释」、几段注释是几段 Markdown、一行译文的卡片的 `rows`;嵌着的代码里的块没有 `commentTarget`;改了注释那块算修改、「注释」一行逐词标(`diff/blockDiff.test.ts`);规则略去挂着注释的块不合约定(`libraryRules.test.ts`)。webview(`BlockTree.test.tsx`):「注释」一行照属性表画、按钮在这一行上,一行译文的卡片排在译文下面,没有注释时按钮在右上角叫「按注释规范写注释」,不能写注释的块没有按钮。
- 注释挂到它讲的块上(各语言的 `languages/*.test.ts`「注释挂到它讲的块上」,`outline` 的 `notes` 是挂着的各段注释在哪):Python、TypeScript、Rust、Shell、Dockerfile 各一份,覆盖隔着空行的注释挂到后面那块、主体最后的挂到前面那块、主体里只有注释时挂到父块、文件最后的挂到最后一块、文件开头隔着空行的是文件的说明而紧挨着第一块代码的挂到它上面、`#!` / 写编码的 / `//!` / `@file` / 模块 docstring 是文件的、说明和注释并存、函数体里语句前面的注释;luffy 的 `fixtures/acquisitionRepository.py` 导入后面的注释挂到类上(`blockView.test.ts`:类卡片「名称、注释、访问级别」、编辑范围连着注释、写回原样);TypeScript 的「名称、说明、注释」、几段注释是几段 Markdown、一行译文的卡片的 `rows`;嵌着的代码里的块没有 `commentTarget`;改了注释那块算修改、「注释」一行逐词标(`diff/blockDiff.test.ts`);规则略去挂着注释的块不合约定(`libraryRules.test.ts`)。webview(`BlockTree.test.tsx`):「注释」一行照属性表画,一行译文的卡片排在译文下面,有没有「注释」一行按钮都只有一个、排在右上角那排最前面,没有注释时叫「按注释规范写注释」,不能写注释的块没有按钮。
- 重写注释(`commentRewrite.test.ts`、`qa/claudeAsk.test.ts`):发给模型的就是 `/ecc:writing-code-comments 路径:起-止` 一句;读结构化结果(正文逐字长出来、读文件是在做的事、只回一段话没有正文、报错);命令行在只读那套后面加 `--json-schema`、stdin 里只有那一句;交回结构化正文 → 换掉块前面几段注释(套回 `#` 和缩进)、文件开头的说明换它自己;没有注释的块新写一段插在上面(缩进同块,docstring 不动);没交回正文、出错、重写期间代码变了 → 失败不写,叫停 → 不写;套回壳的几种写法(三引号、`#`、`/** */`、`//`)。
- 注释的 Markdown:「注释」一行、文件开头的说明、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、`{@link}`、HTML 都不转换,块标签拆出去),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / `./` / `..` / 绝对路径的链接和 http 图片不渲染,仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」,仓库路径链接能点、点了打开文件并跳到符号;跳到定义不认 Markdown 里的名字。不带路径的引用:行内规则(`webview-ui/.../markdown.test.ts`:`[Name]`、`[A.b]` 是 `symbol:` 链接,`` `[x]` ``、`items[i]`、`[会话进展]`、`[名字](路径)`、`[Agent](javascript:x)` 各自的结果,问 AI 的回答不认、带增删的注释照样认);问答(`repoLink.test.ts`:`symbol:` 地址只认标识符、点了发不带 `path` 的 `openRepoLink`、和仓库路径一起问、找不到的标坏、打开成功不再标、同样的链接不再问、文档换了版本再问);扩展端(`symbolLinks.test.ts`:本文件优先于导入且不问语言扩展、本文件的 `Class.member`、类有成员没有就是找不到、导入的名字问定义再在那个类底下找成员、整模块导入从顶层找、找不到;Python / TypeScript 各种导入写法引进来的名字和问定义的位置,`gate.py` 的导入)。
- 行替换:多行块替换后缩进正确。
@@ -2304,7 +2304,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 取旧内容(`git/fileAt.test.ts`,真的 git):取的是提交里的、不是工作区改过的;提交里没有这个文件是 null;提交不存在抛错。对比基准的字(`uncommittedFiles.test.ts`、`reviewRules.test.ts`、`changedFiles.test.ts`):未跟踪的按状态认(两种 `git.untrackedChanges` 设置都认)、上一次提交里没有时的三种说明;PR 目标分支的文案;改名的取改名前的路径、PR 里新加的目标分支上没有。「只看改动」开关(`editor/blockFilter.test.ts`):记进 `workspaceState`、关了不记、改了发给所有预览带着眼下的块筛选。
- 页面(`diffView.test.ts`、`markdown.test.ts`、`viewState.test.ts`):块按记号认成页面上的 id、找不到的不标、不是这一版不画卡片里的标记,删掉的块按层挂、找不到挂处的和被筛掉的不画,新文件不逐块标,标了改动的默认展开;统计的字;删掉的块收起时的名字;只看改动折哪几段(改过的块下面按层折、删掉的块打断、被筛掉的不算、点开过的和要露出来的不折);各处改动的先后和行、被筛掉的不算;上一处 / 下一处接着走、按视口顶上的行找、到头绕回;带增删的 Markdown 照常排版、跨行逐行包、新加的列表项留住记号。
- 复制出什么(`explorer/copyText.test.ts`):文件、目录的名字 / 相对路径 / 绝对路径;不带后缀只去最后一个扩展名(`a.test.ts`、`archive.tar.gz`),`.env` 原样、`.env.local` → `.env`、没有扩展名的原样;相对路径总用 `/`,绝对路径照本机写法(Windows 反斜杠)。
- 块右上角的图标按钮(webview `BlockTools.test.tsx`,渲染成 HTML):一组一个按钮、没有字、悬停提示(`data-tooltip`,没有原生 `title`)和 `aria-label` 是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和 `aria-expanded`。
- 块右上角的图标按钮(webview `BlockTools.test.tsx`,渲染成 HTML):一组一个按钮、没有字、悬停提示(`data-tooltip`,没有原生 `title`)和 `aria-label` 是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和 `aria-expanded`;给了「重写注释」时它排在最前面(「问 AI」左边)、只有图标,正在重写时禁用、`aria-busy`,块还没有注释时叫「按注释规范写注释」。
- 块的概念图标(webview `kindIcons.test.tsx`,渲染成 HTML):内置种类 → 图标名和原型一一对应(`contract.ts` 的 `KIND_ICONS`);这组里每个名字画的就是 lucide 里同名的图标;webview 配了图标的名字和契约的 `KIND_ICON_NAMES` 一样、没有多的(`kindIconNames.test.ts`);只有图标、没有字,悬停提示(`data-tooltip`,没有原生 `title`)是各自的名字,整排 `role="img"` 的 `aria-label` 把名字用顿号连起来,20px 的框、16px 的图形,横排间距紧;语言的种类按 id 上色,规则的图标用规则给的颜色。概念列表(`blockView.test.ts`、`rules/dataclass.test.ts`):普通类只有「类」;数据类是「类」和「数据类」,`frozen=True` 再加「不可修改」的锁,`slots` 不加;同一个 id 只留第一个(语言的种类优先于后面重复的);悬停名是这些。各库规则的测试核对 `badge` 的名字、颜色、图标。
- 悬停提示(webview `components/ui/tooltip.test.ts`):停 500ms 才弹、在同一个元素里移动不重新计时、没等到就移开不弹、弹着时移到另一个上立刻换、移到没提示的地方就收、按下鼠标 / 按键 / 滚动就收且鼠标不离开不再弹、等着弹时按下这次不弹、键盘聚焦立刻弹失焦收、提示是空的不弹;位置在上方、上方放不下翻到下方、左右钳在视口内。Markdown 链接的标题写成 `data-tooltip`(`markdown.test.ts`);网页 TSX 里的原生 `title` 由 lint 规则拦(`button`、`span` 上报错,`iframe`、组件属性不报)。页面上(无头 Chrome,真的移鼠标、按 Tab):种类图标中心点命中的是图标里的线条、没有盖在上面的东西;停 250ms 还没弹、停满 500ms 在图标上方弹「函数」「接口」,颜色是悬停框的那组变量;Tab 到「编辑」立刻弹、Esc 收;页面上没有原生 `title`;看板上「待接管」、禁用的 .env 锁按钮(禁用的按钮照样弹)、刷新按钮、摘修复弹框里「迁移」的三行文件名(一行一个)都弹,Tab 到按钮立刻弹、Esc 收。亮、暗两套颜色都看过。
- 问 AI:
@@ -1,6 +1,6 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { BlockTools, RewriteButton } from './BlockTools'
import { BlockTools } from './BlockTools'
function render(editing: boolean, ask?: { asking: boolean }, rewrite?: { rewriting: boolean, fresh?: boolean }): string {
return renderToStaticMarkup(
@@ -55,11 +55,13 @@ describe('块右上角的图标按钮', () => {
expect(busy).not.toContain('cr-qa-spin')
})
it('块还没有注释时按钮叫「按注释规范写注释」;「注释」一行上的按钮是同一个,只有图标', () => {
const fresh = render(false, undefined, { rewriting: false, fresh: true })
expect(fresh).toContain('data-tooltip="按注释规范写注释" aria-label="按注释规范写注释"')
const onRow = renderToStaticMarkup(<RewriteButton tool={{ rewriting: false, fresh: false, onRewrite: () => {} }} />)
expect(onRow).toMatch(/^<button[^>]*data-tooltip="按注释规范重写" aria-label="按注释规范重写"[^>]*><svg/)
expect(visibleText(onRow)).toBe('')
it('块还没有注释时同一个位置的按钮叫「按注释规范写注释」,同样只有图标', () => {
const fresh = render(false, { asking: false }, { rewriting: false, fresh: true })
expect(visibleText(fresh)).toBe('')
const buttons = [...fresh.matchAll(/<button[^>]*>/g)].map(match => match[0])
expect(buttons).toHaveLength(3)
expect(buttons[0]).toContain('data-tooltip="按注释规范写注释" aria-label="按注释规范写注释"')
expect(buttons[1]).toContain('data-tooltip="问 AI"')
expect(buttons[2]).toContain('data-tooltip="编辑"')
})
})
+16 -24
View File
@@ -8,30 +8,9 @@ export interface RewriteTool {
onRewrite: () => void
}
/**
* 「按注释规范重写」:用写注释的技能重写这块代码的注释并写回文件(块还没有注释时是「按注释规范写注释」,新写一段);正在重写时不能再点。
* 只有图标(注释),名字在悬停提示和 aria-label 里。卡片有「注释」一行时画在那一行上,没有时排在右上角那排按钮里。
*/
export function RewriteButton({ tool }: { tool: RewriteTool }): ReactElement {
const label = tool.fresh ? '按注释规范写注释' : '按注释规范重写'
return (
<button
type="button"
className="cr-tool flex h-5 w-6 shrink-0 items-center justify-center rounded"
data-tooltip={label}
aria-label={label}
aria-busy={tool.rewriting}
disabled={tool.rewriting}
onClick={tool.onRewrite}
>
<MessageSquareText size={14} strokeWidth={1.8} aria-hidden />
</button>
)
}
/**
* 块右上角的一排图标按钮(普通块、属性表、库规则的卡片共用):只放图标,名字写在悬停提示(data-tooltip)和读屏用的 aria-label 里。
* 「按注释规范重写」(见 RewriteButton):卡片没有「注释」一行时排在这里。
* 「按注释规范重写」:用写注释的技能重写这块代码的注释并写回文件,块还没有注释时叫「按注释规范写注释」、新写一段;正在重写时不能再点。图标是注释。
* 「问 AI」:点开 / 收起块下面的问答区,展开时带按下的底色。
* 「编辑 / 预览」切换:预览时是铅笔「编辑」,编辑时是眼睛「预览」、带按下的底色。
*/
@@ -42,14 +21,27 @@ export function BlockTools({ editing, onToggleEdit, asking, onToggleAsk, rewrite
asking?: boolean
/** 没给就不显示「问 AI」(文件不在工作区文件夹里,问答没地方存) */
onToggleAsk?: () => void
/** 没给就不显示「按注释规范重写」(不能写注释,或按钮画在「注释」一行上) */
/** 没给就不显示「按注释规范重写」(不能写注释的块) */
rewrite?: RewriteTool
}): ReactElement {
const label = editing ? '预览' : '编辑'
const Icon = editing ? Eye : Pencil
const rewriteLabel = rewrite?.fresh ? '按注释规范写注释' : '按注释规范重写'
return (
<div className="flex h-5 shrink-0 items-center gap-0.5" role="group" aria-label="块操作">
{rewrite && <RewriteButton tool={rewrite} />}
{rewrite && (
<button
type="button"
className="cr-tool flex h-5 w-6 items-center justify-center rounded"
data-tooltip={rewriteLabel}
aria-label={rewriteLabel}
aria-busy={rewrite.rewriting}
disabled={rewrite.rewriting}
onClick={rewrite.onRewrite}
>
<MessageSquareText size={14} strokeWidth={1.8} aria-hidden />
</button>
)}
{onToggleAsk && (
<button
type="button"
@@ -111,29 +111,31 @@ describe('挂在卡片子行上的子块', () => {
describe('「注释」一行和「重写注释」按钮', () => {
const comment = { label: '注释', value: { markdown: '按名字找' } }
const html = (block: BlockView): string => renderToStaticMarkup(<BlockLevel blocks={[block]} tree={state(new Set())} />)
/** 「注释」那一行(cr-prop-row)里的 HTML */
const commentRowHtml = (markup: string): string => markup.split('cr-prop-row').find(part => part.includes('按名字找')) ?? ''
/** 按钮只有一个,排在右上角那排按钮(块操作)的最前面 */
const inToolbar = (markup: string, label: string): void => {
expect(markup.match(new RegExp(`aria-label="${label}"`, 'g'))).toHaveLength(1)
expect(markup).toMatch(new RegExp(`role="group" aria-label="块操作"><button[^>]*aria-label="${label}"`))
}
it('属性表卡片:「注释」一行照属性表画,按钮画在这一行上,右上角那排不再有', () => {
it('属性表卡片:「注释」一行照属性表画,按钮在右上角那排,不在这一行上', () => {
const card = view('1', 'function', '', {
properties: [{ label: '名称', value: [{ text: 'find', role: 'function' }] }, comment, { label: '异步', value: [{ text: '否', role: 'storage' }] }],
commentRow: 1,
commentTarget: { kind: 'replace', startLine: 0, endLine: 0 },
})
const markup = html(card)
expect(commentRowHtml(markup)).toContain('aria-label="按注释规范重写"')
expect(markup.match(/aria-label="按注释规范重写"/g)).toHaveLength(1)
inToolbar(markup, '按注释规范重写')
expect(markup.indexOf('按名字找')).toBeLessThan(markup.indexOf('异步'))
})
it('一行译文的卡片:「注释」一行排在译文下面,按钮同样在这一行上', () => {
it('一行译文的卡片:「注释」一行排在译文下面,按钮同样在右上角那排', () => {
const markup = html(view('2', 'statement', 'save()', { rows: [comment], commentRow: 0, commentTarget: { kind: 'replace', startLine: 0, endLine: 0 } }))
expect(markup.indexOf('save()')).toBeLessThan(markup.indexOf('按名字找'))
expect(commentRowHtml(markup)).toContain('aria-label="按注释规范重写"')
inToolbar(markup, '按注释规范重写')
})
it('还没有注释的块:按钮在右上角那排,叫「按注释规范写注释」;不能写注释的块没有按钮', () => {
expect(html(view('3', 'statement', 'save()', { commentTarget: { kind: 'insert', line: 0 } }))).toMatch(/role="group" aria-label="块操作"><button[^>]*aria-label="按注释规范写注释"/)
it('还没有注释的块:按钮同样在右上角那排,叫「按注释规范写注释」;不能写注释的块没有按钮', () => {
inToolbar(html(view('3', 'statement', 'save()', { commentTarget: { kind: 'insert', line: 0 } })), '按注释规范写注释')
expect(html(view('4', 'statement', 'save()'))).not.toContain('按注释规范')
})
})
+7 -10
View File
@@ -9,7 +9,7 @@ import type { BlockMarks, BlockView } from './types'
import { ChevronDown, ChevronRight, ChevronsUpDown, EyeOff } from 'lucide-react'
import { Fragment, useState } from 'react'
import { AskThread, RewriteThread } from './AskThread'
import { BlockTools, RewriteButton } from './BlockTools'
import { BlockTools } from './BlockTools'
import { CodeEditor } from './CodeEditor'
import { DiffText } from './DiffText'
import { removedTitle } from './diffView'
@@ -204,12 +204,10 @@ function BlockRewrite({ anchor, edit }: { anchor: string, edit: EditTree }): Rea
/**
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明、几行属性(「注释」一行)。标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
* rewrite 是「重写注释」按钮,画在「注释」一行上(commentRow);只读的块树(定义卡片浮窗)、删掉的块没有。
*/
function BlockPreview({ block, tree, marks, rewrite }: { block: BlockView, tree: BlockTreeState, marks?: BlockMarks, rewrite?: RewriteTool }): ReactElement {
const rowTools = rewrite && block.commentRow !== undefined ? new Map([[block.commentRow, <RewriteButton key="rewrite" tool={rewrite} />]]) : undefined
function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTreeState, marks?: BlockMarks }): ReactElement {
if ('properties' in block)
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} slots={cardSlots(block, tree)} rowTools={rowTools} />
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} slots={cardSlots(block, tree)} />
return (
<>
{marks?.translation ? <DiffText diff={marks.translation} /> : <TextPreview text={block.translation} />}
@@ -221,14 +219,14 @@ function BlockPreview({ block, tree, marks, rewrite }: { block: BlockView, tree:
{marks?.doc ? <DiffText diff={marks.doc} className="mt-0.5" /> : block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
{block.rows && (
<div className="mt-[3px]">
<PropertyGrid blockId={block.id} rows={block.rows} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} rowTools={rowTools} />
<PropertyGrid blockId={block.id} rows={block.rows} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} />
</div>
)}
</>
)
}
/** 这块的「重写注释」按钮:能写注释(commentTarget)、页面上能改代码时才有。 */
/** 这块的「重写注释」按钮,排在右上角那排按钮里:能写注释(commentTarget)、页面上能改代码时才有。 */
function rewriteTool(block: BlockView, edit: EditTree | undefined): RewriteTool | undefined {
if (!edit || !block.commentTarget)
return undefined
@@ -271,7 +269,6 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
const hasChildren = toggled || hasShownChildren(block, tree)
const Chevron = collapsed && !toggled ? ChevronRight : ChevronDown
const change = tree.diff?.changes.get(block.id)
const rewrite = rewriteTool(block, edit)
return (
<div
className="cr-block relative flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
@@ -307,7 +304,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
onExit={() => edit.onToggleEdit(block.id)}
/>
)
: <BlockPreview block={block} tree={tree} marks={tree.diff?.current ? change?.marks : undefined} rewrite={rewrite} />}
: <BlockPreview block={block} tree={tree} marks={tree.diff?.current ? change?.marks : undefined} />}
</div>
{tree.temporary.has(block.id) && (
<span className="cr-temporary flex h-5 shrink-0 items-center gap-1 rounded px-1.5 text-[11px] whitespace-nowrap">
@@ -321,7 +318,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
onToggleEdit={() => edit.onToggleEdit(block.id)}
asking={tree.qa?.state.open.includes(block.anchor)}
onToggleAsk={tree.qa && (() => tree.qa?.onToggle(block.anchor))}
{...(rewrite && block.commentRow === undefined && { rewrite })}
rewrite={rewriteTool(block, edit)}
/>
)}
</div>
@@ -252,7 +252,7 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。
* 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
*/
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, slots, rowTools }: {
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, slots }: {
blockId: string
rows: PropertyRow[]
collapsed: ReadonlySet<string>
@@ -261,8 +261,6 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
marks?: RowsDiff
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
slots?: GridSlots
/** 排在某一行值右边的按钮(「注释」一行的「重写注释」),按行的下标 */
rowTools?: ReadonlyMap<number, ReactNode>
}): ReactElement {
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
.map(({ value, index }) => <RemovedRow key={`removed-${index}`} id={`${blockId}/x${index}`} row={value} />)
@@ -298,14 +296,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
</button>
)
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
{rowTools?.has(index)
? (
<div className="flex min-w-0 items-start gap-1">
<div className="min-w-0 flex-1"><RowValue row={row} mark={mark} /></div>
{rowTools.get(index)}
</div>
)
: <RowValue row={row} mark={mark} />}
<RowValue row={row} mark={mark} />
</div>
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} toggles={toggles} />}
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} marks={mark?.items} />}
+1 -1
View File
@@ -143,7 +143,7 @@ interface BlockViewBase {
attributes: Record<string, string>
/** 这块认出来的概念,从左到右。webview 按图标名画,不自己用种类查 */
concepts: BlockConcept[]
/** 「注释」一行(讲这块的普通注释)排在第几行,0 起:属性表卡片是 properties 里的下标,一行译文的卡片是 rows 里的下标。「重写注释」按钮画在这一行上 */
/** 「注释」一行(讲这块的普通注释)排在第几行,0 起:属性表卡片是 properties 里的下标,一行译文的卡片是 rows 里的下标;没有注释就没有 */
commentRow?: number
/** 「重写注释」写到哪(扩展端用,这边只看有没有、是换掉还是新写);没有就不显示按钮 */
commentTarget?: CommentTarget