From 5e994cd90e31eed05ba714d8ec4e390eb80d06f2 Mon Sep 17 00:00:00 2001 From: cruldra Date: Mon, 5 Oct 2026 05:48:07 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(vscode):=20?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E9=87=8C=E7=9A=84=E5=AD=90=E5=9D=97=E5=8F=AA?= =?UTF-8?q?=E7=95=99=E6=8C=82=E5=9C=A8=E5=AD=90=E8=A1=8C=E4=B8=8A=E3=80=81?= =?UTF-8?q?=E7=82=B9=E4=BA=86=E6=89=8D=E5=B1=95=E5=BC=80=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E7=A7=8D=EF=BC=8C=E6=B5=8B=E8=AF=95=E5=81=87=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=8D=A2=E6=8E=89=E6=97=A7=20React=20=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E7=9A=84=E5=90=8D=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/specs/code-reader/spec.md | 26 +++++----- vscode/src/codeReader/blockView.ts | 10 ++-- vscode/src/codeReader/contract.ts | 10 ++-- vscode/src/codeReader/libraryRules.test.ts | 27 +++++----- vscode/src/codeReader/libraryRules.ts | 10 ++-- vscode/src/codeReader/rules/rustImpl.ts | 2 +- .../src/codeReader/BlockTree.test.tsx | 50 +++++++++---------- .../webview-ui/src/codeReader/BlockTree.tsx | 12 ++--- .../src/codeReader/PropertyGrid.tsx | 18 +++---- .../webview-ui/src/codeReader/codeReader.css | 2 +- .../src/codeReader/diffView.test.ts | 24 ++++----- vscode/webview-ui/src/codeReader/diffView.ts | 17 ++----- .../webview-ui/src/codeReader/reveal.test.ts | 14 +++--- vscode/webview-ui/src/codeReader/types.ts | 8 +-- .../src/codeReader/viewState.test.ts | 13 +++-- vscode/webview-ui/src/codeReader/viewState.ts | 8 +-- 16 files changed, 121 insertions(+), 130 deletions(-) diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index cc9a819..4e9b372 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -1010,14 +1010,14 @@ export interface RuleCard { defines?: SourceRange // 块自己的名字(类名):跳到的定义是它时闪整块 attributes?: BlockAttributes // 规则自己的筛选属性,叠在宿主从原块推出的上面;kind 填了不算;名字由 LibraryRule.attributeNames 给 absorbs: CodeBlock[] // 并进来的子块(原块 children 里的对象本身),不再单独成块;没并的照源码顺序留作子块,编辑范围同别的块,是原块整条语句 - slots?: RuleSlot[] // 画在卡片某一行下面的子块,一块只能放一处 + slots?: RuleSlot[] // 挂在卡片某一行的子行上、点子行才展开的子块,一块只能放一处 } /** - * 卡片里的一处子块:block(没并进来的子块)不排在卡片后面,画在第 row 行下面当子卡片,照常转成块(翻译、库规则、编辑、问 AI、差异都照常)。 - * item 是这一行的子行里讲它的那一个:这时它默认收着,点那个子行的第一格才展开、一次只展开一个(React 组件的事件处理表点名称)。 + * 卡片里的一处子块:block(没并进来的子块)不排在卡片后面,挂在第 row 行的第 item 个子行上,照常转成块(翻译、库规则、编辑、问 AI、差异都照常)。 + * 它默认收着,点那个子行的第一格才在这一行下面展开当子卡片、同一行一次只展开一个(Rust 实现块的方法表点方法名)。 */ -export interface RuleSlot { block: CodeBlock, row: number, item?: number } +export interface RuleSlot { block: CodeBlock, row: number, item: number } /** 把这块认成契约里现成的一种属性表块(enum 库的类 → 枚举块):宿主当语言插件报的这种块处理,翻译插件排属性表;头部是原块整条语句。 */ export interface RuleFacts { @@ -1133,8 +1133,8 @@ export interface CommandRule { - 略去(`RuleOmit`,`{ omit: true }`):这块讲在了别的卡片里(React 文件开头的 `"use client"` 讲成组件卡片的「客户端组件」标签,组件上面没并进块头部的 JSDoc 讲成「说明」),预览不画它、不占块的位置(后面的块 id 接着往下数)。它不能有子块,有子块时宿主当替身不合约定。 - 认成现成的属性表块(`RuleFacts`):`as` 是种类和 facts(`BlockFacts`,例如 `{ kind: 'enum', facts: EnumFacts }`),`absorbs` 同卡片。宿主把原块换成这种块(节点不变,头部是原块整条语句,子块是留下的),此后和语言插件报的这种块完全一样:翻译插件的 `describe` 排属性表、种类名取翻译插件的、颜色取这个种类的、筛选属性从 facts 推、块自己的名字从 facts 取。规则不给用词,也不给标签和颜色。只要检查并进来的是不是这块的子块。 - 规则自己的卡片(`RuleCard`),下面几条都说的是它。 -- 卡片必须给:`kind`(写成「规则 id.名字」,宿主核对前缀;筛选记住的就是它,要稳定)、`badge`(种类的名字、暗 / 亮两种颜色和 lucide 图标名)、`rows`(用现成的 `PropertyRow`:属性名 + 着色的值或 Markdown,表格就是带 `columns` 的子行)、`absorbs`(并进来的子块)。可以给:`defines`(块自己的名字,跳到定义是它时闪整块)、`attributes`(自己的筛选属性)、`concepts`(额外认出的概念,排在种类图标后面)、`slots`(放进卡片某一行下面的子块,见下);表格子行填 `PropertyItem.defines`(字段名),跳到这个定义时只闪那一行。 -- 放进卡片里的子块(`RuleSlot`):留下的子块(没并进来的)可以不排在卡片后面,画在卡片第 `row` 行下面当子卡片(React 组件的副作用画在「副作用」那一行下面);它照常转成块,照样问各规则(副作用那句也被 React 规则认成自己的卡片),编辑、问 AI、差异、筛选都照常。给了 `item` 的是那一行的子行里讲它的那一个:默认收着,点那个子行的第一格才展开、同一行里别的收起(事件处理表点名称)。一块只能放一处,不能同时并进来;`row` 要有这一行、`item` 要有这个子行。 +- 卡片必须给:`kind`(写成「规则 id.名字」,宿主核对前缀;筛选记住的就是它,要稳定)、`badge`(种类的名字、暗 / 亮两种颜色和 lucide 图标名)、`rows`(用现成的 `PropertyRow`:属性名 + 着色的值或 Markdown,表格就是带 `columns` 的子行)、`absorbs`(并进来的子块)。可以给:`defines`(块自己的名字,跳到定义是它时闪整块)、`attributes`(自己的筛选属性)、`concepts`(额外认出的概念,排在种类图标后面)、`slots`(挂在卡片某一行的子行上、点子行才展开的子块,见下);表格子行填 `PropertyItem.defines`(字段名),跳到这个定义时只闪那一行。 +- 挂在卡片子行上的子块(`RuleSlot`):留下的子块(没并进来的)可以不排在卡片后面,挂在卡片第 `row` 行的第 `item` 个子行上(讲它的那一个子行);它默认收着,点那个子行的第一格才在这一行下面展开当子卡片、同一行里别的收起(Rust 实现块的方法表点方法名)。它照常转成块,照样问各规则,编辑、问 AI、差异、筛选都照常。一块只能放一处,不能同时并进来;`row` 要有这一行、`item` 要有这个子行。只有这一种放法:子块要么挂在讲它的子行上,要么照常排在卡片后面。 - 只经契约:规则拿到的是块(kind、node、header、children、facts)和 `RuleContext`;源码里的名字照抄时自己带下标(`Segment.offset`),表达式、类型交给 `context.translate`(当前翻译插件的 `expression`,名字带下标,⌘ + 点击、高亮各处出现照常),说明文字交给 `context.doc`(当前翻译插件的 `doc`,和别的「说明」一样按 Markdown 显示),别的文件里定义的名字交给 `context.lookup`(见「规则认领时查别的文件里的定义」);库自己的配色(HTTP 方法)写在段的 `color` 上。规则可以读自己那门语言的语法树(规则声明了 `languageIds`),但不 import 语言插件、翻译插件和宿主模块(`rules/contractOnly.test.ts` 检查 `rules/` 下非测试文件只 import `../contract` 和同目录的文件)。 - 用词归规则:属性名、表头、种类名、类型译名都在规则自己的词表里,按 `context.displayLanguage`(翻译插件的 `displayLanguage`)挑,没有这种语言的词表就用规则默认的那份;加一种显示语言就是给规则加一份词表。webview 只画结构,不认识「数据表」这些字。 @@ -1142,9 +1142,9 @@ export interface CommandRule { - 启用:宿主收集这份源码里各层导入块的 `ImportFacts.sources`(去掉引号),有一个等于规则的某个库、或以「库.」「库/」「库::」开头(子模块;Rust 的路径用 `::`,`use axum::routing::get` 的来源是 `axum::routing`),规则才对这份源码启用。规则不用自己读导入语句。`libraries` 是空数组的规则不看导入,`languageIds` 里这几种语言的源码都启用(React:新的 JSX 写法不用导入 react。Pydantic:子类文件常常只导入父模型、不再导入 pydantic,见「内置:Pydantic 数据模型」)。 - 认领:宿主从顶层往下每块问一遍启用的规则,按注册顺序,第一个认领的算(先注册的优先);被认领的块留下的子块照样问。 -- 出错:规则抛错、或替身不合约定(种类 id 没带规则 id 前缀、并进来的不是这块的子块、属性表里的引用标的不是自己 / 规则没给 `resolve` / 引用的段没带下标、放进卡片里的块不是留下的子块 / 放了几处 / 没有那一行或子行、要略去的块还有子块)时,宿主记一条警告日志(输出面板「Superpowers」),当它没认、接着问下一个规则;都没认这块照常显示。预览不会因为规则出错而失败。 +- 出错:规则抛错、或替身不合约定(种类 id 没带规则 id 前缀、并进来的不是这块的子块、属性表里的引用标的不是自己 / 规则没给 `resolve` / 引用的段没带下标、挂在子行上的块不是留下的子块 / 放了几处 / 没有那一行或子行、要略去的块还有子块)时,宿主记一条警告日志(输出面板「Superpowers」),当它没认、接着问下一个规则;都没认这块照常显示。预览不会因为规则出错而失败。 - 并进来的子块:不再单独成块,由规则挑(`absorbs`),宿主不替规则多并。没并进来的子块(方法、规则不讲的注释)不管写在哪(字段中间也算),都照源码顺序是替身的子块,预览里照常看得见。替身的编辑范围同别的块,是原块整条语句(从头部开始,到原块连同各子块的最后一行,类就是整个类),盖住留下的子块的行:替身编辑时预览不画这些子块,同一行不会同时在两处能改(见「预览(代码块树)」)。内置规则并成员时连同紧挨在它前面的注释块一起并(讲的是这个成员),讲方法的注释、最后面的注释照常留下。 -- 卡片在预览里:左边的图标是语言的概念(原来的类、函数……)再加 `badge` 这一个、以及 `concepts`(同一个 id 只留第一个),悬停各自的名字;卡片边框色取 `badge.color`(暗色、亮色主题各取一个),内容是 `rows`,放进卡片的子块画在它那一行下面(这一行有折叠箭头,收起时它们跟着收起;画成一层块,同块树),不算卡片的子块(卡片的折叠箭头只管后面留下的子块);筛选的 `kind` 是替身的种类 id,其余属性从原块推出(原来的类是公共的,数据表也是公共的),叠上规则给的(规则自己的属性叫 `LibraryRule.attributeNames` 给的名字,规则起名时抛错记日志、照写 id);筛选栏里这个种类叫 `badge.label`;和类块一样默认收起子块。多了图标不改筛选、不改边框色。 +- 卡片在预览里:左边的图标是语言的概念(原来的类、函数……)再加 `badge` 这一个、以及 `concepts`(同一个 id 只留第一个),悬停各自的名字;卡片边框色取 `badge.color`(暗色、亮色主题各取一个),内容是 `rows`,挂在子行上的子块展开时画在它那一行下面(收起这一行时它们跟着收起;画成一层块,同块树),不算卡片的子块(卡片的折叠箭头只管后面留下的子块);筛选的 `kind` 是替身的种类 id,其余属性从原块推出(原来的类是公共的,数据表也是公共的),叠上规则给的(规则自己的属性叫 `LibraryRule.attributeNames` 给的名字,规则起名时抛错记日志、照写 id);筛选栏里这个种类叫 `badge.label`;和类块一样默认收起子块。多了图标不改筛选、不改边框色。 ### 规则自己查定义 @@ -1805,7 +1805,7 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 - `enum` → enum。变体表「变体 | 携带的数据」:没带数据是「—」,`Failed(String)` 写类型,`Running { pid: u32 }` 写字段原文。 - `fn` → function。有才显示:说明、异步(只有 `async fn` 写「是」)、可见性、输入参数表「名称 | 类型」、返回(没写是 `()`)。函数体里直接写的 `match` 收成「分情况」表「分支 | 做什么」,N 支:优先用这一支里的注释,否则是 `return` 那句或这一支的第一句,不展开成子块。 - `trait` → trait(图标 `network`,颜色同声明关键字的蓝)。方法表「名称 | 参数 | 返回」,只列签名,不展开函数体。 -- `impl` → impl(图标 `square-function`,颜色同函数)。标题是「某类型 的方法」(`impl Trait for Type` 取 Type)。方法是子块。不看导入的规则 `rules/rustImpl.ts`(`libraries: []`,种类 `rust.impl`)把每张方法卡放进方法表那一行,点方法名展开,和 React 事件处理同一条路。方法卡的参数表叫「参数」。身体里的 `const`、`static` 不收成语句,跟文件顶层同一条路(见下)。 +- `impl` → impl(图标 `square-function`,颜色同函数)。标题是「某类型 的方法」(`impl Trait for Type` 取 Type)。方法是子块。不看导入的规则 `rules/rustImpl.ts`(`libraries: []`,种类 `rust.impl`)把每张方法卡挂在方法表讲它的那个子行上(`RuleSlot`),点方法名展开。方法卡的参数表叫「参数」。身体里的 `const`、`static` 不收成语句,跟文件顶层同一条路(见下)。 - `mod 名字 { … }`(`mod_item` 有 `body`)→ module(中文「模块」,图标 `layers`,颜色同阶段)。卡片行:名称、说明(有文档注释才有)、可见性(没写也是本模块,跟结构体一样总有这一行)。身体用和文件顶层同一套规则拆成子块,排在这张卡片下面,不另做一张表把子块塞进某一行。模块自己的文档注释留在模块卡片上,身体里每项自己的文档注释留在那一项上。`mod 名字;` 没有身体,不拆,仍是一条 statement。 - `const`(`const_item`)→ const,`static`(`static_item`)→ variable。属性表不另写一套译文,走中文模板里常量 / 变量那一套:名称、说明、访问级别(公开才有,模板里叫「公共」)、类型、值(`static` 的值叫「初始值」)。文件顶层、模块身体、impl 身体同一条路。 - `extern "…" { … }`(`foreign_mod_item`)不单独成一块源码。里面的函数签名(`function_signature_item`,没有函数体)是函数卡片,跟普通函数一样有名称、说明、可见性、参数、返回,作为所在这一层(模块、文件顶层或 impl)的子块。 @@ -1915,7 +1915,7 @@ interface BlockViewBase { attributes: Record // 筛选属性(含 kind),见「筛选」 concepts: BlockConcept[] // 认出来的概念,从左到右;webview 按图标名画,不自己用种类查 badge?: KindBadge // 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色;图标在 concepts 里。和类块一样默认折叠 - slot?: { row: number, item?: number } // 库规则放进父卡片属性表里的子块才有(RuleSlot):画在父卡片第 row 行下面;item 是讲它的子行,点它才展开(它的块 id 记成折叠着就是收着) + slot?: { row: number, item: number } // 库规则挂在父卡片子行上的子块才有(RuleSlot):挂在父卡片第 row 行的第 item 个子行上,点它才在这一行下面展开(它的块 id 记成折叠着就是收着) children: BlockView[] } @@ -2020,7 +2020,7 @@ vscode/src/codeReader/ languages/rust.ts Rust:代码块树 rules/rustTree.ts Rust 库规则读语法树的办法(可见性、属性、字段、变体、match) rules/rustRows.ts Rust 属性表的行(中文直译和库规则共用) - rules/rustImpl.ts impl 的方法放进方法表那一行 + rules/rustImpl.ts impl 的方法挂在方法表的子行上,点方法名展开 rules/serde.ts serde:可序列化的结构体 rules/thiserror.ts thiserror:错误枚举 rules/axum.ts axum:接口 @@ -2117,7 +2117,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 语言插件(`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`)。 -- Rust(`languages/rust.test.ts`、`rules/rustLibs.test.ts`,真实解析):`fixtures/rustLang.rs` 的导入、结构体、枚举、实现(方法放进方法表那一行)、函数(异步、分情况)、特质逐行同预览;给结构体加一个字段,字段表那一行标成新增。`fixtures/rustLibs.rs` 的 serde(改名、不输出、字段名改驼峰)、thiserror(展示、透传来源)、axum(方法、路径、路径参数)、sqlx、tauri 逐行同预览;没导入 serde 时 derive 仍是结构体;改 axum 的路径,路径那一格标成改过。`axum::routing` 这种来源算导入了 axum。 +- Rust(`languages/rust.test.ts`、`rules/rustLibs.test.ts`,真实解析):`fixtures/rustLang.rs` 的导入、结构体、枚举、实现(方法挂在方法表的子行上)、函数(异步、分情况)、特质逐行同预览;给结构体加一个字段,字段表那一行标成新增。`fixtures/rustLibs.rs` 的 serde(改名、不输出、字段名改驼峰)、thiserror(展示、透传来源)、axum(方法、路径、路径参数)、sqlx、tauri 逐行同预览;没导入 serde 时 derive 仍是结构体;改 axum 的路径,路径那一格标成改过。`axum::routing` 这种来源算导入了 axum。 - Dockerfile: - 语言插件(`languages/dockerfile.test.ts`,真实解析):两份 luffy 的 Dockerfile 的代码块树(同原型,后端的三行注释是阶段说明);并块(紧挨着才并,空行、注释、别的指令隔开,COPY 要从同一个地方,ADD、heredoc 的 COPY 不并,第一个 FROM 之前的是顶层);ONBUILD 和各种设置;facts(解析指令、阶段的名称 / 序号 / 说明 / 标签 / 摘要 / 平台 / 接着前面的阶段、构建参数和环境变量并块、健康检查两种写法和 NONE、启动);嵌着的 shell 的几段(续行去掉、heredoc 正文);数组写法的命令(VOLUME、SHELL 的数组不算);同一文件里的名字(阶段名在 --from、--mount 的 from、FROM 里,变量);中文名;着色。 - 中文直译(`translators/dockerfileZh.test.ts`,走 `readBlockViews`,RUN 交给 bash 的插件):两份 luffy 的 Dockerfile 整份逐行同 `NEXT_READING`、`BACKEND_READING`;阶段的各种写法、变量的展开和注解、RUN 的选项 / 数组写法 / heredoc / 一条循环 / 带重定向的一串、没有 shell 插件时照抄、复制和 ADD、设置、健康检查、启动;阶段名和表里变量名的源码下标、RUN 的源码按 shell 着色。 @@ -2125,7 +2125,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 命令规则(`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。 +- 库规则(宿主):导入了库(`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。挂在卡片子行上的子块(`RuleSlot`)记下行和子行,不是留下的子块、没有那一行 / 子行、放了两处的记下来(`libraryRules.test.ts`);webview 里它默认收着、子行名称是展开它的按钮、展开时画在那一行下面、那一行垫上种类色,收起那一行时跟着收起(`BlockTree.test.tsx`);默认折叠、跳到定义 / 上一处下一处 / 只看改动要展开的是那一行和它自己(`viewState`、`reveal`、`diffView` 的测试)。 - 同一行不在两处能改(webview `editing.test.ts`):块编辑时它的子孙都不画(卡片留下的方法连同主体、类的方法),跳到被盖住的块改闪它;子块编辑时只盖住自己的子孙;点「编辑」时重叠的块(祖先、子孙、`} else {` 共用一行的兄弟块)退出编辑;任意点法下正在编辑、画出来的块两两不重叠。 - SQLAlchemy 规则:luffy `referral/models.py` 的节选(`fixtures/sqlalchemyModels.py`)里 `ReferralRelation` 的属性表逐字同上面的样例,其余模型覆盖 `Mapped[bool]`、`int | None` 可空、`default=None`、`server_default`、别名的 `PgUUID`、`_enum` 辅助函数、关系、唯一约束、删除时置空 / 级联删除;枚举类、函数照常显示;卡片的编辑范围是整个类、同一行只在块和它的子孙之间共用;两个字段中间写着的方法照常是子块、卡片「编辑」写回整个类、字段前面的注释并进卡片、讲方法的注释留下;类名、字段名的定义范围和带下标的词都指回源码原文;webview 里跳到类名闪整块、跳到字段名只闪字段表里那一行并先展开「字段」那一行,卡片默认折叠;`import sqlalchemy as sa` 的老写法 `Column`、类型全名、只写注解的字段、不认得的类型照写法、docstring 当说明、方法留作子块。 - 库规则的引用:引用标的不是自己、规则没给 `resolve`、段没带下标时当替身不合约定;查引用交给标它的规则,规则不在、没给 `resolve`、出错时记下来、当作没有定义;视图数据里的引用(下标 → 引用)只从规则认领的块的属性表里取,查定义时是引用的问规则、其余问语言扩展,只对记下它的那个文档版本有效(`editor/definitionSources.test.ts`)。 diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index 32a9391..c6bc6f3 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -41,15 +41,15 @@ interface BlockViewBase { commentEndLine?: number /** 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色(RuleCard.badge)。图标在 concepts 里。预览同类块一样默认收起它的子块。 */ badge?: KindBadge - /** 库规则放进父卡片里的子块才有(见 RuleSlot):画在父卡片属性表第 row 行下面,不排在父卡片的子块里。 */ + /** 库规则挂在父卡片子行上的子块才有(见 RuleSlot):展开时画在父卡片属性表第 row 行下面,不排在父卡片的子块里。 */ slot?: BlockSlot children: BlockView[] } -/** 子块画在父卡片的哪儿:属性表第 row 行下面;item 是讲它的那个子行,点那个子行的第一格才展开(见 RuleSlot.item)。 */ +/** 子块挂在父卡片的哪儿:属性表第 row 行的第 item 个子行,点那个子行的第一格才在这一行下面展开(见 RuleSlot)。 */ export interface BlockSlot { row: number - item?: number + item: number } /** @@ -378,7 +378,7 @@ export function toBlockViews( return plain({ ...replacement.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, span, id, scope) } const { kind, badge, rows, defines, attributes } = replacement - const slots = new Map((replacement.slots ?? []).map(({ block: child, row, item }): [CodeBlock, BlockSlot] => [child, { row, ...(item !== undefined && { item }) }])) + const slots = new Map((replacement.slots ?? []).map(({ block: child, row, item }): [CodeBlock, BlockSlot] => [child, { row, item }])) const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), rowsText(span)) return { id, @@ -395,7 +395,7 @@ export function toBlockViews( properties: rows, } } - /** 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。slots 是父卡片放进它属性表里的那几块(见 BlockSlot)。 */ + /** 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。slots 是父卡片挂在它属性表子行上的那几块(见 BlockSlot)。 */ function list(siblings: readonly CodeBlock[], prefix: string, scope: string, from = 0, slots?: ReadonlyMap): BlockView[] { const spans = blockSpans(siblings, source) const views: BlockView[] = [] diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index 62c7b33..dcc97b3 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -889,19 +889,19 @@ export interface RuleCard { * 这一块的编辑范围同别的块,是原块整条语句(类就是整个类),盖住留下的子块:它编辑时预览不画这些子块,同一行不会同时在两处能改。 */ absorbs: CodeBlock[] - /** 画在卡片某一行下面的子块(见 RuleSlot);一块只能放一处。 */ + /** 挂在卡片某一行的子行上、点那个子行才展开的子块(见 RuleSlot);一块只能放一处。 */ slots?: RuleSlot[] } /** - * 卡片里的一处子块:block(原块 children 里的对象,没并进来的)不排在卡片后面,画在卡片第 row 行下面当子卡片, - * 照常转成块(翻译、库规则、编辑、问 AI、差异都照常)。item 是这一行的子行里讲它的那一个(第几个,0 起): - * 这时它默认收着,点那个子行的第一格才展开、一次只展开一个(Rust 实现块的方法表点方法名展开那张函数卡)。 + * 卡片里的一处子块:block(原块 children 里的对象,没并进来的)不排在卡片后面,挂在卡片第 row 行的第 item 个子行上(都从 0 起), + * 照常转成块(翻译、库规则、编辑、问 AI、差异都照常)。它默认收着,点那个子行的第一格才在这一行下面展开当子卡片, + * 同一行一次只展开一个(Rust 实现块的方法表点方法名展开那张函数卡)。 */ export interface RuleSlot { block: CodeBlock row: number - item?: number + item: number } /** diff --git a/vscode/src/codeReader/libraryRules.test.ts b/vscode/src/codeReader/libraryRules.test.ts index e940a07..0fa90ad 100644 --- a/vscode/src/codeReader/libraryRules.test.ts +++ b/vscode/src/codeReader/libraryRules.test.ts @@ -194,30 +194,33 @@ describe('库规则:认领', () => { expect(reports).toEqual(['库规则 omit 给第 3 行的 class 块的替身不合约定,这块照常显示:要略去的块还有子块']) }) - it('放进卡片属性表里的子块:照常转成块,记下画在哪一行(哪个子行);不是留下的子块、没有那一行 / 子行、放了两处的不合约定', async () => { - const rows = [{ label: '字段', value: [], items: [{ cells: [[{ text: 'name', role: 'variable' as const }]] }], columns: ['名称'] }, { label: '方法', value: [] }] + it('挂在卡片子行上的子块:照常转成块,记下挂在哪一行的哪个子行;不是留下的子块、没有那一行 / 子行、放了两处的不合约定', async () => { + const rows = [ + { label: '字段', value: [], items: [{ cells: [[{ text: 'name', role: 'variable' as const }]] }], columns: ['名称'] }, + { label: '方法', value: [], items: [{ cells: [[{ text: 'greet', role: 'function' as const }]] }], columns: ['名称'] }, + ] const slotting = (slots: (block: CodeBlock) => RuleSlot[], id = 'slot'): LibraryRule => ({ ...cardRule(id), claim: block => block.kind === 'class' ? { kind: `${id}.card`, badge: { label: '卡片', color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows, absorbs: block.children.slice(0, 1), slots: slots(block) } : undefined, }) - const [, card] = await views(MODEL, [slotting(block => [{ block: block.children[2], row: 0, item: 0 }, { block: block.children[3], row: 1 }])]) - expect(card.children.map(child => [child.kind, child.slot])).toEqual([['comment', undefined], ['variable', { row: 0, item: 0 }], ['function', { row: 1 }]]) + const [, card] = await views(MODEL, [slotting(block => [{ block: block.children[2], row: 0, item: 0 }, { block: block.children[3], row: 1, item: 0 }])]) + expect(card.children.map(child => [child.kind, child.slot])).toEqual([['comment', undefined], ['variable', { row: 0, item: 0 }], ['function', { row: 1, item: 0 }]]) const reports: string[] = [] const bad = await views(MODEL, [ - slotting(block => [{ block: block.children[0], row: 1 }], 'absorbed'), - slotting(block => [{ block: block.children[3], row: 5 }], 'row'), - slotting(block => [{ block: block.children[3], row: 1, item: 0 }], 'item'), - slotting(block => [{ block: block.children[3], row: 1 }, { block: block.children[3], row: 0 }], 'twice'), + slotting(block => [{ block: block.children[0], row: 1, item: 0 }], 'absorbed'), + slotting(block => [{ block: block.children[3], row: 5, item: 0 }], 'row'), + slotting(block => [{ block: block.children[3], row: 1, item: 1 }], 'item'), + slotting(block => [{ block: block.children[3], row: 1, item: 0 }, { block: block.children[3], row: 0, item: 0 }], 'twice'), ], reports) expect(bad[1].kind).toBe('class') expect(reports.map(report => report.replace(/^.*照常显示:/, ''))).toEqual([ - '放进卡片里的块不是留下的子块', - '放进卡片第 5 行下面的块,卡片没有这一行', - '放进卡片第 1 行第 0 个子行的块,这一行没有这个子行', - '同一块放进了卡片里的几处', + '挂在子行上的块不是留下的子块', + '挂在卡片第 5 行的块,卡片没有这一行', + '挂在卡片第 1 行第 1 个子行的块,这一行没有这个子行', + '同一块挂在了卡片里的几处', ]) }) diff --git a/vscode/src/codeReader/libraryRules.ts b/vscode/src/codeReader/libraryRules.ts index 65552b0..5d843ef 100644 --- a/vscode/src/codeReader/libraryRules.ts +++ b/vscode/src/codeReader/libraryRules.ts @@ -50,14 +50,14 @@ function problemOf(rule: LibraryRule, block: CodeBlock, claimed: RuleBlock): str return `种类 id「${claimed.kind}」没以「${rule.id}.」开头` const slots = claimed.slots ?? [] if (slots.some(slot => !block.children.includes(slot.block) || claimed.absorbs.includes(slot.block))) - return '放进卡片里的块不是留下的子块' + return '挂在子行上的块不是留下的子块' if (new Set(slots.map(slot => slot.block)).size < slots.length) - return '同一块放进了卡片里的几处' + return '同一块挂在了卡片里的几处' for (const { row, item } of slots) { if (!claimed.rows[row]) - return `放进卡片第 ${row} 行下面的块,卡片没有这一行` - if (item !== undefined && !claimed.rows[row].items?.[item]) - return `放进卡片第 ${row} 行第 ${item} 个子行的块,这一行没有这个子行` + return `挂在卡片第 ${row} 行的块,卡片没有这一行` + if (!claimed.rows[row].items?.[item]) + return `挂在卡片第 ${row} 行第 ${item} 个子行的块,这一行没有这个子行` } for (const { reference, offset } of referencedSegments(claimed.rows)) { if (reference?.rule !== rule.id) diff --git a/vscode/src/codeReader/rules/rustImpl.ts b/vscode/src/codeReader/rules/rustImpl.ts index 6534a6d..40d13b9 100644 --- a/vscode/src/codeReader/rules/rustImpl.ts +++ b/vscode/src/codeReader/rules/rustImpl.ts @@ -7,7 +7,7 @@ const BADGE: KindBadge = { label: '实现', color: COLOR, icon: 'square-function /** * impl 的方法表:点方法名展开那张函数卡。 - * 子块放进属性表某一行(RuleSlot)。不看导入,Rust 文件都启用。 + * 方法挂在方法表的子行上(RuleSlot)。不看导入,Rust 文件都启用。 */ export const rustImplRule: LibraryRule = { id: 'rust', diff --git a/vscode/webview-ui/src/codeReader/BlockTree.test.tsx b/vscode/webview-ui/src/codeReader/BlockTree.test.tsx index 5b33586..2c5b9bc 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.test.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.test.tsx @@ -12,7 +12,7 @@ vi.mock('./useOccurrences', async () => { }) vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } })) -const BADGE: KindBadge = { label: 'React 组件', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'atom' } +const BADGE: KindBadge = { label: '卡片', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'box' } function view(id: string, kind: string, text: string, extra: Partial = {}): BlockView { const badge = extra.badge @@ -32,23 +32,21 @@ function view(id: string, kind: string, text: string, extra: Partial } as BlockView } -/** 一张组件卡片:第 1 行下面放着一个副作用,第 2 行的两个子行各讲一个处理函数,后面还有一个留下的注释块。 */ +/** 一张卡片:第 1 行的两个子行各挂一张子卡片,后面还有一个留下的注释块。 */ const CARD: BlockView = { - ...view('0', 'react.component', ''), + ...view('0', 'demo.card', ''), badge: BADGE, properties: [ - { label: '名称', value: [{ text: 'Page', role: 'function' }] }, - { label: '副作用', value: [{ text: '1 个', role: 'hint' }] }, - { label: '事件处理', value: [{ text: '2 个', role: 'hint' }], columns: ['名称', '绑定在', '做什么'], items: [ - { cells: [[{ text: 'handleA', role: 'function' }], [], []] }, - { cells: [[{ text: 'handleB', role: 'function' }], [], []] }, + { label: '名称', value: [{ text: 'Server', role: 'type' }] }, + { label: '方法', value: [{ text: '2 个', role: 'hint' }], columns: ['名称', '参数', '返回'], items: [ + { cells: [[{ text: 'start', role: 'function' }], [], []] }, + { cells: [[{ text: 'stop', role: 'function' }], [], []] }, ] }, ], children: [ - view('0.0', 'react.effect', '滚回顶部', { slot: { row: 1 }, badge: { ...BADGE, label: '副作用', icon: 'zap' } }), - view('0.1', 'react.handler', '处理 A', { slot: { row: 2, item: 0 }, badge: { ...BADGE, label: '事件处理', icon: 'mouse-pointer-click' } }), - view('0.2', 'react.handler', '处理 B', { slot: { row: 2, item: 1 }, badge: { ...BADGE, label: '事件处理', icon: 'mouse-pointer-click' } }), - view('0.3', 'comment', '留下的注释'), + view('0.0', 'demo.method', '启动服务', { slot: { row: 1, item: 0 }, badge: { ...BADGE, label: '方法', icon: 'square-function' } }), + view('0.1', 'demo.method', '停止服务', { slot: { row: 1, item: 1 }, badge: { ...BADGE, label: '方法', icon: 'square-function' } }), + view('0.2', 'comment', '留下的注释'), ], } @@ -75,28 +73,28 @@ function render(folds: Record): string { return renderToStaticMarkup() } -describe('放进卡片属性表里的子块', () => { - it('副作用画在它那一行下面;点子行才展开的处理函数默认不画,名称是展开它的按钮;卡片收着时留下的子块不画', () => { +describe('挂在卡片子行上的子块', () => { + it('默认不画,子行的名称是展开它的按钮;卡片收着时留下的子块不画', () => { const html = render({}) - expect(html).toContain('滚回顶部') - expect(html).not.toContain('处理 A') + expect(html).not.toContain('启动服务') + expect(html).not.toContain('停止服务') expect(html).not.toContain('留下的注释') - expect(html).toContain('')) - expect(html.indexOf('滚回顶部')).toBeLessThan(html.indexOf('事件处理')) + expect(html).toContain('')) }) - it('折叠放着子块的那一行:它下面的子块跟着收起', () => { - expect(render({ '0/p1': true })).not.toContain('滚回顶部') + it('折叠挂着子块的那一行:展开着的子块跟着收起', () => { + expect(render({ '0.0': false })).toContain('启动服务') + expect(render({ '0.0': false, '0/p1': true })).not.toContain('启动服务') }) }) diff --git a/vscode/webview-ui/src/codeReader/BlockTree.tsx b/vscode/webview-ui/src/codeReader/BlockTree.tsx index 545b14e..d33d111 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.tsx @@ -67,7 +67,7 @@ export interface QaTree { /** * 块下面有没有画出来的子块(删掉的子块也算):子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。 - * 放进卡片属性表里的子块(BlockSlot)画在卡片里,不算。 + * 挂在卡片子行上的子块(BlockSlot)画在卡片属性表里,不算。 */ function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean { return block.children.some(child => !child.slot && shown(child, tree)) || (!tree.covered.has(block.id) && tree.diff?.removed.has(block.id) === true) @@ -125,7 +125,7 @@ function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data- * slotted:这一层是画在卡片属性表某一行下面的子块(BlockSlot),blocks 只有那几块。 */ export function BlockLevel({ blocks, owner, tree, slotted = false }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState, slotted?: boolean }): ReactElement { - // 放进卡片属性表里的子块画在卡片里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层 + // 挂在卡片子行上的子块画在卡片属性表里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层 const listed = slotted ? blocks : blocks.filter(block => !block.slot) const removed = slotted ? [] : tree.diff?.removed.get(owner?.id ?? '') ?? [] const listedIds = new Set(listed.map(block => block.id)) @@ -212,8 +212,8 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre } /** - * 卡片属性表里放的子块(BlockSlot)怎么画:一行下面的几块画成一层块(同块树);点子行才展开的,点那个子行的第一格展开它、 - * 同一行里别的收起,展开着的再点收起。卡片没有放进属性表的子块时没有。 + * 挂在卡片子行上的子块(BlockSlot)怎么画:一行下面展开着的几块画成一层块(同块树);点那个子行的第一格展开它、 + * 同一行里别的收起,展开着的再点收起。卡片没有挂在子行上的子块时没有。 */ function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefined { const blocks = block.children.filter(child => child.slot) @@ -227,7 +227,7 @@ function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefine tree.onSetFolds({ [child.id]: true }) return } - const others = blocks.filter(other => other !== child && other.slot?.row === child.slot?.row && other.slot?.item !== undefined) + const others = blocks.filter(other => other !== child && other.slot?.row === child.slot?.row) tree.onSetFolds({ ...Object.fromEntries(others.map(other => [other.id, true])), [child.id]: false }) }, } @@ -238,7 +238,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }): const editing = tree.editing.has(block.id) const collapsed = tree.collapsed.has(block.id) // 点父卡片子行才展开的子块:画出来就是展开着的,箭头收起整张 - const toggled = block.slot?.item !== undefined + const toggled = block.slot !== undefined const hasChildren = toggled || hasShownChildren(block, tree) const Chevron = collapsed && !toggled ? ChevronRight : ChevronDown const change = tree.diff?.changes.get(block.id) diff --git a/vscode/webview-ui/src/codeReader/PropertyGrid.tsx b/vscode/webview-ui/src/codeReader/PropertyGrid.tsx index 1397d83..73868f9 100644 --- a/vscode/webview-ui/src/codeReader/PropertyGrid.tsx +++ b/vscode/webview-ui/src/codeReader/PropertyGrid.tsx @@ -18,8 +18,8 @@ function gridTemplate(count: number): string { } /** - * 库规则放进卡片属性表里的子块(BlockSlot):blocks 是卡片的这些子块,render 把一行下面的几块画成一层块(同块树), - * onToggle 是点子行第一格时展开 / 收起讲它的那一块(点子行才展开的,见 BlockSlot.item)。 + * 库规则挂在卡片子行上的子块(BlockSlot):blocks 是卡片的这些子块,render 把一行下面展开着的几块画成一层块(同块树), + * onToggle 是点子行第一格时展开 / 收起讲它的那一块。 */ export interface GridSlots { blocks: BlockView[] @@ -234,7 +234,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl onToggleFold: (id: string) => void /** 标差异时这张属性表改了哪些行 */ marks?: RowsDiff - /** 放进这张属性表里的子块(库规则的卡片),见 GridSlots */ + /** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */ slots?: GridSlots }): ReactElement { const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after) @@ -249,15 +249,15 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl const Chevron = folded ? ChevronRight : ChevronDown const mark = marks?.changed[index] const placed = slots?.blocks.filter(block => block.slot?.row === index) ?? [] - // 点子行才展开的子块只画展开着的;别的放进这一行的子块总是画 - const shownBlocks = placed.filter(block => block.slot?.item === undefined || !collapsed.has(block.id)) - const toggles = new Map(placed.flatMap((block): [number, ItemToggle][] => block.slot?.item === undefined - ? [] - : [[block.slot.item, { open: !collapsed.has(block.id), badge: block.badge, onToggle: () => slots?.onToggle(block) }]])) + // 挂在子行上的子块只画展开着的 + const shownBlocks = placed.filter(block => !collapsed.has(block.id)) + const toggles = new Map(placed.flatMap((block): [number, ItemToggle][] => block.slot + ? [[block.slot.item, { open: !collapsed.has(block.id), badge: block.badge, onToggle: () => slots?.onToggle(block) }]] + : [])) return (
- {layout || placed.length > 0 + {layout ? (