diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index c341621..723c8b1 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -14,7 +14,7 @@ | --- | --- | | 流程 | 源码 →(tree-sitter)语法树 → 通用代码块树 → 译文 | | 第一版翻译 | 只做中文模板直译;AI 解读以后作为另一个翻译插件加 | -| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python、Shell 脚本(bash / sh / zsh) | +| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python、Shell 脚本(bash / sh / zsh)、Dockerfile | | 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 `activate()` 导出的 API 注册;内置实现也走同一个 API | | 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 | | 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 | @@ -44,6 +44,9 @@ | 调用与构造 | 实参列表跨了几行写的调用不挤成一长行:写成「[等待 ]新建 / 调用 被调用的名字 · N 个参数」,实参摊成一张表「参数 \| 值」(按名字传的写名字,按位置传的写「第 N 个」,`*x` / `...x` 写「展开」,`**x` 写「展开关键字」);一行写完的调用照旧一行译文。实参的值又是跨几行写的调用时,值那一格写它的一行、它的表套在格子里,一层层往下套;埋在式子里、后面还跟着东西的(`X(\n…\n).model_dump()`)括起来「(新建 X · 3 个参数).model_dump()」。新建还是调用:TypeScript 看有没有 `new`;Python 写法一样,查被调用的名字的定义,是类就是「新建」,是函数、查不到都是「调用」,先按调用画、查到了再刷新。表在变量 / 常量属性表的初始值 / 值那一行下面(能折叠),在返回、语句这些一行译文的块里排在那一行下面。语言插件报结构,翻译插件定用词,webview 只画表。见「调用与构造」 | | Shell 脚本 | `.sh`、`.bash`、`.zsh` 和首行写着 shell 解释器的无扩展名文件(VS Code 的 shellscript):读成解释器、选项(`set -euo pipefail` 每个开关一行)、变量 / 常量、分情况(短的一张「情况 \| 做什么」表,长的每个情况一块子块)、函数(参数按位置「位置 \| 函数里叫」、输出、返回)、命令、管道、条件、遍历;`$1` → 第 1 个参数,`${x:-d}` → x(没设就用 d),`$(cmd)` → 执行〔…〕的输出。语言插件在 tree-sitter-bash 上报块和事实,翻译插件定用词,webview 不认识 shell。同一文件里的函数、变量由语言插件认,⌘ / Ctrl + 点击、单击高亮照常。见「Shell 脚本」 | | 命令规则 | 第五种插件:按命令名注册(`rm`、`curl`、`tar`……),把一条命令连同选项读成一句(「删除 dest(连同里面的东西、不提示)」);规则不认识的选项照抄在后面,没有规则的命令照抄命令名,重定向由翻译插件统一读。见「命令规则」 | +| Dockerfile | `Dockerfile`、`Dockerfile.*`、`*.dockerfile`、`Containerfile`:读成解析指令、阶段(每个 FROM 一个,后面的指令是子块;阶段名能跳过去)、执行(RUN 按命令拆,只有一条命令时一行)、复制、变量(构建参数 / 环境变量)、设置、标签、健康检查、启动;连着写的 ENV / ARG / COPY(从同一个地方)/ LABEL 并成一块加一张表。语法包取自 npm 包 tree-sitter-wasm。见「Dockerfile」 | +| 嵌着的代码 | 一种语言里嵌着另一种语言的代码(Dockerfile 的 RUN 是 shell):语言插件报出是哪几段,宿主交给那种语言的语言插件、翻译插件、命令规则读,只解析这几段;外层的翻译插件只问读出来的样子,不再读一遍。见「Dockerfile」 | +| 选项多的命令 | 命令规则认出的选项有 4 个以上、或同一个选项写了几遍时,读成「… · N 个选项」加一张「选项 \| 意思」表,写了几遍的并成一行;少的照旧写在括号里 | ## 用户可见行为 @@ -115,7 +118,7 @@ ### 预览(代码块树) -- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释;Shell 脚本另有解释器、选项、分情况、命令、管道(见「Shell 脚本」)。 +- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释;Shell 脚本另有解释器、选项、分情况、命令、管道(见「Shell 脚本」);Dockerfile 另有解析指令、阶段、执行、复制、设置、标签、健康检查、启动(见「Dockerfile」)。 - 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--vscode-editorIndentGuide-background`)。 - 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类图标、内容、右上角一排图标按钮。 - 种类图标(原型 `kindBadge`、画板「Prototype / 块种类图标」):块左边只画图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),颜色是种类色;种类的名字(翻译插件给 `kind` 值起的,见「筛选」;库规则的种类是规则给的 `badge.label`)写在悬停提示(见下「悬停提示」,同块右上角按钮)和 `aria-label` 里,`role="img"`。和折叠箭头、内容同一行顶对齐。每种内置种类由插件定图标、颜色、名字:图标从插件定义的一组图标里取(见下),内置种类 → 图标只在 webview 的一处(`kindIcons.tsx` 的 `KIND_ICONS`);颜色在 `codeReader.css` 的 `--cr-kind-*`(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种: @@ -130,15 +133,18 @@ | exception 异常 | `triangle-alert` | loop 循环 | `repeat` | comment 注释 | `message-square-text` | | interpreter 解释器 | `terminal` | options 选项 | `sliders-horizontal` | case 分情况 | `signpost` | | command 命令 | `square-terminal` | pipeline 管道 | `square-terminal` | | | + | directive 解析指令 | `terminal` | stage 阶段 | `layers` | run 执行 | `square-terminal` | + | copy 复制 | `copy` | setting 设置 | `sliders-horizontal` | label 标签 | `tag` | + | health 健康检查 | `activity` | start 启动 | `play` | | | 库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`。 - 这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 22 个——命令、管道共用一个——加库规则用的 6 个,lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,`KindBadge.icon` 只能写其中一个,写别的是类型错误。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。 + 这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置共用一个,字段、标签共用一个——加库规则用的 6 个,lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,`KindBadge.icon` 只能写其中一个,写别的是类型错误。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 能走到,回车 / 空格切换。 - 悬停提示(整个插件的网页只走这一条路,预览和看板共用同一个组件):带提示的元素只写 `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 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文里有跨几行写的调用时,摊开实参的表排在译文下面(见「调用与构造」);块带说明(Python 字段的属性 docstring)时,说明按 Markdown 再排在下面。 + - 预览:函数、类、枚举、异常、导入、常量、变量、解释器、选项、解析指令、阶段、健康检查、启动块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文里有跨几行写的调用时,摊开实参的表排在译文下面(见「调用与构造」);块带说明(Python 字段的属性 docstring)时,说明按 Markdown 再排在下面。 - 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮换成眼睛「预览」;Esc 也回到预览。 - 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里;Python 常量、变量、字段的属性 docstring 算在它那一块里(中间隔着的空行也是)。 - 库规则认领的块(数据表卡片等,见「库规则」):种类的图标、颜色和名字由规则给,内容是规则给的属性表;它的编辑范围是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,各自的「编辑」照旧只改自己那几行。 @@ -304,7 +310,7 @@ 3. 变量、常量(variable / const,属性表,同别的语言的变量块):赋值和 `local` / `export` / `readonly` / `declare`;`local url="$1" sha="$2" out` 一句几个是一块,没给值的写「无」。常量是 `readonly` 的,和脚本顶层名字全大写、值里没有展开的。 4. 分情况(case):每个情况都短(最多 3 句、每句一行写完、没有注释和嵌套的语句块)时整条一块,一行「按 x 分情况」,下面一张表「情况 \| 做什么」,做的几句在格子里一句一行;否则头部是「按 x 分情况」,每个情况一块子块「情况:以 -apple-darwin 结尾」,要做的各句是它的子块。 5. 函数(function,属性表):紧贴在上面的注释(中间没空行)是「说明」,不单独成块。shell 的函数没有参数表,参数按位置取:「输入参数」写「N 个,按位置」,表「位置 \| 函数里叫」,函数里用 `local x="$N"`(或 `x=$N`)起了名字的写名字,没起的写 `$N`。「输出」是函数自己打印到输出的那几句(`echo` / `printf`,重定向到别处的不算;进 if、循环、分情况,不进 `$(…)`、管道、嵌套的函数),同一文件里有 `$(函数名 …)` 时后面淡淡写「(调用处用 $(fetch …) 拿到)」。「返回」:写了 `return N` 的写那几个值,什么都不打印、也没写 return 的写「最后一句成不成功」。主体各句是子块,同别的语言的函数,默认收起。 - 6. 命令(command):一条命令一行,由命令规则读(「删除 dest(连同里面的东西、不提示)」「下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)」);没有规则的命令照抄命令名,后面的词照常翻(引号去掉,展开读成中文)。重定向统一读(不归命令规则):`>&2` →「(打印到错误输出)」,`2>/dev/null` →「(丢掉错误输出)」,`> f` →「(写到 f)」,`>> f` →「(追加到 f)」,`2>&1` →「(错误输出并进输出)」,`< f` →「(从 f 读)」。用 `&&` 连起来的几条命令读成「a 然后 b」,`a || b` 读成「a,失败就 b」。 + 6. 命令(command):一条命令一行,由命令规则读(「删除 dest(连同里面的东西、不提示)」「下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)」);没有规则的命令照抄命令名,后面的词照常翻(引号去掉,展开读成中文)。重定向统一读(不归命令规则):`>&2` →「(打印到错误输出)」,`2>/dev/null` →「(丢掉错误输出)」,`> f` →「(写到 f)」,`>> f` →「(追加到 f)」,`2>&1` →「(错误输出并进输出)」,`< f` →「(从 f 读)」。用 `&&` 连起来的几条命令读成「a 然后 b」,`a || b` 读成「a,失败就 b」。tree-sitter-bash 把 `a && b > f` 读成整串带着重定向,其实重定向只属于 b:语言插件把它算在 b 上(「a 然后 b(写到 f)」)。 7. 管道(pipeline):`a | b | c` 读成「a → b → c」;最后一段是循环(`find … | while read -r link; do … done`)时读成「找出 dest 下面所有符号链接 → 逐行读进 link」,循环的主体是子块。 8. 条件、遍历(if / else / loop):`if` / `elif` / `else` 同 Python(`elif` / `else` 展平成兄弟块,读成「否则如果」「否则」)。条件:`[ -f x ]` → 文件 x 存在,`-d` 目录 x 存在,`-e` 存在,`-L` 是符号链接,`-z` / `-n` 为空 / 不为空,`-r` / `-w` / `-x` 可读 / 可写 / 可执行,`-eq` / `-ne` / `-lt` / `-gt` / `-le` / `-ge` 等于 / 不等于 / 小于 / 大于 / 不大于 / 不小于;`=` / `==` / `!=` 等于 / 不等于,`[[ … ]]` 里右边是通配时读成「以 .sh 结尾」「不以 .sh 结尾」;`!` → 不,`&&` / `||` → 并且 / 或者。一句 `[ … ]`(或一串全是条件)自己就是一句语句时是条件块,读成「成立:…」;条件后面跟一句的(`[ -f x ] && rm x`、`cond || continue`)读成「如果 文件 x 存在,删除 x」「如果不是〔cond〕,跳过这一个」。遍历:`for f in "$gc"/git-*` → 遍历 gc/git-* 匹配到的每个文件 f,`for x in a b` → 遍历 a、b 中的每个 x,`for a in "$@"` → 遍历 全部参数 的每个 a;`while cond` → 只要 cond 就一直重复,`until cond` → 直到 cond 之前一直重复,`while read -r x` → 逐行读进 x。 - 值和展开(一行译文、变量的值、表格里都一样):`$1` → 第 1 个参数,`$0` → 脚本路径,`$@` / `$*` → 全部参数,`$#` → 参数个数,`$?` → 上一条命令的退出码,`$$` → 当前进程号;`${x:-d}` → x(没设就用 d),第几个参数写「没给」:`${1:-}` → 第 1 个参数(没给就是空);`${x:=d}` → x(没设就设成 d),`${x:?m}` → x(没设就报错:m),`${x:+y}` → y(x 设了才有),`${#x}` → x 的长度,`${x#p}` / `${x##p}` → x(去掉开头的 / 开头最长的 p),`${x%s}` / `${x%%s}` → x(去掉结尾的 / 结尾最长的 s),`${x/a/b}` → x(其中第一个 a 换成 b),`${x//a/b}` → x(其中 a 全换成 b),`${x:1:2}` → x(从第 1 个字符起,取 2 个)。整个值是 `$(cmd)` 时读成「执行〔…〕的输出」,写在别的字中间的读成「〔…〕」;命令规则读出来是一个东西的直接写它:`$(dirname "$0")` → 脚本所在目录。双引号字符串去掉引号,写死的部分按字符串色,展开的变量按变量色;全大写的名字(`PY_VERSION`、`HOME`)按常量色。 @@ -336,6 +342,60 @@ - 和原型不一样的地方(插件生成的,比手写的更一致):函数里的变量也是属性表块(同别的语言,不写成一行「局部 …」);`$N` 一律写「第 N 个参数」;选项的意思之间一律用顿号(「连同里面的东西、不提示」);`[ "$(readlink "$f")" = git ]` 照结构读成「〔读出 f 指向的路径〕 等于 git」,不合写成「f 指向 git」。 +### Dockerfile + +原型画板「Dockerfile」(`code.js` 的「Dockerfile」一节:`DOCKER_KINDS`、`NEXT_DOCKERFILE`、`BACKEND_DOCKERFILE`;README「Dockerfile」)。样例是 luffy 的 `frontend/client-nextjs/Dockerfile` 和 `backend/docker/Dockerfile.backend`(复制成 `fixtures/clientNextjs.Dockerfile`、`fixtures/backend.Dockerfile`);原型里的中文是手写的目标,下面的译文都由插件生成,逐行核对见 `translators/dockerfileZh.test.ts` 的 `NEXT_READING`、`BACKEND_READING`,原型略去的几段也照样读。 + +- 哪些文件:VS Code 认成 `dockerfile` 的文件:`Dockerfile`、`Dockerfile.*`、`*.dockerfile`、`Containerfile`。语法包是 tree-sitter-dockerfile,`@vscode/tree-sitter-wasm` 里没有,取自 npm 包 `tree-sitter-wasm`(预编译、每个 wasm 都签过名、上游提交钉死),打包时和别的语法包一起复制进 `dist/tree-sitter/`(见「解析运行时」)。 +- 分工同别的语言:语言插件(`languages/dockerfile.ts`)报块和 facts;翻译插件(`translators/dockerfileZh.ts`)定用词;RUN、shell 写法的 CMD / ENTRYPOINT、HEALTHCHECK 的检查命令是嵌着的 shell(`EmbeddedCode`),宿主交给 shell 的语言插件、翻译插件和命令规则读(见下「嵌着的代码」),Dockerfile 这边不再读一遍 shell;数组写法的命令(`["node", "server.js"]`)不经过 shell,由 Dockerfile 语言插件报成命令(`LanguageAdapter.command`),同样交给命令规则。webview 不认识 Dockerfile,只多认几种块的种类。 +- 块(图标、颜色见「预览(代码块树)」「颜色」): + 1. 解析指令(directive,属性表):文件最开头连着的 `# syntax=…`、`# escape=…`、`# check=…`,一条一行:「语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件)」「转义字符 …」「检查 …」。别的注释、空行、指令之后的不算。 + 2. 阶段(stage,属性表):每个 FROM 开一个阶段,到下一个 FROM 之前的指令、注释都是它的子块;第一个 FROM 之前的(全局的 ARG)照常是顶层的块。卡片:名称(`AS` 后面的名字;没起名的写「没起名,按序号 N 引用」)/ 说明(紧贴在 FROM 上面、中间没空行的几行注释,算在阶段的头部里)/ 基础镜像(「node,标签 22-alpine」,写了摘要的加「,摘要 sha256:…」,接着前面某个阶段建的写「阶段 builder」,写成构建参数的读成「BASE(构建参数)」)/ 平台(写了 `--platform` 才有)。阶段名能点:`COPY --from=deps`、`RUN --mount=…,from=deps`、`FROM deps` 里的 deps ⌘ / Ctrl + 点击跳到那个阶段,单击高亮各处出现。阶段默认展开。 + 3. 执行(run):RUN 里的 shell 由 shell 的语言插件分步(`LanguageAdapter.steps`):只有一条命令时一行,就是那条命令读成的(「pnpm 装依赖(严格按 lock 文件、不跑安装脚本)」),一条循环、条件的子块归这个 RUN;只用 `&&` 连着的一串拆开,一条一步,头一行写「执行」,3 条以上加「(N 条命令,一条失败就停)」(原型 2 条时只写「执行」,10 条时带条数);分几句写的(heredoc 里一行一句)是「(N 条命令)」。续行的 `\` 去掉、各段拼起来读(`RUN apt-get install -y \⏎ curl git`);`RUN < f && b` 这种整串带重定向的(tree-sitter-bash 这样读),重定向只算在 echo 那一步上。RUN 的选项写在括号里:`--mount=type=cache,target=X` → 缓存目录 X 跨构建保留,`type=secret,id=X` → 临时挂上密钥 X,`type=ssh` → 转发 SSH 密钥,`type=tmpfs,target=X` → X 用内存临时目录,`type=bind`(默认)→ 临时挂上 [阶段 F 的] 源 到 目标,`--network=none / host / default` → 不联网 / 用宿主机的网络 / 用默认网络,`--security=insecure / sandbox` → 不隔离权限 / 隔离权限,别的照抄。 + 4. 复制(copy):「[从阶段 X | 从镜像 Y] 复制 源 → 目标(属主 u:g)」;源是 `.` 写「整个构建目录」,目标是 `.` 写「./」;`--chmod` → 权限,`--parents` → 保留目录层级,`--exclude` → 跳过 X,`--link` → 单独成层。ADD:源全是网址写「下载」,全是本地的压缩包(.tar、.tar.gz、.tgz……,ADD 会自动解开)写「解压」,git 仓库写「拉取仓库」,`--checksum` → 校验;`COPY < f` 写「打印 "…"(写到 f)」(原型「写入 … 到 f(覆盖)」,重定向统一由翻译插件接上);echo 里的 `[signed-by=…]` 照源码写全。 + - 检查命令 `curl -f … || exit 1` 照 Shell 读成「请求 …(失败就报错),失败就 退出,退出码 1」(原型「或者」)。 + - 原型第 12、13 行(`COPY --from=deps …`、`COPY . .`)分开画,所以 COPY 只在从同一个地方复制时才并。 + - 后端文件开头三行注释紧贴着 FROM,读成这个阶段的「说明」(原型没画这三行)。 + - RUN 拆开的各步是 shell 的块,种类照 shell 的(命令、管道、变量……),不都叫「执行」。 +- 嵌着的代码(通用的做法,见「通用契约」的 `EmbeddedCode`):语言插件报出这份源码里嵌着的别的语言的代码(`LanguageAdapter.embedded`:装着它的节点、语言、源码里属于它的几段,去掉了外层的记号)。宿主按语言找注册的语言插件、选中的翻译插件和命令规则,只解析这几段(`includedRanges`,节点下标照旧是整份源码的),按 `LanguageAdapter.steps` 分步:只有一步时这一步的子块归嵌着它的块,几步时各步是它的子块,都用那种语言的翻译插件翻、那种语言的词性着色(外层的源码着色上面叠一层);翻译插件翻嵌着它的块、属性表的行时问 `SourceDocument.embedded`,拿到各步读成的一行,连词、「N 条命令」由它自己定。那种语言没注册时照抄、没有子块。同一文件里的名字把嵌着的代码里认出的一并交给宿主。 +- 样例(`fixtures/clientNextjs.Dockerfile`、`fixtures/backend.Dockerfile`): + +| 源码 | 预览 | +| --- | --- | +| `# syntax=docker/dockerfile:1.7` | 语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件) | +| `FROM node:22-alpine AS deps` | 名称 deps / 基础镜像 node,标签 22-alpine | +| `RUN corepack enable && corepack prepare pnpm@latest --activate` | 执行,子块「开启 corepack」「准备 pnpm latest(并设为当前用的版本)」 | +| `COPY package.json pnpm-lock.yaml ./` | 复制 package.json、pnpm-lock.yaml → ./ | +| `RUN pnpm install --frozen-lockfile --ignore-scripts` | pnpm 装依赖(严格按 lock 文件、不跑安装脚本) | +| `COPY --from=deps /app/node_modules ./node_modules` | 从阶段 deps 复制 /app/node_modules → ./node_modules | +| `COPY . .` | 复制 整个构建目录 → ./ | +| `ARG DEPLOYMENT_ID` ⏎ `ENV NEXT_DEPLOYMENT_ID=$DEPLOYMENT_ID` | 构建参数 DEPLOYMENT_ID / 默认值 没写,构建时传;环境变量 NEXT_DEPLOYMENT_ID / 值 DEPLOYMENT_ID(构建参数) | +| `RUN pnpm build` | pnpm 构建 | +| `ENV NODE_ENV=production` ⏎ …(4 条) | 环境变量 4 条连着写,表 `NODE_ENV \| production`、`NEXT_TELEMETRY_DISABLED \| 1`、`PORT \| 3000`、`HOSTNAME \| 0.0.0.0` | +| `RUN addgroup --system --gid 1001 nodejs && \` ⏎ `adduser --system --uid 1001 nextjs` | 执行,子块「新建系统用户组 nodejs(gid 1001)」「新建系统用户 nextjs(uid 1001)」 | +| `COPY --from=builder --chown=nextjs:nodejs …`(2 条) | 复制(2 条连着写,属主都是 nextjs:nodejs),表 `阶段 builder \| /app/.next/standalone \| ./` | +| `USER nextjs`、`EXPOSE 3000` | 之后以 nextjs 身份运行、开放端口 3000 | +| `CMD ["node", "server.js"]` | 命令 node server.js / 写法 数组,不经过 shell | +| `LABEL maintainer="Cruldra" \` ⏎ `description="…"` | 标签,表 `maintainer \| Cruldra`、`description \| luffy-agent-platform - …` | +| `RUN apt-get update && \` ⏎ …(10 条) | 执行(10 条命令,一条失败就停),子块「更新软件包列表」「安装 ca-certificates、curl、gnupg、postgresql-client、git(不装推荐的包、不用确认)」「建目录 /usr/share/keyrings」「请求 https://…/ACCC4CF8.asc(不打印进度、失败就报错、跟着跳转走) → 转成二进制密钥 写到 /usr/share/keyrings/postgresql.gpg」「读入 /etc/os-release 里的变量和函数」「打印 "deb …"(写到 /etc/apt/sources.list.d/pgdg.list)」…「打印 pg_repack 的版本」「删除 /var/lib/apt/lists/*(连同里面的东西、不提示)」 | +| `COPY --from=node:22-slim …`(2 条) | 复制(2 条连着写),表 `镜像 node:22-slim \| /usr/local/bin/node \| /usr/local/bin/node` | +| `COPY --chown=appuser:appuser …`(12 条) | 复制(12 条连着写,属主都是 appuser:appuser),表「源 \| 到」12 行 | +| `RUN uv sync --frozen --no-dev --no-install-project \` ⏎ `--no-install-package agent-dsl \` …(11 个) | uv 按 lock 同步依赖 · 4 个选项,表 `--frozen \| 严格按 lock 文件,不改它`、`--no-dev \| 不装开发依赖`、`--no-install-project \| 不装本项目`、`--no-install-package × 11 \| 不装这些包:agent-dsl、…` | +| `HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \` ⏎ `CMD curl -f http://localhost:8000/health \|\| exit 1` | 间隔 30 秒 / 超时 5 秒 / 启动后宽限 10 秒 / 失败几次算不健康 3 次 / 检查命令 请求 http://localhost:8000/health(失败就报错),失败就 退出,退出码 1 | +| `CMD ["uv", "run", "uvicorn", "luffy_agent.main:app", "--host", "0.0.0.0", "--port", "8000"]` | 命令 用 uv 运行 uvicorn luffy_agent.main:app,监听 0.0.0.0:8000 / 写法 数组,不经过 shell | + ### 注释的 Markdown 排版 - 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring 和别的当注释用的单独字符串)的预览,函数、类、常量、变量属性表的「说明」,字段译文下面的说明。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。 @@ -401,6 +461,7 @@ | TypeScript / JavaScript | 定义那一行行尾的 `// @Comment 当前用户`(块注释不算) | 参数、变量、枚举成员、类字段、接口属性 | | TypeScript / JavaScript | 兜底:函数的 JSDoc 里 `@param [{类型}] 名字 [-] 说明`(`[名字]`、`[名字=默认]` 也认,`opts.x` 这种属性不认),同上取第一小句、不超过 12 个字 | 参数(函数声明、方法、值是函数的变量和类字段) | | Shell | 赋值、`local` / `export` / `readonly` 声明那一行行尾的 `# @Comment 安装目录` | 变量 | +| Dockerfile | 紧挨在指令上面一行、只写了 `# @Comment 部署 ID` 的注释(Dockerfile 的注释只能单独成行) | ARG、ENV(只有一个变量的)、`FROM … AS` 的阶段名 | - 行尾记号给那一行第一个能带行尾记号的定义(上表第三行、第六行那些;函数、类用装饰器),单独成行的注释、那一行没有这种定义的不算。 - 同一个定义几处都写了:写在代码里的记号(注解、装饰器)优先,其次行尾记号,最后才是说明文字。 @@ -472,6 +533,7 @@ export type BlockKind = | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable' | 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment' | 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' // shell:解释器、选项(属性表),分情况、命令、管道(一行译文) + | 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start' // Dockerfile:解析指令、阶段、健康检查、启动(属性表),执行、复制、设置、标签(一行译文) /** 源码里一段的下标(源码字符串下标),end 为开区间。 */ export interface SourceRange { startIndex: number, endIndex: number } @@ -514,6 +576,30 @@ export interface InterpreterFacts { program: string, lookup: 'env' | 'path', com export interface SwitchFacts { id: string, on: boolean, flag: string } export interface OptionsFacts { switches: SwitchFacts[] } +/** 文件开头给构建工具看的解析指令(`# syntax=docker/dockerfile:1.7`),一行一条。 */ +export interface ParserDirective { name: string, value: string } +export interface DirectiveFacts { directives: ParserDirective[] } + +/** 指令后面的一个选项(`--from=deps`):值在源码里从哪开始(值里的阶段名、变量能跳到定义)。 */ +export interface InstructionFlag { node: Node, name: string, value: string, valueStart: number } + +/** 一个构建阶段(FROM):阶段名、第几个(没起名的按它引用)、紧贴在上面的注释、镜像名、是不是前面的阶段、标签 / 摘要(带 : / @)、选项。 */ +export interface StageFacts { name?: Node, index: number, doc?: Node[], image: Node, stage: boolean, tag?: Node, digest?: Node, flags: InstructionFlag[] } + +/** 要执行的命令的两种写法:数组(不经过 shell,node 是整个数组)、交给 shell 的一段嵌着的代码。 */ +export type CommandForm = { form: 'exec', node: Node } | { form: 'shell', code: EmbeddedCode } +/** 健康检查:none 是 `HEALTHCHECK NONE`,否则选项和检查命令。 */ +export interface HealthFacts { none: boolean, flags: InstructionFlag[], command?: CommandForm } +/** 容器启动时执行的命令:CMD(默认命令)或 ENTRYPOINT(入口)。 */ +export interface StartFacts { instruction: 'cmd' | 'entrypoint', command: CommandForm } + +/** 写在这门语言里的另一种语言的代码:外层语法树里装着它的节点、语言、源码里属于它的几段(去掉了外层的记号,拼起来是一份那种语言的代码)。 */ +export interface EmbeddedCode { node: Node, languageId: string, ranges: SourceRange[] } +/** 嵌着的代码被当成一串步骤执行时的分块:一步一块,chained 表示前一步失败就不往下走(shell 的 &&)。 */ +export interface StepBlocks { blocks: CodeBlock[], chained: boolean } +/** 宿主读嵌着的代码读出来的:各步读成的一行(注释不算一步)、是不是一步失败就停、整段是不是就一步(这一步的子块归嵌着它的块)。 */ +export interface EmbeddedReading { steps: { text: Inline[], table?: PreviewTable }[], chained: boolean, whole: boolean } + /** 父类的一项:TS `extends Base` 的 Base(表达式)和它后面的泛型实参 ;Python 类参数列表的一项(含 metaclass=…、*bases)。 */ export interface BaseFacts { value: Node, typeArguments?: Node } @@ -626,7 +712,8 @@ export interface DeclaratorFacts { export interface VariableFacts { access?: Access doc?: Node // 同 FunctionFacts.doc:Python 模块、类主体里紧跟在这条赋值后面的字符串节点(属性 docstring) - declarators: DeclaratorFacts[] + declarators: DeclaratorFacts[] // 几条连着写的声明并成一块时(Dockerfile 连着的 ENV)是它们的全部 + scope?: 'build' | 'environment' // 语言分这个才填:build 是构建参数(ARG),environment 是环境变量(ENV) } /** @@ -656,7 +743,9 @@ export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts } export interface ExceptionBlock extends BlockShape { kind: 'exception', facts: ExceptionFacts } // 头部是整个类,留下的方法(含构造函数)等是子块 export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts } export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts } -export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock +export type FactBlock + = | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock + | DirectiveBlock | StageBlock | HealthBlock | StartBlock // 各带自己的 facts:InterpreterFacts、OptionsFacts、DirectiveFacts、StageFacts、HealthFacts、StartFacts /** 带说明(facts.doc)的块。 */ export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock /** 属性表块的种类和 facts,不带块的形状:库规则把一块认成现成的一种属性表块时给它。 */ @@ -667,6 +756,8 @@ export type TextKind = Exclude export interface TextBlock extends BlockShape { kind: TextKind doc?: Node // 同 VariableFacts.doc:Python 字段后面紧跟的属性 docstring;预览在译文下面按 Markdown 显示(Translator.doc) + group?: Node[] // 几条连着写的同种语句并成的一块(Dockerfile 连着的 COPY、LABEL):各条,node 是第一条;翻译插件排成一行字加一张表 + embedded?: EmbeddedCode // 这块执行的是嵌着的另一种语言的代码(RUN):宿主分步,步是它的子块(只有一步时是那一步的子块) } export type CodeBlock = TextBlock | FactBlock @@ -754,6 +845,10 @@ export interface LanguageAdapter { command?: (node: Node) => CommandFacts | undefined /** 一份源码里自己认得出的名字(LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */ symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[] + /** 这份源码里嵌着的别的语言的代码(块里的、属性表里的都算);没有的语言不填。 */ + embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[] + /** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块;不填就按 toBlocks 分、不算一步失败就停。 */ + steps?: (root: Node, source: string, languageId: string) => StepBlocks /** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */ displayNames?: DisplayNameReader } @@ -784,6 +879,8 @@ export interface SourceDocument { calleeKind?: (call: CallFacts) => BlockKind | undefined /** 这个命令名的命令规则(这种语言注册的,先注册的算);没有时命令照抄。规则出错由宿主记下、当它没读。 */ commandRule?: (name: string) => CommandRule | undefined + /** 嵌着的代码(EmbeddedCode.node 是 node 的那段)读出来的样子:宿主用那种语言的插件读好了交出来;那种语言没注册时没有。 */ + embedded?: (node: Node) => EmbeddedReading | undefined } /** 一份源码里语言插件自己认得出的一个名字:定义处,和用到它的各处(含定义处)。没有定义、高亮提供者的语言(shell)靠它跳转、高亮。 */ @@ -907,7 +1004,7 @@ export interface CommandWord { node: Node, literal?: string } export interface CommandFacts { name: CommandWord, arguments: CommandWord[], redirects: Redirect[] } /** 规则读出来的一行;value:读出来的是一个东西(`dirname "$0"` 是脚本所在目录),不是一个动作。 */ -export interface CommandReading { text: Inline[], value?: boolean } +export interface CommandReading { text: Inline[], value?: boolean, table?: PreviewTable } // table:选项写得多的命令的「选项 | 意思」表 export interface CommandContext { displayLanguage: string // 当前翻译插件的译文语言,规则按它挑词表 @@ -919,17 +1016,18 @@ export interface CommandRule { id: string // 'shell.rm' label: string // 'rm' languageIds: string[] - commands: string[] // 管哪些命令名;几条规则管同一个名字时先注册的算 + commands: string[] // 管哪些命令名;几条规则管同一个名字时先注册的算;'*' 管没有自己的规则的命令 read: (command: CommandFacts, context: CommandContext) => CommandReading | undefined // 读不了返回 undefined,命令照抄 } ``` - `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。 -- 内置的 TypeScript、Python、Shell 语言插件、三个中文直译翻译插件、SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则和 shell 命令规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。 +- 内置的 TypeScript、Python、Shell、Dockerfile 语言插件、四个中文直译翻译插件、shell / 装系统 / 装软件包的命令规则和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。 - 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。 - 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的 `call` 给结构(节点 → `CallFacts`),跨几行写的由翻译插件排成一行字加一张表(`PreviewTable`,见「调用与构造」);webview 只画表。 - 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ source, language }`(`SourceDocument`,language 是解析这份源码的语言插件)交给 `translate` / `describe`,翻译插件要词性、遍历结构都从 `language` 取,不 import 任何语言插件的模块(lint 规则挡住 `translators/` 下的非测试文件 import `languages/`)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。 - 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`call`(调用的结构)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `call` 的调用不摊成表,没有 `displayNames` 就没有中文名。 +- 嵌着的代码的分工:外层的语言插件只报是哪几段(`LanguageAdapter.embedded`、`TextBlock.embedded`、`CommandForm`),宿主按语言找那种语言的语言插件、翻译插件、命令规则解析和分步(`LanguageAdapter.steps`),外层的翻译插件只问读出来的样子(`SourceDocument.embedded`),连词、条数这些叫法归它;两种语言的插件互不 import。 - 命令的分工:语言插件认出一条命令的结构(`LanguageAdapter.command`),命令规则按命令名读成译文(`SourceDocument.commandRule`,宿主按语言、注册顺序给),翻译插件读命令时问它、接上重定向;规则只从契约拿东西,不认识语言插件、翻译插件。 - 中文名的分工:语言插件只认写法(`DisplayNameReader.find`:定义处的名字、中文名、注释里的记号);宿主解析一次源码时顺带问它,记号放进 `SourceDocument.markers` 交给翻译插件(翻注释、说明时去掉,中文模板直译在 `ZhTable.commentText` 之前去掉),定义交给预览找用到它的各处(`nameResolution.ts`);翻译插件不认识 `@Comment`,webview 不认识任何语言。 - 翻译插件拿到的:代码块(kind、node、header、children,属性表块还有 facts)和 `SourceDocument`。按语法节点类型写的叫法(关键字表、按语境换的译法、节点模板、注解位置这类只关乎措辞的判断)属于翻译插件自己,写在它的表里。 @@ -1318,13 +1416,16 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 - 规则不认识的选项照抄在译文后面(`rm -rf --one-file-system x` → 删除 x(连同里面的东西、不提示) --one-file-system)。 - 重定向不归规则:翻译插件读完命令再统一接上(见「Shell 脚本」),所以每条规则都不用管 `>&2`、`2>/dev/null`。 - 译文语言:内置规则只有中文词表,`CommandContext.displayLanguage` 不是 `zh` 时不读(照抄)。 -- 内置规则(`commands/shellCommands.ts`,认选项的共用办法在 `commands/reading.ts`:`-rf` 连写拆开,`-o 值` / `-o值` / `--retry 3` / `--retry=3` 都认,`--` 之后都不是选项): +- 管哪些语言:内置规则的 `languageIds` 是 shellscript 和 dockerfile(Dockerfile 数组写法的命令不经过 shell,执行的还是同一些程序)。 +- 没有自己的规则的命令交给管 `'*'` 的规则:内置的只认只写了一个 `--version`(`-V`、`version`)的命令,读成「打印 pg_repack 的版本」,别的照抄。 +- 选项多的:规则把认出的选项连同意思交给 `commands/reading.ts` 的 `explained`(选项在源码里的写法、写了几遍都由 `parseOptions` 记下),4 个以上(同一个写几遍算一个)或有写了几遍的,这一行后面写「· N 个选项」,意思排成「选项 | 意思」表(`CommandReading.table`,翻译插件排在这一行下面,同调用的实参表),写了几遍的并成一行「--no-install-package × 11 | 不装这些包:…」;别的写在括号里。 +- 内置规则(文件、下载、打包、文本处理和 shell 自带的在 `commands/shellCommands.ts`,建用户、密钥、装文件、改属主和 `--version` 在 `commands/systemCommands.ts`,装软件包、装依赖、跑程序的在 `commands/packageCommands.ts`;认选项的共用办法在 `commands/reading.ts`:`-rf` 连写拆开,`-o 值` / `-o值` / `--retry 3` / `--retry=3` 都认,`--` 之后都不是选项): | 命令 | 读成 | | --- | --- | | `rm -rf dest` | 删除 dest(连同里面的东西、不提示) | | `mkdir -p d` | 建目录 d(连同上级目录) | -| `curl -fL --retry 3 -o out url` | 下载 url 到 out(失败就报错、跟着跳转走、最多重试 3 次) | +| `curl -fL --retry 3 -o out url`、`curl -fsSL url` | 下载 url 到 out(失败就报错、跟着跳转走、最多重试 3 次)、请求 url(不打印进度、失败就报错、跟着跳转走;`-sS` 一起写只写「不打印进度」) | | `tar -xzf f -C d`、`unzip -q f -d d` | 解压 tar.gz f 到 d、解压 zip f 到 d(不打印过程) | | `find d -mindepth 1 -maxdepth 1 -type d ! -name m -exec rm -rf {} +` | 找出 d 下一层的目录,除了 m,逐个 删除(连同里面的东西、不提示);`find d -maxdepth 1 -name '*.dll' -delete` → 找出 d 这一层的 *.dll,逐个 删除;`find d -type l` → 找出 d 下面所有符号链接 | | `cp -L a b`、`mv a b`、`mv a b d` | 复制 a 指向的实体文件 到 b、改名 a → b、移动 a、b 到 d | @@ -1338,15 +1439,26 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 | `du -sh a b` | 统计占用空间 a、b(按人读的单位、只看总数) | | `cat f`、`grep -qi error log`、`sed -i 's/a/b/g' conf` | 打印 f 的内容、在 log 里找含 error 的行(只看找没找到、不分大小写)、把 conf 里的 a 全换成 b(直接改文件) | | `chmod +x f`、`ln -sf t link`、`touch f` | 给 f 加上可执行权限、建符号链接 link → t(已有就替换)、更新修改时间 f(没有就建空文件) | -| `source f`、`read -r x`、`sleep 1` | 载入并执行 f、读一行进 x(不认转义)、等 1 秒 | +| `source f` / `. f`、`read -r x`、`sleep 1` | 读入 f 里的变量和函数、读一行进 x(不认转义)、等 1 秒 | | `continue`、`break`、`true` / `:`、`false` | 跳过这一个、跳出循环、什么也不做、失败 | +| `apt-get update`、`apt-get install -y --no-install-recommends a b`、`apt-get purge -y x`、`apt-get clean` | 更新软件包列表、安装 a、b(不装推荐的包、不用确认)、卸载 x(连配置一起删、不用确认)、清掉下载的安装包 | +| `apk add --no-cache --virtual .deps gcc`、`apk del .deps` | 安装 gcc(不留索引缓存、记成一组 .deps)、卸载 .deps | +| `pip install --no-cache-dir -r requirements.txt`、`pip3 install -U pip` | pip 安装 requirements.txt 里列的包(不留缓存)、pip 安装 pip(升级到最新) | +| `npm ci --omit=dev`、`npm run build`、`npm run migrate` | npm 按 lock 文件装依赖(不装开发依赖)、npm 构建、npm 运行脚本 migrate(build / test / dev / start / lint 这些常见的脚本名写成要做的事) | +| `pnpm install --frozen-lockfile --ignore-scripts`、`pnpm build`、`yarn install --immutable` | pnpm 装依赖(严格按 lock 文件、不跑安装脚本)、pnpm 构建、yarn 装依赖(严格按 lock 文件) | +| `corepack enable`、`corepack prepare pnpm@latest --activate` | 开启 corepack、准备 pnpm latest(并设为当前用的版本) | +| `uv sync --frozen --no-dev`、`uv pip install --system -r req.txt`、`uv run --no-sync uvicorn app:app --host 0.0.0.0 --port 8000` | uv 按 lock 同步依赖(严格按 lock 文件,不改它、不装开发依赖)、uv 安装 req.txt 里列的包(装进系统的 Python)、用 uv 运行(不先同步依赖) uvicorn app:app,监听 0.0.0.0:8000(`uv run` 后面的命令照它自己的规则读) | +| `useradd -m -s /bin/false appuser`、`adduser --system --uid 1001 nextjs`、`addgroup --system --gid 1001 nodejs`、`adduser web www` | 新建用户 appuser(建主目录、不能登录)、新建系统用户 nextjs(uid 1001)、新建系统用户组 nodejs(gid 1001)、把 web 加进组 www(Debian 和 Alpine / BusyBox 的写法都认) | +| `gpg --dearmor -o k.gpg`、`install -d /d`、`install -m 644 a /etc/a`、`chown -R u:g /x` | 转成二进制密钥 写到 k.gpg、建目录 /d、安装 a 到 /etc/a(权限 644)、把 /x 的属主改成 u:g(连同里面的东西) | +| `pg_repack --version` | 打印 pg_repack 的版本(管 `'*'` 的规则) | ## 解析运行时 -- 依赖 `@vscode/tree-sitter-wasm`(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python 语法包,版本配套)。 +- 依赖 `@vscode/tree-sitter-wasm`(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python / Bash 语法包,版本配套)。它没有的语法包(Dockerfile)取自开发依赖 `tree-sitter-wasm`(Crysthamus/tree-sitter-wasm:各家语法包在 GitHub Actions 里编译,上游提交钉死,每个 wasm 都用 cosign 签名、发 npm 带来源证明;tree-sitter-dockerfile 是 ABI 15,和 `@vscode/tree-sitter-wasm` 的运行时配得上),`esbuild.mjs` 用它的 `getWasmPath` 取出来,和别的一起复制进 `dist/tree-sitter/`;单元测试直接用包里的(`testSupport.ts` 的 `DOCKERFILE_WASM_DIR`)。 - 打包:`esbuild.mjs` 构建时把 `tree-sitter.wasm` 和用到的语法包复制到 `dist/tree-sitter/`(`package.json` 的 `files` 已包含 `dist/**/*`)。 - 运行时初始化一次 `Parser.init({ locateFile })` 指向 `dist/tree-sitter/tree-sitter.wasm`;语法包按路径懒加载并缓存。 -- 文件扩展名到语法包:`.ts` → typescript;`.tsx` → tsx;`.js/.jsx/.mjs/.cjs` → javascript(tree-sitter-javascript 支持 JSX);`.py` → python;shellscript(`.sh` / `.bash` / `.zsh`、首行写着 shell 解释器的文件)→ bash。以 `languageId` 为准。 +- 文件扩展名到语法包:`.ts` → typescript;`.tsx` → tsx;`.js/.jsx/.mjs/.cjs` → javascript(tree-sitter-javascript 支持 JSX);`.py` → python;shellscript(`.sh` / `.bash` / `.zsh`、首行写着 shell 解释器的文件)→ bash;dockerfile(`Dockerfile`、`Dockerfile.*`、`*.dockerfile`、`Containerfile`)→ dockerfile。 +- 嵌着的代码只解析源码里的几段(`TreeSitterRuntime.parse` 的 `ranges`,交给 web-tree-sitter 的 `includedRanges`),节点下标、行列照旧是整份源码的。以 `languageId` 为准。 ## 代码块树规则 @@ -1399,6 +1511,14 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 - `[ … ]`、`[[ … ]]`(前面可以带 `!`),和用 `&&` / `||` 连起来、全是条件或前面全是条件最后跟一句的一串 → if。 - `return` 命令 → return;别的命令(带不带重定向)、`&&` / `||` 连起来的一串命令 → command;其余语句 → statement。 +### Dockerfile + +- 文件最开头连着的 `# syntax=` / `# escape=` / `# check=`(从第一行起、中间没空行)→ directive。 +- 每个 `from_instruction` 开一个 stage,头部是 FROM 那一行,紧贴在上面(中间没空行)的几行注释是它的说明、头部往上延;到下一个阶段(连同它上面的说明)之前的指令、注释是子块;第一个 FROM 之前的照常是顶层的块。 +- 相邻行的注释合成一块注释;只写了中文名记号、紧挨着下一条指令的注释算在那条指令的块里。 +- `run_instruction` → run(整条指令,shell 写法的带 `embedded`);`copy_instruction`、`add_instruction` → copy;`label_instruction` → label;`env_instruction`、`arg_instruction` → variable(facts 的 scope 分构建参数、环境变量);WORKDIR / USER / EXPOSE / VOLUME / STOPSIGNAL / SHELL / MAINTAINER → setting;`healthcheck_instruction` → health;`cmd_instruction`、`entrypoint_instruction` → start;`onbuild_instruction` → setting,包着的指令照这些规则是它的子块;别的(语法错误)→ statement。 +- 连着写的同种指令(紧挨着下一行)并成一块:ENV、ARG 并成一个变量块(declarators 是各条的全部),LABEL 和从同一个地方复制的 COPY 并成一块(`TextBlock.group`,头部盖住全部);ADD、带 heredoc 的 COPY 不并。 + ### 属性表信息(facts)从哪来 - TypeScript 函数:`function_declaration` / `generator_function_declaration` / `function_signature`、类里的 `method_definition` / 方法签名、值是带语句块主体函数的变量和类字段、`export default` 的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自 `formal_parameters`(`required_parameter` / `optional_parameter` 的 pattern、type、value,JavaScript 的 `assignment_pattern`),不带括号的单参数箭头函数取 `parameter`;返回类型取 `return_type` 里的类型;异步看有没有 `async`;类成员的装饰器是类主体里排在它前面的 `decorator` 兄弟节点。 @@ -1411,6 +1531,7 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 - TypeScript 枚举:`enum_declaration` 的 name、body;说明、访问级别同类(`export` 了公共、顶层没 `export` 私有、嵌套的没有);没有装饰器、父类。成员是 body 里的 `property_identifier`(没写值)和 `enum_assignment`(name、value),照源码顺序;没写值的接着上一个数往上加(第一个 0),上一个的值是数字字面量(可带正负号,`0x10` 这类也算)或算出来的数才算得出,否则没有 `computed`。说明:从上一个成员之后起单独成行的注释(`//` 和 JSDoc),和写在它这一行行尾的注释;同一行上一个成员后面的 JSDoc 讲的是下一个成员;最后一个成员后面的注释不算。 - Python 导入:`import_statement` 每个 name 一个来源、导入项是整个模块;`import_from_statement` 的 module_name 和各 name(`aliased_import` 拆出 alias),`wildcard_import` 是全部;`future_import_statement` 的来源是 `__future__`。 - Shell:解释器取 `#!` 后面的整句,第一个词是 `env` 的是按 PATH 找、程序名是它后面那个词,否则是写死的路径、程序名是路径最后一节。选项:每个开关一项,单字母换成 bash 的长名字(e → errexit、u → nounset、x → xtrace……),`-o 名字` 照写,`-` 开、`+` 关。变量:`variable_assignment` 的 name / value,声明里没给值的只有 name。函数:name 是函数名,doc 是紧贴在上面的注释;`positional` 是主体里(不进嵌套的函数)用到的 `$N` / `${N}` / `${N:-…}`,照位置排,`local x="$N"`、`x=$N` 给它起名字;`prints` 是没重定向到别处的 `echo` / `printf`(进 if、循环、分情况,不进 `$(…)`、管道、嵌套的函数);`captured` 看同一文件里有没有 `$(函数名 …)`;`returns` 是主体里的 `return` 命令。 +- Dockerfile:解析指令是每行注释 `# 名字=值` 的名字(小写)和值;阶段取 `image_spec` 的 name / tag / digest 字段、`as` 字段(image_alias),stage 看镜像名是不是前面某个阶段的名字(不分大小写),选项是 `param`、`mount_param` 节点的原文 `--名字=值`(值在语法树里没有单独的节点,按原文拆,记下值从哪开始);变量是 `env_pair` 的 name / value、`arg_instruction` 的 name / default;健康检查的 none 看有没有 NONE,检查命令是里面的 `cmd_instruction`;数组写法取 `json_string_array`,shell 写法取 `shell_command` 里各个 `shell_fragment`(`line_continuation` 不算),只有一个 `< f` 在 shell 脚本里也把重定向写在 echo 上(`translators/bashZh.test.ts`)。 + - 命令规则(`commands/buildCommands.test.ts`):装软件包、装依赖、跑程序、建用户、密钥、装文件、改属主各条的读法;选项到 4 个、写了几遍时排成表;`'*'` 的规则只认 `--version`;命令名 → 规则先注册的算、没有的交给 `'*'`、出错记下来当没读。 + - 宿主(`editor/documentMessage.test.ts`):注册了 shell 和 Dockerfile 时 RUN 的各步是子块、按 shell 的种类,筛选栏有 Dockerfile 的种类名,名字连同 shell 里的一起交出。 - 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中;库规则按语言过滤、保持注册顺序,注销后不再给。 - 库规则(宿主):导入了库(`from sqlalchemy.orm import …`、`import sqlalchemy as sa`、`import os, sqlalchemy`、函数里的导入)才启用,`sqlalchemy_utils`、相对导入 `.sqlalchemy` 不算;先注册的规则优先;规则抛错、替身不合约定时记下来、接着问下一个、都没认照常显示,document 消息不带 error;编辑范围是整个类、没并进来的子块(夹在中间的注释、两个字段中间的方法)照源码顺序留下,卡片「编辑」写回整个类,能编辑的范围只在块和它的子孙之间重叠;筛选的种类计数、名字取规则的标签;规则自己报的筛选属性名字取规则的 `attributeNames`(翻译插件给了的以它为准,`attributes.test.ts` 的合并、`documentMessage.test.ts` 的「请求方法」),规则起名时抛错记下来、照写 id;`rules/` 下的非测试文件只 import contract。 - 同一行不在两处能改(webview `editing.test.ts`):卡片编辑时盖住它留下的子块(字段中间的方法连同主体),跳到被盖住的块改闪卡片;只编辑头部的块不盖住子块,一行写完的块盖住同一行的子块;点「编辑」时重叠的块退出编辑;任意点法下正在编辑、画出来的块两两不重叠。