diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 40427fc..c341621 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 | +| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python、Shell 脚本(bash / sh / zsh) | | 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 `activate()` 导出的 API 注册;内置实现也走同一个 API | | 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 | | 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 | @@ -42,6 +42,8 @@ | 中文名 | 代码里给定义起的中文名(`Annotated[T, Comment("当前用户")]`、`@Comment("…")`、行尾 `# @Comment 当前用户`,没有时取参数在 docstring / JSDoc 里的说明):预览里这个定义和用到它的各处都显示中文名,悬停看原名,定义处本身在中文名后面淡淡写原名。语言插件认写法,用到它的各处由语言扩展按语义认(同一文件问 document highlights,别的文件定义的先查工作区索引、再问定义提供者),webview 只按源码下标换字。见「中文名」 | | 推导式(写在表达式里的遍历) | Python 的列表 / 集合 / 字典推导式、生成器表达式出现在译文任何地方(一行译文、属性表的值、默认值、字典项的值),都原地换成一个带循环图标和结果类型(生成器 / 列表 / 集合 / 字典)的小框,颜色同「循环」块,框里按读的顺序重排:「遍历 来源 的每个 变量,仅当 条件,取 值」。语言插件报结构,翻译插件定用词,webview 只认「框」不认语言 | | 调用与构造 | 实参列表跨了几行写的调用不挤成一长行:写成「[等待 ]新建 / 调用 被调用的名字 · 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(连同里面的东西、不提示)」);规则不认识的选项照抄在后面,没有规则的命令照抄命令名,重定向由翻译插件统一读。见「命令规则」 | ## 用户可见行为 @@ -113,7 +115,7 @@ ### 预览(代码块树) -- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。 +- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释;Shell 脚本另有解释器、选项、分情况、命令、管道(见「Shell 脚本」)。 - 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--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-*`(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种: @@ -126,15 +128,17 @@ | type 类型 | `braces` | if 条件 | `split` | return 返回 | `corner-down-left` | | enum 枚举 | `list-ordered` | else 否则 | `corner-down-right` | statement 语句 | `code` | | exception 异常 | `triangle-alert` | loop 循环 | `repeat` | comment 注释 | `message-square-text` | + | interpreter 解释器 | `terminal` | options 选项 | `sliders-horizontal` | case 分情况 | `signpost` | + | command 命令 | `square-terminal` | pipeline 管道 | `square-terminal` | | | 库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`。 - 这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 18 个加库规则用的 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`(上表 22 个——命令、管道共用一个——加库规则用的 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 算在它那一块里(中间隔着的空行也是)。 - 库规则认领的块(数据表卡片等,见「库规则」):种类的图标、颜色和名字由规则给,内容是规则给的属性表;它的编辑范围是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,各自的「编辑」照旧只改自己那几行。 @@ -164,7 +168,7 @@ ### 属性表(函数、类、枚举、异常、导入、常量、变量块) -卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。 +卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px,这张表里有更长的属性名时整列按最长的那个放宽,值照样对齐在同一处,属性名不折行:shell 的「管道里任一步失败都算失败」),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。 输入参数的表格(原型 `paramRows`):往右缩进 5px,第一行是表头「名称 | 类型 | 默认值」,下面每个参数一行。列宽按整列的内容定,webview 不认是哪种表格(库规则的字段表同样排,见 `tableTemplate`):整张表格一个网格、各行借用它的列,前几列宽裕时放到整列不换行,最后一列(默认值、说明)分剩下的、至少留 `min(20%, 8em)`;放不下时先让最后一列换行,编辑区再窄前几列平分剩下的宽度、在格子里换行(长名字从中间断开),不撑出卡片、不藏掉哪一列。格子左右留 8px、上下 2px,长类型、长默认值在格子里换行,不挤掉别的列。外框 1px、圆角 4px,格子之间、行之间 1px 分隔线,颜色都是主题的表格分隔色(`--vscode-tree-tableColumnsBorder`,主题没给时退到 `--vscode-editorWidget-border`);表头垫主题的表头底色(`--vscode-keybindingTable-headerBackground`,比编辑区深一档或浅一档),列名界面字体 12px、`descriptionForeground` 色。没有默认值的格子留空。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(`PropertyRow.columns`)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。 @@ -288,6 +292,50 @@ | `result = await config.get_payment_driver(\n order.payment_method\n).query_refund_status(\n out_trade_no=…,\n out_request_no=…,\n)` | 初始值 `等待 调用 config.get_payment_driver(order.payment_method).query_refund_status · 2 个参数` | | `context.subscriptions.push(\n treeView,\n commands.registerCommand('…', () => {\n …\n }),\n …\n)` | 一行 `调用 context.subscriptions.push · 4 个参数`;第 2 ~ 4 个那一格 `调用 commands.registerCommand · 2 个参数`,下面套它的 2 行表 | +### Shell 脚本 + +原型画板「Shell 脚本」(`code.js` 的「Shell 脚本」一节:`SHELL_KINDS`、`SHELL_BLOCKS`、`shellBlock`;README「Shell 脚本」)。样例是 luffy 桌面端的 `apps/desktop/scripts/fetch-runtime.sh`(复制成 `fixtures/fetchRuntime.sh`)。原型里的中文是手写的目标,插件读出来的逐行和它对得上;原型略去的 95–100、116–121 行也照样读。 + +- 哪些文件:VS Code 认成 `shellscript` 的文件,`.sh`、`.bash`、`.zsh`,还有首行写着 shell 解释器的无扩展名文件(VS Code 自己按首行认)。sh、zsh 也按 bash 的语法读(`tree-sitter-bash.wasm`)。 +- 分工和别的语言一样:语言插件(`languages/bash.ts`)在 tree-sitter-bash 上报块和事实,不认识中文;翻译插件(`translators/bashZh.ts`,中文模板直译)定用词;一条命令怎么读交给命令规则(见「命令规则」),按命令名注册;webview 不认识 shell,只多认几种块的种类。 +- 块(图标、颜色见「预览(代码块树)」「颜色」): + 1. 解释器(interpreter,属性表):首行 `#!`。「解释器」写程序名(`bash`),「怎么找」写 `按 PATH 去找(/usr/bin/env bash)`,写死路径的写 `写死的路径(/bin/sh -e)`。 + 2. 选项(options,属性表):一条 `set -euo pipefail` 一块,每个开关一行:行名是开关的意思(出错就停、用到没设的变量就报错、管道里任一步失败都算失败、打印执行的每条命令……),值是「是 / 否」,后面淡淡写源码里的写法(`-e`、`-o pipefail`、`+x`)。`set -- a b` 这种不是开关,照常是命令。 + 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」。 + 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`)按常量色。 +- 通配(分情况的情况、`[[ … ]]` 的右边):`*.zip` → 以 .zip 结尾,`*-x` → 以 -x 结尾,`x*` → 以 x 开头,`*x*` → 含有 x,别的写「匹配 …」;`a | b` → a 或 b;分情况里单独一个 `*` → 其他。 +- 照旧能用的:按种类筛选、单击名字高亮各处出现、⌘ / Ctrl + 点击跳到定义、中文名(行尾 `# @Comment 安装目录`)、文件注释、问 AI。VS Code 自己没有 shell 的定义、高亮提供者,所以同一文件里的名字由语言插件认(`LanguageAdapter.symbols`):函数的定义是函数名,用到的是同名的命令;变量的定义是赋值、`local` / `export` 声明、`for` 的变量、`read` 读进的名字,用到的是 `$x`、`${x…}`;第几个参数、`$?` 这类不算。跳到用到的那处之前最近的一处定义(同一个变量前后赋过几次值),前面没有就跳到第一处;高亮时定义处算写入、别处算读取。装了给 shell 提供定义、高亮的扩展时,先问它,它答不上才用语言插件认的。 +- 样例(`fixtures/fetchRuntime.sh`,`translators/bashZh.test.ts` 的 `FETCH_READING` 逐行核对): + +| 源码 | 预览 | +| --- | --- | +| `#!/usr/bin/env bash` | 解释器 bash / 怎么找 按 PATH 去找(/usr/bin/env bash) | +| `set -euo pipefail` | 出错就停 是 -e / 用到没设的变量就报错 是 -u / 管道里任一步失败都算失败 是 -o pipefail | +| `target="${1:-}"` | 名称 target / 初始值 第 1 个参数(没给就是空) | +| `here="$(cd "$(dirname "$0")/.." && pwd)"` | 名称 here / 初始值 执行 〔进入 脚本所在目录/.. 然后 打印当前目录〕 的输出 | +| `cache="${XDG_CACHE_HOME:-$HOME/.cache}/luffy-desktop-runtime"` | 名称 cache / 初始值 XDG_CACHE_HOME(没设就用 HOME/.cache)/luffy-desktop-runtime | +| `PY_RELEASE=20260924` | 常量:名称 PY_RELEASE / 值 20260924 | +| `case "$target" in x86_64-pc-windows-msvc) … ;; *) echo "用法: …" >&2; exit 2 ;; esac` | 按 target 分情况,表「情况 \| 做什么」:`x86_64-pc-windows-msvc \| GIT_URL = …⏎GIT_SHA256 = …⏎PY_SHA256 = …`、`其他 \| 打印 "用法: 脚本路径 <…>"(打印到错误输出)⏎退出,退出码 2` | +| `sha_ok() { [ -f "$1" ] && [ "$(shasum -a 256 "$1" \| cut -d' ' -f1)" = "$2" ]; }` | 名称 sha_ok / 输入参数 2 个,按位置:`第 1 个 \| $1`、`第 2 个 \| $2` / 返回 最后一句成不成功;子块「成立:文件 第 1 个参数 存在 并且 〔算 第 1 个参数 的 sha256 → 按空格切,取第 1 段〕 等于 第 2 个参数」 | +| `# 下载 …⏎fetch() { local url="$1" sha="$2" out … echo "$out"; }` | 名称 fetch / 说明 下载 (已缓存且校验通过就不重下), 回显本地文件路径 / 输入参数 2 个,按位置:`第 1 个 \| url`、`第 2 个 \| sha` / 输出 打印 out(调用处用 $(fetch …) 拿到) | +| `out="$cache/$(basename "${url//%2B/+}")"` | 名称 out / 初始值 cache/〔取文件名 url(其中 %2B 全换成 +)〕 | +| `if ! sha_ok "$out" "$sha"; then` | 如果 不 sha_ok out sha | +| `curl -fL --retry 3 -o "$out.part" "$url"` | 下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次) | +| `case "$git_pkg" in *.zip) unzip -q … ;; *) tar -xzf … ;; esac` | 按 git_pkg 分情况:`以 .zip 结尾 \| 解压 zip git_pkg 到 dest/git(不打印过程)`、`其他 \| 解压 tar.gz git_pkg 到 dest/git` | +| `*-apple-darwin)`(情况里有注释、好几句) | 子块「情况:以 -apple-darwin 结尾」 | +| `find "$gc" -mindepth 1 -maxdepth 1 -type d ! -name mergetools -exec rm -rf {} +` | 找出 gc 下一层的目录,除了 mergetools,逐个 删除(连同里面的东西、不提示) | +| `for f in "$gc"/git-*; do` | 遍历 gc/git-* 匹配到的每个文件 f | +| `[ -L "$f" ] && [ "$(readlink "$f")" = git ] \|\| continue` | 如果不是 〔f 是符号链接 并且 〔读出 f 指向的路径〕 等于 git〕,跳过这一个 | +| `find "$dest" -type l \| while read -r link; do` | 找出 dest 下面所有符号链接 → 逐行读进 link(循环主体是子块) | +| `du -sh "$dest/git" "$dest/python" >&2` | 统计占用空间 dest/git、dest/python(按人读的单位、只看总数)(打印到错误输出) | + +- 和原型不一样的地方(插件生成的,比手写的更一致):函数里的变量也是属性表块(同别的语言,不写成一行「局部 …」);`$N` 一律写「第 N 个参数」;选项的意思之间一律用顿号(「连同里面的东西、不提示」);`[ "$(readlink "$f")" = git ]` 照结构读成「〔读出 f 指向的路径〕 等于 git」,不合写成「f 指向 git」。 + ### 注释的 Markdown 排版 - 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring 和别的当注释用的单独字符串)的预览,函数、类、常量、变量属性表的「说明」,字段译文下面的说明。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。 @@ -314,6 +362,7 @@ 3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。 - 同一文件原地跳、跳进另一个文件的预览,规则一样。 - 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 +- VS Code 里没有这种语言的定义提供者(shell):语言扩展答不上时,同一文件里的名字由语言插件认(`LanguageAdapter.symbols`,见「Shell 脚本」),跳到用到的那处之前最近的定义。 - 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠,也不动高亮。 ### 高亮名字的各处出现 @@ -332,6 +381,7 @@ - 浏览器对触发命令的那个键自己的动作(⌘↓ 滚到底)照常发生,由跳转盖掉(`occurrences.ts` 的 `KeyScroll`):页面在按下任何键时记下滚在哪;命令发来时,按键后 1 秒内的(`KEY_TO_COMMAND_MS`)先回到按键那一刻的位置,再把目标滚进视口;按键后 0.5 秒内(`KEY_SCROLL_MS`)浏览器又滚走了就拉回跳到的位置。按键后点了鼠标、滚了滚轮、又按了键,或从命令面板运行(没有刚按的键),都不回、不守。 - 实测(隔离的真 VS Code,逐帧录屏):命令十几毫秒就到页面,浏览器的滚到底动画再过一帧才开始,而且在合成线程上播、页面忙着重画时照样播;不管的话页面最后停在底部(目标看不见)。只在滚动时拉回,重画期间仍有约 100 毫秒来回跳的画面;滚到原地浏览器当没滚、不打断已排上的动画,所以回位时先挪一个像素再回来(同一段脚本里连着两下,中间不画),动画在开始前就被打断,录屏里一帧都没有被滚走。 - 和编辑器一样每回点都现问,回答不留到下次用(语言扩展刚启动时答的「没有」或按文字认的结果不会一直留着);只认最近一次询问的回答,迟到的丢掉。点另一个名字:回答到之前旧高亮留着,到了就换成新的;回答是空的(没装语言扩展、没答)就什么都不亮,不按文字自己找。 +- 语言扩展答不上(shell 没有高亮提供者)时用语言插件认的同一文件里的名字(`LanguageAdapter.symbols`):定义处算写入、别处算读取;中文名落到哪些名字上也走这条路。 - 清掉(连同筛选栏上那句):点空白处或不是名字的字、按 Esc(输入框、下拉列表自己用掉的 Esc 不算)、文档换了版本、换了翻译。 - 不抢别的点击:⌘ / Ctrl(以及 Alt、Shift)+ 点击、双击的第二下、拖着选字都不动高亮(双击的第一下照样算一次单击,和编辑器一样);点在按钮、链接、输入框、下拉列表、筛选栏、属性名上照旧,高亮不动——折叠箭头、「编辑」、Markdown 里的链接都照旧;名字本身在按钮里(带下一层的子行的键)时照旧折叠,同时高亮。 @@ -350,6 +400,7 @@ | TypeScript / JavaScript | 装饰器 `@Comment("…")`(名字最后一节是 Comment) | 类(`@d export class` 也算)、方法、字段、方法和构造函数的参数 | | TypeScript / JavaScript | 定义那一行行尾的 `// @Comment 当前用户`(块注释不算) | 参数、变量、枚举成员、类字段、接口属性 | | TypeScript / JavaScript | 兜底:函数的 JSDoc 里 `@param [{类型}] 名字 [-] 说明`(`[名字]`、`[名字=默认]` 也认,`opts.x` 这种属性不认),同上取第一小句、不超过 12 个字 | 参数(函数声明、方法、值是函数的变量和类字段) | +| Shell | 赋值、`local` / `export` / `readonly` 声明那一行行尾的 `# @Comment 安装目录` | 变量 | - 行尾记号给那一行第一个能带行尾记号的定义(上表第三行、第六行那些;函数、类用装饰器),单独成行的注释、那一行没有这种定义的不算。 - 同一个定义几处都写了:写在代码里的记号(注解、装饰器)优先,其次行尾记号,最后才是说明文字。 @@ -420,6 +471,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:解释器、选项(属性表),分情况、命令、管道(一行译文) /** 源码里一段的下标(源码字符串下标),end 为开区间。 */ export interface SourceRange { startIndex: number, endIndex: number } @@ -446,8 +498,22 @@ export interface FunctionFacts { decorators: Node[] parameters: ParameterFacts[] returnType?: Node + positional?: PositionalParameter[] // 不写参数表、按位置取参数的语言(shell 的 $1):用到的第几个参数,照位置排;填了 parameters 就是空的 + prints?: Node[] // 函数打印到输出的那几句(shell 的 echo / printf,没重定向到别处) + captured?: boolean // 函数的输出被调用处拿去用了(shell 同一文件里有 $(函数名 …)) + returns?: Node[] // 只有成不成功的语言(shell):写了的 return 语句;空数组是「最后一句成不成功」。别的语言不填 } +/** 按位置取的一个参数:第几个(1 起),函数里给它起的名字(`local url="$1"`)。 */ +export interface PositionalParameter { position: number, name?: Node } + +/** 第一行 `#!` 写的解释器:程序名(bash),交给 env 按 PATH 找还是写死的路径,`#!` 后面的整句。 */ +export interface InterpreterFacts { program: string, lookup: 'env' | 'path', command: string } + +/** 打开 / 关掉解释器的一个开关(`set -e`、`set -o pipefail`):id 是语言自己的叫法(bash 的长名字 errexit……),flag 是源码里的写法。 */ +export interface SwitchFacts { id: string, on: boolean, flag: string } +export interface OptionsFacts { switches: SwitchFacts[] } + /** 父类的一项:TS `extends Base` 的 Base(表达式)和它后面的泛型实参 ;Python 类参数列表的一项(含 metaclass=…、*bases)。 */ export interface BaseFacts { value: Node, typeArguments?: Node } @@ -611,6 +677,7 @@ export type TokenRole = | 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null … | 'operator' // 被翻成文字的运算符:全等 并且 或者 非 | 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 … + | 'constant' // 按习惯全大写、当常量用的名字(shell 的 PY_VERSION、HOME) | 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain' /** @@ -683,6 +750,10 @@ export interface LanguageAdapter { iteration?: (node: Node) => IterationFacts | undefined /** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */ call?: (node: Node) => CallFacts | undefined + /** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;没有命令的语言不填。 */ + command?: (node: Node) => CommandFacts | undefined + /** 一份源码里自己认得出的名字(LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */ + symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[] /** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */ displayNames?: DisplayNameReader } @@ -711,8 +782,13 @@ export interface SourceDocument { * 同步地只交出已经查到的;还没查到、查不到、宿主不查都是 undefined,查到了宿主重排、重发(见「调用与构造」)。 */ calleeKind?: (call: CallFacts) => BlockKind | undefined + /** 这个命令名的命令规则(这种语言注册的,先注册的算);没有时命令照抄。规则出错由宿主记下、当它没读。 */ + commandRule?: (name: string) => CommandRule | undefined } +/** 一份源码里语言插件自己认得出的一个名字:定义处,和用到它的各处(含定义处)。没有定义、高亮提供者的语言(shell)靠它跳转、高亮。 */ +export interface LocalSymbol { definitions: SourceRange[], occurrences: SourceRange[] } + /** 筛选属性和值的显示名;没给名字的显示 id。 */ export interface AttributeNames { attributes: Record // access → 访问级别 @@ -737,8 +813,8 @@ export interface Translator { /** 一种颜色在暗色、亮色主题下各用哪个;高对比主题跟着暗 / 亮走。 */ export interface ThemeColor { dark: string, light: string } -/** 种类图标:插件自己定义的一组 lucide 图标(kebab-case 名字);前 18 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */ -export const KIND_ICON_NAMES = ['import', 'square-function', /* …18 个内置种类的… */ 'database', 'package', 'server-cog', 'calendar-clock', 'globe', 'network'] as const +/** 种类图标:插件自己定义的一组 lucide 图标(kebab-case 名字);前 22 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */ +export const KIND_ICON_NAMES = ['import', 'square-function', /* …22 个内置种类的… */ 'database', 'package', 'server-cog', 'calendar-clock', 'globe', 'network'] as const export type KindIconName = (typeof KIND_ICON_NAMES)[number] /** 库规则的种类:块左边只画图标(种类色),label 是悬停提示和读屏念的名字,筛选栏里也叫它;icon 只能从 KindIconName 里挑。 */ @@ -813,15 +889,48 @@ export interface CodeReaderApi { registerLanguage: (adapter: LanguageAdapter) => Disposable registerTranslator: (translator: Translator) => Disposable registerLibraryRule: (rule: LibraryRule) => Disposable + registerCommandRule: (rule: CommandRule) => Disposable +} + +// ── 命令规则(见「命令规则」一节)── + +/** 重定向:输出(1)、错误输出(2)或两样写到 / 追加到文件,并进另一路(`>&2`、`2>&1`),从文件读。 */ +export type Redirect = + | { kind: 'write' | 'append', stream: 'output' | 'error' | 'both', target: Node } + | { kind: 'merge', stream: 'output' | 'error', into: 'output' | 'error' } + | { kind: 'read', source: Node } + +/** 命令的一个词:节点,和它写死的文字(去掉引号、转义;有展开的没有)。 */ +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 CommandContext { + displayLanguage: string // 当前翻译插件的译文语言,规则按它挑词表 + translate: (word: Node) => Inline[] // 一个词照常翻(名字带源码下标) + command: (name: CommandWord, args: CommandWord[]) => CommandReading | undefined // 读写在实参里的另一条命令(find -exec) +} + +export interface CommandRule { + id: string // 'shell.rm' + label: string // 'rm' + languageIds: 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 语言插件、两个中文直译翻译插件和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。 +- 内置的 TypeScript、Python、Shell 语言插件、三个中文直译翻译插件、SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则和 shell 命令规则在 `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.command`),命令规则按命令名读成译文(`SourceDocument.commandRule`,宿主按语言、注册顺序给),翻译插件读命令时问它、接上重定向;规则只从契约拿东西,不认识语言插件、翻译插件。 - 中文名的分工:语言插件只认写法(`DisplayNameReader.find`:定义处的名字、中文名、注释里的记号);宿主解析一次源码时顺带问它,记号放进 `SourceDocument.markers` 交给翻译插件(翻注释、说明时去掉,中文模板直译在 `ZhTable.commentText` 之前去掉),定义交给预览找用到它的各处(`nameResolution.ts`);翻译插件不认识 `@Comment`,webview 不认识任何语言。 - 翻译插件拿到的:代码块(kind、node、header、children,属性表块还有 facts)和 `SourceDocument`。按语法节点类型写的叫法(关键字表、按语境换的译法、节点模板、注解位置这类只关乎措辞的判断)属于翻译插件自己,写在它的表里。 @@ -1199,12 +1308,45 @@ export interface CodeReaderApi { 配色:名称 function、标签 string、依赖名 function、「(参数名)」hint、表格第一列(项)hint、URL 同上、参数名 variable、类型按翻译插件、异常类名 type、消息和说明 comment、自动带的 422 hint。 +## 命令规则 + +shell 脚本大半是一条条命令,一条命令读成什么取决于命令本身(`rm -rf` 是「删除(连同里面的东西、不提示)」,`tar -xzf` 是「解压 tar.gz」)。这些既不是语法(语言插件不管),也不是某种语言的措辞(翻译插件不该认识几十个命令)。命令规则是第五种插件:按命令名注册,把一条命令连同它的选项读成一句译文,像库规则按库认写法一样。 + +- 契约:`CommandRule`(`id`、`label`、`languageIds`、`commands`、`read`,见「通用契约」)。语言插件认出一条命令(`LanguageAdapter.command` → `CommandFacts`:命令名、后面的各个词连写死的文字、重定向),翻译插件读到命令时按命令名问 `SourceDocument.commandRule`,交给规则的 `read`,拿回一行译文(`CommandReading`)。规则读选项、选项的值用词的 `literal`(去掉引号、转义的文字),要翻的词(变量、字符串、展开)交回 `CommandContext.translate`,名字照样带源码下标;`find … -exec rm -rf {} +` 里的命令用 `CommandContext.command` 读。 +- 注册:`CodeReaderApi.registerCommandRule(rule)`,返回 `Disposable`;内置规则走同一个 API。宿主按语言取注册着的规则(`registry.commandRulesFor`),按命令名排好(`commandRules.ts` 的 `commandRuleLookup`):几条规则管同一个名字时先注册的算。 +- 读不了:规则返回 undefined(不认识的写法、不是这种译文语言)或读的时候出错(宿主记下「命令规则 X 读「rm」时出错,照抄命令:…」、当它没读),命令照抄命令名、后面的词照常翻。没有规则的命令一样(`foo -x "$y"` → `foo -x y`)。 +- 规则不认识的选项照抄在译文后面(`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` 都认,`--` 之后都不是选项): + +| 命令 | 读成 | +| --- | --- | +| `rm -rf dest` | 删除 dest(连同里面的东西、不提示) | +| `mkdir -p d` | 建目录 d(连同上级目录) | +| `curl -fL --retry 3 -o out url` | 下载 url 到 out(失败就报错、跟着跳转走、最多重试 3 次) | +| `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 | +| `echo "下载 $url"`、`echo "$out"`、`printf '%s\n' "$x"` | 打印 "下载 url"(有写死的文字时连引号)、打印 out、按格式打印 "%s\n",填入 x | +| `exit 1`、`return 1` | 退出,退出码 1、返回 1 | +| `cd d`、`pwd` | 进入 d、打印当前目录 | +| `basename x`、`dirname x`、`dirname "$0"` | 取文件名 x、取所在目录 x、脚本所在目录(读出来是一个东西,`$(…)` 直接写它) | +| `readlink x`、`readlink -f x` | 读出 x 指向的路径、取 x 的真实路径 | +| `shasum -a 256 f`、`sha256sum f`、`md5sum f` | 算 f 的 sha256、算 f 的 md5 | +| `cut -d' ' -f1`、`cut -c1-3 f` | 按空格切,取第 1 段、取 f 每一行的第 1 到 3 个字符 | +| `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 秒 | +| `continue`、`break`、`true` / `:`、`false` | 跳过这一个、跳出循环、什么也不做、失败 | + ## 解析运行时 - 依赖 `@vscode/tree-sitter-wasm`(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python 语法包,版本配套)。 - 打包:`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。以 `languageId` 为准。 +- 文件扩展名到语法包:`.ts` → typescript;`.tsx` → tsx;`.js/.jsx/.mjs/.cjs` → javascript(tree-sitter-javascript 支持 JSX);`.py` → python;shellscript(`.sh` / `.bash` / `.zsh`、首行写着 shell 解释器的文件)→ bash。以 `languageId` 为准。 ## 代码块树规则 @@ -1244,6 +1386,19 @@ export interface CodeReaderApi { - `return_statement` → return;其余 → statement。 - 复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。 +### Shell + +- 首行 `#!` → interpreter;别的 `comment` 连着的几行合成一块注释;紧贴在函数定义上面(中间没空行)的注释是函数的说明,进 `facts.doc`、头部往上延,不单独成块。 +- `set` 后面全是开关(`-euo pipefail`、`+x`、`-o nounset`)→ options;有不是开关的词(`set -- a b`)→ command。 +- `variable_assignment`、`declaration_command`(local / export / readonly / declare 声明了变量的)→ variable;readonly 的、脚本顶层名字全大写且值里没有展开的 → const。一句声明几个变量是一块(`DeclaratorFacts` 各一项)。 +- `function_definition` → function,子块来自主体各句。 +- `if_statement` → if,子块是 then 后面的各句;`elif_clause` / `else_clause` 展平成紧跟的兄弟块 else。 +- `for_statement` / `c_style_for_statement` / `while_statement`(until 也是)→ loop,头部到 do 为止,子块是 do … done 里的各句。 +- `case_statement` → case:每个情况都短(最多 3 句,都是一行写完的命令、赋值、声明,没有注释)时整条一块;否则头部到 in 为止,每个 `case_item` 一块子块 case(头部是它的模式,到 `)` 为止),子块是要做的各句。 +- `pipeline` → pipeline;最后一段是循环时头部到 do 为止,循环主体是子块。 +- `[ … ]`、`[[ … ]]`(前面可以带 `!`),和用 `&&` / `||` 连起来、全是条件或前面全是条件最后跟一句的一串 → if。 +- `return` 命令 → return;别的命令(带不带重定向)、`&&` / `||` 连起来的一串命令 → command;其余语句 → statement。 + ### 属性表信息(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` 兄弟节点。 @@ -1255,6 +1410,7 @@ export interface CodeReaderApi { - 异常(TypeScript、Python 一样):名称、说明、访问级别、装饰器、父类取自原来的类(同上两条),TypeScript 的抽象、泛型参数、实现的接口不带;类属性、构造参数见「异常」。 - 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` 命令。 - Python 变量:`assignment` 的 left / type / right,说明是紧跟在后面的属性 docstring 的字符串节点(见上面代码块树规则);right 是 `dictionary` 时 entries 是它的具名子节点(注释除外):`pair` 的 key / value(value 又是 `dictionary` 时连它的 entries)、`dictionary_splat` 里的表达式。 ## 中文模板直译 @@ -1278,7 +1434,9 @@ export interface CodeReaderApi { | TypeScript | `ternary_expression`(`c ? a : b`) | 如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 | | Python | `conditional_expression`(`a if c else b`,没有字段名,具名子节点依次是 a、c、b) | 如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 | -写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 `iteration`(翻译时从 `SourceDocument.language` 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。跨几行写的调用也不走节点模板:由语言插件的 `call` 认出来,引擎只在翻一行译文(`translate`)、变量 / 常量的值(`declaredValue`)和实参表里的值时按「调用与构造」一节排(每段一个 `CallSlot`,见 `expandedText`、`emitCall`、`argumentTable`),属性表别的值、库规则借的 `expression` 照常一行。 +写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 `iteration`(翻译时从 `SourceDocument.language` 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。跨几行写的调用也不走节点模板:由语言插件的 `call` 认出来,引擎只在翻一行译文(`translate`)、变量 / 常量的值(`declaredValue`)和实参表里的值时按「调用与构造」一节排(每段一个 `TableSlot`,见 `expandedText`、`emitCall`、`argumentTable`),属性表别的值、库规则借的 `expression` 照常一行。 + +节点模板也排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`、`for f in glob`)由翻译插件的节点读法排(`ZhTable.readers`:节点类型 → 读法,读法拿到 `ReaderContext`:照常翻子节点的 `say`、名字照抄带下标的 `name`、读一条命令的 `command`,能给一张排在这一段下面的表,分情况的「情况 \| 做什么」就是它;返回 undefined 照常翻)。一条命令由语言插件的 `command` 认出、按命令名交给 `SourceDocument.commandRule`(见「命令规则」),引擎接上重定向;没有规则时命令名照抄、各个词照常翻。解释器开关的叫法在 `ZhTable.switches`(errexit → 出错就停)。 TypeScript 关键字表(流程 = keyword,声明 = storage): @@ -1394,13 +1552,14 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTab | number / literal | #b5cea8 | #098658 | | comment | #6a9955 | #008000 | | hint | `--vscode-descriptionForeground` | 同左 | +| constant | #4fc1ff | #0070c1 | | plain | `--vscode-editor-foreground` | 同左 | 属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认导出、全部、整个模块、匿名按 hint;输入参数表格的列名是界面文字(`descriptionForeground`),不是着色的词。推导式框里的「遍历」「的每个」「仅当」「取」「异步遍历」按 keyword,框头按「循环」块的种类色。父类名(TypeScript `extends` 后面、JavaScript `class_heritage` 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。 带自己颜色的段(`Segment.color`,库规则按库的配色给,例如 FastAPI 的 HTTP 方法、成功的状态码):暗色、亮色两个写在元素的 style 上(`--color-dark` / `--color-light`),`.cr-colored` 按主题挑一个,盖过词性的颜色;词性照旧报(`role`)。 -种类(种类图标的颜色 + 块底 7% + 边框 35%;亮色主题同色系;图标见「预览(代码块树)」):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;enum 枚举 #b180d7(亮色 #652d90);exception 异常 #f14c4c(亮色 #cd3131,和尝试、捕获、最终同一色系,更红一些);field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框);库规则的种类用规则给的颜色(`badge.color` 的 dark / light,webview 写在块的 style 上按主题挑),内置 SQLAlchemy 数据表 #e8a860 / #a3590d,Celery 应用 #98c379 / #4d7f17,定时任务 #56b6c2 / #127f8c,数据类 #ef7fb0 / #b3306c,FastAPI 接口 #61afef / #1a6fb5,FastAPI 路由器 #7aa2f7 / #34548a。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。 +种类(种类图标的颜色 + 块底 7% + 边框 35%;亮色主题同色系;图标见「预览(代码块树)」):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;enum 枚举 #b180d7(亮色 #652d90);exception 异常 #f14c4c(亮色 #cd3131,和尝试、捕获、最终同一色系,更红一些);field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框);Shell 的 interpreter 解释器同语句 #9d9d9d,options 选项 #e5c07b(亮色 #9a6a00),case 分情况同条件 #ce9178,command 命令、pipeline 管道 #d7ba7d(亮色 #7c5f12);库规则的种类用规则给的颜色(`badge.color` 的 dark / light,webview 写在块的 style 上按主题挑),内置 SQLAlchemy 数据表 #e8a860 / #a3590d,Celery 应用 #98c379 / #4d7f17,定时任务 #56b6c2 / #127f8c,数据类 #ef7fb0 / #b3306c,FastAPI 接口 #61afef / #1a6fb5,FastAPI 路由器 #7aa2f7 / #34548a。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。 ## 代码位置 @@ -1421,6 +1580,10 @@ vscode/src/codeReader/ rules/enum.ts 内置 enum 库规则(Python):枚举类认成枚举块,成员照 Python 的规则认 rules/dataclass.ts 内置 dataclasses 库规则(Python):数据类卡片,字段照 dataclasses 的规则认 rules/fastapi.ts 内置 FastAPI 库规则(Python):路由器卡片(接口表)、接口卡片(请求表、响应表),参数照 FastAPI 的规则放,异常类的状态码、类型别名、枚举经索引查 + commandRules.ts 命令规则的宿主部分:命令名 → 规则(先注册的算),规则出错记下、当它没读 + commands/reading.ts 命令规则共用的:认选项(连写拆开、带值的、长选项、--)、拼译文(动作、括号里的意思、照抄不认识的选项) + commands/shellCommands.ts 内置 shell 命令规则(rm、mkdir、curl、tar、unzip、find、cp、mv、echo / printf、exit、cd、pwd、basename、dirname、readlink、shasum、cut、du、cat、grep、sed、chmod、ln、touch、source、read、sleep、continue / break……) + localSymbols.ts 语言插件认的同一文件里的名字怎么用:盖住位置的名字、跳到哪处定义、各处出现(不碰 VS Code) ruleLookups.ts 库规则认领时查别的文件里的定义(RuleContext.lookup):索引 + 语言扩展查定义(不碰 VS Code) calleeKinds.ts 调用的名字定义成哪种块(SourceDocument.calleeKind):按被调用的表达式原文记、问最后那个名字(不碰 VS Code) deferredAnswers.ts 上面两样共用的:排视图数据时先交出已知的、记下问了什么,发出预览后按版本查一次、没答的隔一会儿再查,变了叫宿主重发(不碰 VS Code) @@ -1433,10 +1596,13 @@ vscode/src/codeReader/ languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__) languages/displayNames.ts 中文名的共用部分:第一小句、行尾记号、按优先级汇总 languages/pythonNames.ts Python 的中文名写法(Annotated、@Comment、行尾记号、docstring) + languages/bash.ts bash / sh / zsh(shellscript):代码块树、facts(解释器、选项、按位置的参数、输出、返回)、command(命令的结构、重定向)、symbols(同一文件里的函数、变量) + languages/bashNames.ts Shell 的中文名写法(行尾 # @Comment) languages/typescriptNames.ts TypeScript / JavaScript 的中文名写法(@Comment 装饰器、行尾记号、JSDoc) translators/zhTemplate.ts 模板直译引擎(按语言的表驱动,词性、遍历结构从 SourceDocument.language 取;推导式排进框见 iterationGroup) translators/typescriptZh.ts TS 表 translators/pythonZh.ts Python 表(含注解位置的判断) + translators/bashZh.ts Shell 表:节点读法(展开、特殊变量、条件、通配、&& / ||、管道、遍历、分情况)、开关的叫法 explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口;showChanges:审查工单时只摆 PR 改过的文件) explorer/copyText.ts 复制菜单的命令和复制出什么(纯函数) explorer/copyCommands.ts 复制命令:写剪贴板、状态栏提示 @@ -1502,6 +1668,10 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 跟着版本走:每个版本查一次、手上有挪过来的就不先交一半、查到一半文档又改了丢掉、跟着改动挪位置(前面的挪、碰到的丢、后面的不动)、没答上照间隔再查到次数为止。 - 页面(`webview-ui/.../displayNames.test.tsx`,渲染成 HTML):只收这个版本的、下标和长度都对上才换、用到的地方写中文名和「原名 x」悬停提示、下标照旧、定义处后面淡淡的原名、框里也换、单击时筛选栏写中文名且当前那一处按下标认。 - 真 VS Code(Pylance):routes.py 的条件读成「如果 当前用户.role 不等于 UserRole.管理员」(管理员来自 models.py),参数表「当前用户 current_user」,docstring 兜底的「要删的那一项的编号」,单击高亮、⌘↓ 跳、⌘ 点击「管理员」跳进 models.py 的预览(「管理员 ADMIN」,没有说明列),改文档时中文名一帧都没少。 +- Shell: + - 语言插件(`languages/bash.test.ts`,真实解析):`fixtures/fetchRuntime.sh` 的代码块树(种类、行号、层级,同原型 `SHELL_BLOCKS`,原型略去的行也在);elif / else 展平、while / for 的主体是子块、条件串和命令串;解释器(env、写死的路径)、选项(连写拆开、`-o`、`+`、`set --` 不是)、变量(local 一句几个、没值的、readonly 和顶层全大写写死的是常量)、函数(按位置的参数和名字、打印的几句、被 `$(…)` 拿去用、写了的 return);命令的词(写死的文字、有展开的没有)和重定向;同一文件里的名字(函数、赋值、local、for、read、展开,第几个参数不算);行尾 `# @Comment`。 + - 中文直译(`translators/bashZh.test.ts`,走 `readBlockViews`):`fetchRuntime.sh` 整份逐行同 `FETCH_READING`(即上面「Shell 脚本」的样例);展开(长度、去头去尾、截取、没设时、替换、特殊变量)、条件、重定向、命令规则(不认识的选项照抄、没有规则的照抄)、`&&` / `||`、遍历、`$(dirname "$0")`、常量;字符串色 / 变量色 / 常量色;规则出错照抄命令并报告。 + - 宿主:命令规则按语言、注册顺序给(`registry.test.ts`),shellscript 的 document 消息带命令规则读的译文、shell 的种类名、同一文件里的名字(`editor/documentMessage.test.ts`),跳到之前最近的定义、定义处算写入(`localSymbols.test.ts`)。 - 注册表:注册 / 注销后 `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`):卡片编辑时盖住它留下的子块(字段中间的方法连同主体),跳到被盖住的块改闪卡片;只编辑头部的块不盖住子块,一行写完的块盖住同一行的子块;点「编辑」时重叠的块退出编辑;任意点法下正在编辑、画出来的块两两不重叠。