From e1a5f4f0127b1d01b9fceda727eb1cf322329740 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 14 Oct 2026 15:16:37 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E4=B8=AD=E6=96=87=E7=9B=B4=E8=AF=91=E9=87=8C?= =?UTF-8?q?=20Python=20=E7=9A=84=20not=20x=E3=80=81TypeScript=20=E7=9A=84?= =?UTF-8?q?=20!x=20=E8=AF=BB=E6=88=90=E3=80=8Cx=20=E4=B8=8D=E6=88=90?= =?UTF-8?q?=E7=AB=8B=E3=80=8D=EF=BC=88=E5=A6=82=E6=9E=9C=20root=5Fdir.is?= =?UTF-8?q?=5Fdir()=20=E4=B8=8D=E6=88=90=E7=AB=8B=EF=BC=89=EF=BC=8CPython?= =?UTF-8?q?=20=E8=B5=B0=E8=8A=82=E7=82=B9=E6=A8=A1=E6=9D=BF=EF=BC=8CTypeSc?= =?UTF-8?q?ript=20=E7=94=A8=E8=8A=82=E7=82=B9=E8=AF=BB=E6=B3=95=E5=8F=AA?= =?UTF-8?q?=E6=8D=A2=20!=EF=BC=8C=E5=8E=BB=E6=8E=89=E3=80=8C=E9=9D=9E?= =?UTF-8?q?=E3=80=8D=E7=9A=84=E8=AF=91=E6=B3=95=E5=92=8C=E5=AE=83=E8=A1=A5?= =?UTF-8?q?=E7=A9=BA=E6=A0=BC=E7=9A=84=E7=89=B9=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/specs/code-reader/spec.md | 21 +++--- vscode/src/codeReader/blockView.test.ts | 2 +- vscode/src/codeReader/contract.ts | 2 +- vscode/src/codeReader/rules/react.test.ts | 14 ++-- vscode/src/codeReader/translators/pythonZh.ts | 3 +- .../codeReader/translators/typescriptZh.ts | 17 ++++- .../codeReader/translators/zhTemplate.test.ts | 66 ++++++++++++++++--- .../src/codeReader/translators/zhTemplate.ts | 7 +- vscode/webview-ui/src/codeReader/types.ts | 2 +- 9 files changed, 99 insertions(+), 35 deletions(-) diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 63c0180..30270f1 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -936,7 +936,7 @@ export interface CommentBlock extends TextBlock { kind: 'comment' } export type TokenRole = | 'keyword' // 流程关键字:import from export if else for return try catch await … | 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null … - | 'operator' // 被翻成文字的运算符:全等 并且 或者 非 + | 'operator' // 被翻成文字的运算符:全等 并且 或者 | 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 … | 'constant' // 按习惯全大写、当常量用的名字(shell 的 PY_VERSION、HOME) | 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain' @@ -1791,7 +1791,7 @@ react.component 23 依赖项: collapsed(useCallback) variable 48 名称: next - 初始值: 非 collapsed + 初始值: collapsed 不成立 statement 49: setCollapsed(next); try 50: 尝试 statement 51: localStorage.setItem(PLAN_CHECKLIST_COLLAPSED_KEY, 如果 next 则 "1" 否则 "0"); @@ -1805,7 +1805,7 @@ react.component 23 …(doneCount、counter、current、headline 四张变量卡片,headline 那张带「注释」一行) react.render 65 JSX 结构大纲: - Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE⟧ open=非 collapsed onOpenChange=toggle + Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE⟧ open=collapsed 不成立 onOpenChange=toggle CollapsibleTrigger ⟦样式 3 个类 · 取决于 FLUSH_ROW_INTERACTIVE⟧ data-testid="plan-checklist-toggle" div ⟦样式 4 个类⟧ data-testid="plan-progress" … @@ -2002,11 +2002,11 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 1. 关键字:只认语法树里的关键字节点(匿名节点),所以属性名 `i.number`、对象键不会被误翻。 2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。 -3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 并且、`||` 或者、`!` 非(Python 的 `not` / `and` / `or` 本身是关键字)。 +3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 并且、`||` 或者(Python 的 `and` / `or` 本身是关键字)。取反换语序读成「x 不成立」:Python 的 `not x` 走节点模板,TypeScript 的 `!x` 走节点读法(见下文)。 同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值、字典 / 对象各项的键和值都走这条路。 -其他规则:空数组字面量 → 空数组、空对象字面量 → 空对象(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释(文件开头的说明、挂在块上的注释)给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。 +其他规则:空数组字面量 → 空数组、空对象字面量 → 空对象(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释(文件开头的说明、挂在块上的注释)给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格。 关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`raise_statement` 指父节点是 raise_statement,`for_statement > else_clause` 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。 @@ -2016,10 +2016,11 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 | --- | --- | --- | | TypeScript | `ternary_expression`(`c ? a : b`) | 如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 | | Python | `conditional_expression`(`a if c else b`,没有字段名,具名子节点依次是 a、c、b) | 如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 | +| Python | `not_operator`(`not x`;`not in`、`is not` 是单独的关键字,不走这里) | 〈argument〉 不成立 | 写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 `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 → 出错就停),解析指令的叫法在 `ZhTable.directives`(syntax → 语法),并成一块的几条(`TextBlock.group`)按第一条的节点类型查 `ZhTable.groups` 读成一行字加一张表。阶段、健康检查、启动、分构建参数 / 环境变量的变量这些属性表块的行由引擎按 facts 排;要执行的命令(`CommandForm`)数组写法照常翻(交给命令规则),shell 写法问 `SourceDocument.embedded`,几步用「然后」(一步失败就停的)或分号连起来。 +节点模板也排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`、`for f in glob`,TypeScript 的 `unary_expression` 只有 `!x` 读成「〈argument〉 不成立」、`-x`、`typeof x` 照常翻)由翻译插件的节点读法排(`ZhTable.readers`:节点类型 → 读法,读法拿到 `ReaderContext`:照常翻子节点的 `say`、名字照抄带下标的 `name`、读一条命令的 `command`,能给一张排在这一段下面的表,分情况的「情况 \| 做什么」就是它;返回 undefined 照常翻)。一条命令由语言插件的 `command` 认出、按命令名交给 `SourceDocument.commandRule`(见「命令规则」),引擎接上重定向,规则给了选项表就排在这一段下面;没有规则时命令名照抄(按函数着色)、各个词照常翻。解释器开关的叫法在 `ZhTable.switches`(errexit → 出错就停),解析指令的叫法在 `ZhTable.directives`(syntax → 语法),并成一块的几条(`TextBlock.group`)按第一条的节点类型查 `ZhTable.groups` 读成一行字加一张表。阶段、健康检查、启动、分构建参数 / 环境变量的变量这些属性表块的行由引擎按 facts 排;要执行的命令(`CommandForm`)数组写法照常翻(交给命令规则),shell 写法问 `SourceDocument.embedded`,几步用「然后」(一步失败就停的)或分号连起来。 TypeScript 关键字表(流程 = keyword,声明 = storage): @@ -2029,7 +2030,7 @@ TypeScript 关键字表(流程 = keyword,声明 = storage): TypeScript 内置类型:string 字符串、number 数值、boolean 布尔、any 任意、unknown 未知、never 永不、void 无、object 对象、bigint 大整数、symbol 符号。`Promise` 等库类型不翻。 -Python 关键字:import 导入、from(导入)从 / (`raise … from`)源自 / (`yield from`)自、as(导入别名、`with … as`、`except … as`)作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else(`if` 后)否则 / (`for` / `while` 后,循环没被 break 才执行)未中断时 / (`try` 后,没出错才执行)未出错时、for 遍历、in(比较里)在 / (for 头部)取自(推导式里的 for / in 不逐词翻,整个重排,见「推导式」)、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、not 非、and 并且、or 或者、is 是、not in 不在、is not 不是、None 空、True 真、False 假、match 匹配、case 情况。 +Python 关键字:import 导入、from(导入)从 / (`raise … from`)源自 / (`yield from`)自、as(导入别名、`with … as`、`except … as`)作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else(`if` 后)否则 / (`for` / `while` 后,循环没被 break 才执行)未中断时 / (`try` 后,没出错才执行)未出错时、for 遍历、in(比较里)在 / (for 头部)取自(推导式里的 for / in 不逐词翻,整个重排,见「推导式」)、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、and 并且、or 或者、is 是、not in 不在、is not 不是、None 空、True 真、False 假、match 匹配、case 情况。 Python 内置类型(仅注解里):str 字符串、int 整数、float 浮点数、bool 布尔、list 列表、dict 字典、set 集合、tuple 元组、bytes 字节串。 @@ -2037,7 +2038,7 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮 | 源码 | 译文 | | --- | --- | -| `if (!folder)` | `如果 (非 folder)` | +| `if (!folder)` | `如果 (folder 不成立)` | | `return []` | `返回 空数组` | | `for (const f of files) {` | `遍历 (常量 f 取自 files)` | | `failed: { path: string, reason: string }[]` | `failed: { path: 字符串, reason: 字符串 }[]` | @@ -2060,7 +2061,7 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮 | `_CONTEST_STATUS_KEYS = {⏎ "published": "contests:publish", "ended": "contests:end", "draft": "contests:update" }` | 名称 _CONTEST_STATUS_KEYS / 访问级别 私有 / 初始值 字典 3 项:`"published" \| "contests:publish"`、`"ended" \| "contests:end"`、`"draft" \| "contests:update"` | | `CONFIG = {"db": {"host": host, "opts": {}}, **defaults, 1: await load()}` | 名称 CONFIG / 访问级别 公共 / 值 字典 3 项:`"db" \| 字典 2 项`(下一层 `"host" \| host`、`"opts" \| 空字典`)、`展开 \| defaults`、`1 \| 等待 load()` | | `const result: EnvLockResult = { total: files.length, ok: [], failed: [] }`(envLock.ts 40 行) | 名称 result / 类型 EnvLockResult / 初始值 对象 3 项:`total \| files.length`、`ok \| 空数组`、`failed \| 空数组` | -| `export const handlers = { total: files.length, 'x-y': await load(), [key]: !ok, files, ...rest, async load(id: string): Promise { … }, nested: { a: [], b: { c: 1 } } }` | 名称 handlers / 访问级别 公共 / 值 对象 7 项:`total \| files.length`、`'x-y' \| 等待 load()`、`[key] \| 非 ok`、`files \| files`、`展开 \| rest`、`load \| 异步 load(id: 字符串): Promise<无>`、`nested \| 对象 2 项`(下一层 `a \| 空数组`、`b \| 对象 1 项`,再下一层 `c \| 1`) | +| `export const handlers = { total: files.length, 'x-y': await load(), [key]: !ok, files, ...rest, async load(id: string): Promise { … }, nested: { a: [], b: { c: 1 } } }` | 名称 handlers / 访问级别 公共 / 值 对象 7 项:`total \| files.length`、`'x-y' \| 等待 load()`、`[key] \| ok 不成立`、`files \| files`、`展开 \| rest`、`load \| 异步 load(id: 字符串): Promise<无>`、`nested \| 对象 2 项`(下一层 `a \| 空数组`、`b \| 对象 1 项`,再下一层 `c \| 1`) | | `export abstract class Repository extends Base implements Loader, Disposable {`(classes.ts 3–9 行,连同前面的 JSDoc 和 `@Injectable()`) | 名称 Repository / 说明 `仓库基类。…` / 访问级别 公共 / 抽象 是 / 装饰器 `@Injectable()` / 类型参数 `K, T 限定为 Entity = Entity` / 父类 `Base<字符串>` / 实现的接口 `Loader, Disposable` | | `export default class {` | 名称 匿名 / 访问级别 公共 / 抽象 否 | | `class Meta(Base, metaclass=ABCMeta):`(classes.py,带 `@dataclass(frozen=True)`) | 名称 Meta / 访问级别 公共 / 装饰器 `@dataclass(frozen=True)` / 父类 `Base, metaclass=ABCMeta` | @@ -2301,7 +2302,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 调用与构造:语言插件读出的结构(Python 按位置 / 按名字 / `*x` / `**x`、唯一的生成器实参、被调用的不是名字时没有 name、装饰器和父类列表不算;TypeScript `new` 是新建、被调用的不含 new、`...x`、带标签的模板字符串和不带括号的 `new X` 不算);中文直译按「调用与构造」一节的样例:`routes.py` 的 `return UserDetailResponse(…)` 定义是类(异常也算)时「返回 新建 UserDetailResponse · 11 个参数」、11 行表、名字带下标,没查到 / 查不到 / 是函数时「调用」、名字按函数着色;`update_user` 的初始值「等待 调用 user_service.update_user · 10 个参数」、表是这一行的子行(列名 参数 / 值)、第一行「第 1 个」按说明词着色;一行写完的 `get_user(...)`、`file.read()` 照旧一行、不问定义;「展开」「展开关键字」、「第 N 个」只数按位置传的;`fixtures/calls.ts.txt`(本仓库的代码)new 是新建、不带 new 是调用、都不问定义;链式调用跨了几行、回调主体是子块的调用不排。嵌套:`paymentConsoleRoutes.py` 的 15 行表逐字一致,`credentials_present` 那一格套 4 行表、`CredentialPresence` 同样问定义、按类型着色、带下标,`bool(…)\n and Path(…).is_file()` 照旧一行;自造的三层(Outer → Middle → Leaf、带 await 的 fetch)一层层套、每层都问定义;`chainedCalls.py` 的 `(调用 base64.b64encode · 1 个参数).decode()`、被调用的表达式里的调用照常一行(圆括号贴着内容),`[0]`、`or` 后面跟着时括起来、前面有东西或后面是逗号时不括,实参值里的 `X(\n…\n).model_dump()` 也括起来、表套在格子里;`nestedCalls.ts.txt` 三个 `commands.registerCommand(…)` 各套一张表,`new Server(new Options(…))` 套 new。`calleeKinds.test.ts`:按表达式原文记、同一版本不再查、被调用的不是名字不问、查了没有不算变、没答的隔一会儿只再查没答的。视图数据带 `table`、`readDefinitionKind` 认类 / 函数 / 异常 / 变量、`renderedNames` 算上表里(连套着的表)的名字、`documentMessage` 把 calleeKind 交给翻译插件;webview 跳到定义、键盘跳到高亮的各处出现都认一行译文下面那张表和值那一格里套着的表(套在变量初始值那一行下面的展开那一行)。 - 只经契约:只照 contract 写的假语言插件(`iteration` 把一个普通调用认成遍历)配中文模板,那个调用换成遍历框;同一个翻译插件配上不给 `iteration` 的语言插件照常翻。测试文件不 import 任何语言插件模块。 - 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取,列宽模板(`tableTemplate`)只看列数。 -- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。 +- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;取反读成「x 不成立」(Python 的 `not` 在 if 条件、和 `and` 连着、括号里、条件表达式里,`not in`、`is not` 照旧;TypeScript 的 `!a`、`!(a && b)`,`-x`、`typeof x`、非空断言照旧);属性表里的值和一行译文用同一套表和模板。 - `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`):「注释」一行照属性表画,一行译文的卡片排在译文下面,有没有「注释」一行按钮都只有一个、排在右上角那排最前面,没有注释时叫「按注释规范写注释」,不能写注释的块没有按钮。 diff --git a/vscode/src/codeReader/blockView.test.ts b/vscode/src/codeReader/blockView.test.ts index e996169..85b2c0e 100644 --- a/vscode/src/codeReader/blockView.test.ts +++ b/vscode/src/codeReader/blockView.test.ts @@ -349,7 +349,7 @@ describe('blockView', () => { ' x | 整数 | ', '输出参数: 布尔', ]) - expect(translationText(list[0].children[0])).toBe('返回 非 x') + expect(translationText(list[0].children[0])).toBe('返回 x 不成立') }) it('跨几行写的调用:一行译文下面带摊开实参的表;calleeKind 交给翻译插件分新建还是调用', async () => { diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index a6d2e77..d3c8a7e 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -600,7 +600,7 @@ export interface CommentBlock extends TextBlock { kind: 'comment' } export type TokenRole = | 'keyword' // 流程关键字:import from export if else for return try catch await … | 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null … - | 'operator' // 被翻成文字的运算符:全等 并且 或者 非 + | 'operator' // 被翻成文字的运算符:全等 并且 或者 | 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 … | 'constant' // 按习惯全大写、当常量用的名字(shell 的 PY_VERSION、HOME) | 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain' diff --git a/vscode/src/codeReader/rules/react.test.ts b/vscode/src/codeReader/rules/react.test.ts index fd1fbbc..e44a32d 100644 --- a/vscode/src/codeReader/rules/react.test.ts +++ b/vscode/src/codeReader/rules/react.test.ts @@ -305,7 +305,7 @@ describe('react 库规则:组件体一块一张卡片(luffy 的 PlanChecklis ' 依赖项: collapsed(useCallback)', ' variable 48', ' 名称: next', - ' 初始值: 非 collapsed', + ' 初始值: collapsed 不成立', ' statement 49: setCollapsed(next);', ' try 50: 尝试', ' statement 51: localStorage.setItem(PLAN_CHECKLIST_COLLAPSED_KEY, 如果 next 则 "1" 否则 "0");', @@ -323,7 +323,7 @@ describe('react 库规则:组件体一块一张卡片(luffy 的 PlanChecklis expect(render.badge).toEqual({ label: '渲染输出', color: { dark: '#89d185', light: '#388a34' }, icon: 'layout-template' }) expect(lines(render).slice(0, 4)).toEqual([ 'JSX 结构大纲: ', - ' Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE|⟧ open=非 collapsed onOpenChange=toggle', + ' Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE|⟧ open=collapsed 不成立 onOpenChange=toggle', ' CollapsibleTrigger ⟦样式 3 个类 · 取决于 FLUSH_ROW_INTERACTIVE|⟧ data-testid="plan-checklist-toggle"', ' div ⟦样式 4 个类|⟧ data-testid="plan-progress"', ]) @@ -351,7 +351,7 @@ describe('react 库规则:luffy 的 QueuedBubble(原型「编辑器全貌」 ' variable 176', ' 名称: el', ' 初始值: textareaRef.current', - ' if 177: 如果 (非 el)', + ' if 177: 如果 (el 不成立)', ' return 177: 返回;', ' statement 178: el.focus();', ' statement 179: el.setSelectionRange(el.value.length, el.value.length);', @@ -381,7 +381,7 @@ describe('react 库规则:luffy 的 QueuedBubble(原型「编辑器全貌」 ' textarea ⟦样式 6 个类 · 取决于 FIELD_TEXT|⟧ ref=textareaRef value=draft onChange=(e) => setDraft(e.target.value) aria-label="排队消息内容"', ' div ⟦样式 3 个类|⟧', ' Button variant="ghost" size="sm" onClick=onCancel "取消"', - ' Button size="sm" disabled=非 canSave onClick=save "保存"', + ' Button size="sm" disabled=canSave 不成立 onClick=save "保存"', ]) }) @@ -600,10 +600,10 @@ describe('react 库规则:渲染大纲', () => { it('luffy 的 DesktopSettingsDialog:先 return null 是如果卡片,最后返回的 JSX 是渲染输出;AppVaultSummary 的片段写「片段,不产生元素」', async () => { const dialog = await card(SETTINGS, 'DesktopSettingsDialog') - expect(viewLines([childAt(dialog, 180)])).toEqual(['if 180: 如果 (非 section)', ' return 180: 返回 空;']) + expect(viewLines([childAt(dialog, 180)])).toEqual(['if 180: 如果 (section 不成立)', ' return 180: 返回 空;']) expect(lines(childAt(dialog, 181)).slice(0, 2)).toEqual([ 'JSX 结构大纲: ', - ' Dialog open onOpenChange=(next) => 非 next 并且 closeSettings()', + ' Dialog open onOpenChange=(next) => next 不成立 并且 closeSettings()', ]) expect(lines(await render(readFixture('appVaultSummary.tsx.txt'), 'AppVaultSummary')).slice(0, 7)).toEqual([ 'JSX 结构大纲: ', @@ -649,7 +649,7 @@ describe('react 库规则:渲染大纲', () => { it('luffy 的 DeleteAppDialog 的删除按钮:文字的三元写成是 / 否两支', async () => { const text = lines(await render(readFixture('deleteAppDialog.tsx.txt'), 'DeleteAppDialog')) - const button = text.indexOf(' Button variant="destructive" onClick=() => remove.mutate() disabled=非 ready 或者 pending') + const button = text.indexOf(' Button variant="destructive" onClick=() => remove.mutate() disabled=ready 不成立 或者 pending') expect(text.slice(button + 1, button + 4)).toEqual([ ' ⟦|如果 pending⟧', ' ⟦|是⟧ "删除中…"', diff --git a/vscode/src/codeReader/translators/pythonZh.ts b/vscode/src/codeReader/translators/pythonZh.ts index 1ee3539..af2e0b3 100644 --- a/vscode/src/codeReader/translators/pythonZh.ts +++ b/vscode/src/codeReader/translators/pythonZh.ts @@ -64,7 +64,6 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({ 'nonlocal': '非局部', 'del': '删除', 'assert': '断言', - 'not': '非', 'and': '并且', 'or': '或者', 'is': '是', @@ -95,5 +94,7 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({ // `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名 nodeTemplates: { conditional_expression: ['如果', { namedChild: 1 }, '则', { namedChild: 0 }, '否则', { namedChild: 2 }], + // `not x` 读成「x 不成立」,照中文语序把条件放在前面;`not in`、`is not` 是单独的词,不走这里 + not_operator: [{ field: 'argument' }, '不成立'], }, }) diff --git a/vscode/src/codeReader/translators/typescriptZh.ts b/vscode/src/codeReader/translators/typescriptZh.ts index 08895bf..696653a 100644 --- a/vscode/src/codeReader/translators/typescriptZh.ts +++ b/vscode/src/codeReader/translators/typescriptZh.ts @@ -1,6 +1,18 @@ import type { Node, Translator } from '../contract' +import type { NodeReader } from './zhTemplate' import { createZhTemplateTranslator } from './zhTemplate' +/** + * `!x` 读成「x 不成立」,照中文语序把条件放在前面;别的一元运算(`-x`、`typeof x`)照常翻。 + * 和节点模板一样,节点没整个落在要翻的这一段里也照常翻。 + */ +const unaryExpression: NodeReader = (node, context) => { + const argument = node.childForFieldName('argument') + if (node.childForFieldName('operator')?.type !== '!' || !argument || !context.whole(node)) + return undefined + return [...context.say(argument), { text: ' ', role: 'plain' }, { text: '不成立', role: 'keyword' }] +} + function commentText(_node: Node, text: string): string { if (text.startsWith('//')) return text.replace(/^\/{2,}\s?/, '') @@ -80,8 +92,8 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({ symbol: '符号', }, isTypePosition: leaf => leaf.parent?.type === 'predefined_type', - operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者', '!': '非' }, - operatorParents: ['binary_expression', 'unary_expression'], + operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者' }, + operatorParents: ['binary_expression'], emptyLiterals: { array: '空数组', object: '空对象' }, collections: { object: '对象', array: '数组' }, stringNodes: ['string', 'template_string', 'regex'], @@ -89,4 +101,5 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({ nodeTemplates: { ternary_expression: ['如果', { field: 'condition' }, '则', { field: 'consequence' }, '否则', { field: 'alternative' }], }, + readers: { unary_expression: unaryExpression }, }) diff --git a/vscode/src/codeReader/translators/zhTemplate.test.ts b/vscode/src/codeReader/translators/zhTemplate.test.ts index a9082ca..b614c7b 100644 --- a/vscode/src/codeReader/translators/zhTemplate.test.ts +++ b/vscode/src/codeReader/translators/zhTemplate.test.ts @@ -112,7 +112,7 @@ describe('typeScript 中文直译', () => { it('spec 样例逐字一致', async () => { const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')) const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')) - expect(text(env.get(25)?.segments)).toBe('如果 (非 folder)') + expect(text(env.get(25)?.segments)).toBe('如果 (folder 不成立)') expect(text(env.get(26)?.segments)).toBe('返回 空数组') expect(text(env.get(41)?.segments)).toBe('遍历 (常量 f 取自 files)') expect(text(env.get(35)?.segments)).toBe('failed: { path: 字符串, reason: 字符串 }[]') @@ -138,7 +138,7 @@ describe('typeScript 中文直译', () => { '`.env` files are only picked up the next time the user clicks the button.', ].join('\n')) expect(issue[5]).toBe('prerequisiteNumber?: 数值') - expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)') + expect(issue[15]).toBe('如果 (issue 不成立 或者 issue.prerequisite 全等 未定义)') }) it('每个被翻的词单独成段,着色取语法树词性', async () => { @@ -165,7 +165,7 @@ describe('typeScript 中文直译', () => { ].join('\n') const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source)) expect(map[1]).toBe('o = { if: 1, return: i.number, string: x!.type, new: a.default }') - expect(map[2]).toBe('n = a 不全等 b 并且 非 c 或者 d 等于 e 不等于 f') + expect(map[2]).toBe('n = a 不全等 b 并且 c 不成立 或者 d 等于 e 不等于 f') expect(map[3]).toBe('类型 S = 字符串 | 无') // eslint-disable-next-line no-template-curly-in-string -- 模板字符串原样照抄进译文 expect(map[4]).toBe('s = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'') @@ -228,7 +228,7 @@ describe('typeScript 中文直译', () => { 1: '如果 err 属于 Error 则 err.message 否则 String(err)', 2: 'z = 如果 x 则 (如果 y 则 1 否则 2) 否则 3', 3: 'w = 如果 a 则 b 否则 如果 c 则 d 否则 e', - 4: 'v = 如果 a 不全等 b 并且 非 c 则 类型为 d 否则 等待 e', + 4: 'v = 如果 a 不全等 b 并且 c 不成立 则 类型为 d 否则 等待 e', }) expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword')).toEqual([ { text: '如果', role: 'keyword' }, @@ -237,6 +237,28 @@ describe('typeScript 中文直译', () => { ]) expectWordsApart(map) }) + + it('取反 `!x` 读成「x 不成立」;别的一元运算、非空断言、不等号照旧', async () => { + const source = [ + 'if (!a) {}', + 'if (!(a && b)) {}', + 'x = -a + typeof b', + 'y = c!.d !== e', + ].join('\n') + const map = await translateAll(ts, 'typescript', typescriptZhTranslator, source) + expect(texts(map)).toEqual({ + 1: '如果 (a 不成立)', + 2: '如果 ((a 并且 b) 不成立)', + 3: 'x = -a + 类型为 b', + 4: 'y = c!.d 不全等 e', + }) + expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role !== 'plain')).toEqual([ + { text: '如果', role: 'keyword' }, + { text: 'a', role: 'variable', offset: source.indexOf('a') }, + { text: '不成立', role: 'keyword' }, + ]) + expectWordsApart(map) + }) }) describe('python 中文直译', () => { @@ -245,7 +267,7 @@ describe('python 中文直译', () => { expect(texts(map)).toEqual({ 1: 'Scan a workspace for .env files.', 12: '跳过备份文件', - 14: '如果 非 found', + 14: '如果 found 不成立', 15: '返回 空列表', 16: '否则如果 len(found) > MAX_DEPTH', 18: '否则', @@ -341,7 +363,7 @@ describe('python 中文直译', () => { 1: '如果 _role 等于 "beat" 则 5680 否则 5679', 2: '如果 x 则 a 否则 如果 y 则 b 否则 c', 3: '如果 y 则 (如果 x 则 a 否则 b) 否则 c', - 4: '如果 非 done 并且 ok 则 ⟦列表|遍历 items 的每个 k,仅当 k 不在 seen,取 k⟧ 否则 空', + 4: '如果 done 不成立 并且 ok 则 ⟦列表|遍历 items 的每个 k,仅当 k 不在 seen,取 k⟧ 否则 空', }) expect(flatSegments(valueOf(map.get(1), '初始值')).filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([ { text: '如果', role: 'keyword' }, @@ -350,6 +372,34 @@ describe('python 中文直译', () => { { text: '否则', role: 'keyword' }, ]) }) + + it('`not x` 读成「x 不成立」:if 的条件、和 and 连着、括号里的、条件表达式里的;`not in`、`is not` 照旧', async () => { + const source = [ + 'def f(a, b, c, root_dir):', + ' if not root_dir.is_dir():', + ' return 1 if not b else 2', + ' if not a and b:', + ' pass', + ' if not (a or b):', + ' pass', + ' if a not in b or a is not c:', + ' pass', + ].join('\n') + const map = await translateAll(py, 'python', pythonZhTranslator, source) + expect([2, 3, 4, 6, 8].map(line => text(map.get(line)?.segments))).toEqual([ + '如果 root_dir.is_dir() 不成立', + '返回 如果 b 不成立 则 1 否则 2', + '如果 a 不成立 并且 b', + '如果 (a 或者 b) 不成立', + '如果 a 不在 b 或者 a 不是 c', + ]) + expect(flatSegments(inlineOf(map.get(4)?.segments ?? [])).filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([ + { text: '如果', role: 'keyword' }, + { text: '不成立', role: 'keyword' }, + { text: '并且', role: 'operator' }, + ]) + expect(linked(map.get(2)?.segments)).toEqual([['root_dir', source.indexOf('root_dir.')], ['is_dir', source.indexOf('is_dir')]]) + }) }) describe('python 写在表达式里的遍历(推导式)', () => { @@ -835,7 +885,7 @@ describe('属性表', () => { '值: 对象 7 项', ' total | files.length', ' \'x-y\' | 等待 load()', - ' [key] | 非 ok', + ' [key] | ok 不成立', ' files | files', ' 展开 | rest', ' load | 异步 load(id: 字符串): Promise<无>', @@ -897,7 +947,7 @@ describe('源码下标(跳到定义用)', () => { const at = (word: string, from = 0): number => source.indexOf(word, from) const map = await translateAll(py, 'python', pythonZhTranslator, source) const ifLine = at('if') - expect(text(map.get(2)?.segments)).toBe('如果 非 user.active 并且 user 不是 空') + expect(text(map.get(2)?.segments)).toBe('如果 user.active 不成立 并且 user 不是 空') expect(linked(map.get(2)?.segments)).toEqual([['user', at('user', ifLine)], ['active', at('active')], ['user', at('user is')]]) expect(linked(map.get(3)?.segments)).toEqual([['HTTPException', at('HTTPException')], ['status_code', at('status_code')]]) }) diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index b86fd22..e073bc0 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -38,7 +38,7 @@ export interface ZhTable { /** 内置类型:只在 isTypePosition 成立的叶子上按原文查。 */ builtinTypes: Record isTypePosition: (leaf: Node) => boolean - /** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */ + /** 翻成文字的运算符:只认父节点在 `operatorParents` 里的运算符节点,同一个符号写在别处的照抄。 */ operators: Record operatorParents: string[] /** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */ @@ -60,7 +60,7 @@ export interface ZhTable { */ nodeTemplates: Record /** - * 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`)。 + * 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`,TypeScript 一元运算里只有 `!x` 换语序)。 * 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。 */ readers?: Record @@ -903,8 +903,7 @@ function joinTokens(tokens: Token[]): Inline[] { segments.push(token.group) else pushSegment(segments, token.text, token.role, token.offset) - // 「非」是个词,后面补一个空格,不和操作数粘在一起 - gap = token.translated && token.raw === '!' + gap = false }) return segments } diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 9feef42..4e68076 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -13,7 +13,7 @@ export type BlockKind export type TokenRole = | 'keyword' // 流程关键字:import from export if else for return try catch await … | 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null … - | 'operator' // 被翻成文字的运算符:全等 并且 或者 非 + | 'operator' // 被翻成文字的运算符:全等 并且 或者 | 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 … | 'constant' // 按习惯全大写、当常量用的名字 | 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'