✨ feat(vscode): Rust 的 #![…] 内部属性做成「编译属性」卡片

This commit is contained in:
cruldra committed 2026-10-08 07:17:25 +08:00
1 parent 2f610e9cb8
commit 04a11eb0b2
14 files changed
+386 -20

No files matched your search

+16 -6
View File
@@ -166,11 +166,11 @@
| directive 解析指令 | `terminal` | stage 阶段 | `layers` | run 执行 | `square-terminal` |
| copy 复制 | `copy` | setting 设置 | `sliders-horizontal` | label 标签 | `tag` |
| health 健康检查 | `activity` | start 启动 | `play` | trait 特质 | `network` |
| impl 实现 | `square-function` | module 模块 | `layers` | | |
| impl 实现 | `square-function` | module 模块 | `layers` | attribute 编译属性 | `sliders-horizontal` |
库规则的种类用规则给的图标(`KindBadge.icon`),跟在语言的种类图标后面:SQLAlchemy 数据表 `database`、数据类 `package`、Pydantic 数据模型 `braces`(和类型块同一个图标,颜色不同)、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`、React 组件 `atom`、状态 `toggle-right`、引用 `anchor`、数据源 `download`、派生值 `sigma`、副作用 `zap`、事件处理函数 `mouse-pointer-click`、渲染输出 `layout-template`、serde 可序列化 `braces`、thiserror 错误 `triangle-alert`、axum 接口 `globe`、sqlx 查询 `database`、tauri 桌面命令 `square-function`。数据类写了 `frozen=True` 再加一把锁(`lock`,悬停「不可修改」)。实现块再由规则 `rust.impl` 换成卡片,左边因此有两个「实现」图标。
这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置共用一个,字段、标签共用一个——加库规则用的 14 个(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出的 8 个也在里面),lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,概念的 `icon` 只能写其中一个,写别的是类型错误。内置种类 → 图标名在 `contract.ts` 的 `KIND_ICONS`(宿主写进概念列表)。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。
这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置、编译属性共用一个,字段、标签共用一个——加库规则用的 14 个(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出的 8 个也在里面),lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,概念的 `icon` 只能写其中一个,写别的是类型错误。内置种类 → 图标名在 `contract.ts` 的 `KIND_ICONS`(宿主写进概念列表)。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。先是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
- 悬停提示(整个插件的网页只走这一条路,预览和看板共用同一个组件):带提示的元素只写 `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` 挂到元素上,读屏当描述念。
- 种类图标和底色按种类区分(颜色见「颜色」);注释块虚线框。
@@ -592,7 +592,7 @@
```ts
export type BlockKind =
| 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module' // Rust:特质、实现、模块(属性表)
| 'trait' | 'impl' | 'module' | 'attribute' // Rust:特质、实现、模块、编译属性(属性表)
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' // shell:解释器、选项(属性表),分情况、命令、管道(一行译文)
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start' // Dockerfile:解析指令、阶段、健康检查、启动(属性表),执行、复制、设置、标签(一行译文)
@@ -805,8 +805,10 @@ export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
export interface ExceptionBlock extends BlockShape { kind: 'exception', facts: ExceptionFacts } // 头部是整个类,留下的方法(含构造函数)等是子块
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
/** Rust 的内部属性 `#![…]`:生效的各个属性(cfg_attr 拆开)、cfg_attr 的生效条件(CfgPredicate:not / all / any / 配置项 / 照原文)、写在哪一层(文件顶层、mod、impl)。 */
export interface AttributeBlock extends BlockShape { kind: 'attribute', facts: AttributeFacts }
export type FactBlock
= | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
= | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | AttributeBlock | InterpreterBlock | OptionsBlock
| DirectiveBlock | StageBlock | HealthBlock | StartBlock // 各带自己的 facts:InterpreterFacts、OptionsFacts、DirectiveFacts、StageFacts、HealthFacts、StartFacts
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
@@ -943,6 +945,8 @@ export interface DisplayNameReader {
export interface SourceDocument {
source: string
language: LanguageAdapter
/** 文件路径(TextDocument.fileName);宿主没给(嵌着的代码、只读一段源码)时没有,按文件名定的说法写成判断不了的那种(Rust 编译属性的作用范围)。 */
fileName?: string
markers: SourceRange[]
/**
* 调用的名字(CallFacts.name)的定义是哪种块:宿主问语言扩展定义在哪,用那个文件的语言插件分块认出来。
@@ -1816,6 +1820,12 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令
- `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)的子块。
- 内部属性 `#![…]`(`inner_attribute_item`)→ attribute(中文「编译属性」,图标 `sliders-horizontal`,颜色同常量词性的蓝 `--cr-constant`:暗 #4fc1ff / 亮 #0070c1,和特质的声明关键字蓝、选项的黄分开)。它写给所在的 crate 或模块,例如 Tauri `main.rs` 开头的 `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]`。语言插件把 `cfg_attr(条件, a, b)` 拆成生效条件和 a、b 两个属性(`AttributeFacts`),别的属性就是它自己一个;`cfg_attr` 只写了条件、没写属性的照一个属性算。卡片行(有才显示):
- 属性:属性名连参数照原文着色(`windows_subsystem = "windows"`、`allow(dead_code, unused_imports)`)。`cfg_attr` 里写了几个属性时值是「N 个」,下面一张「属性 | 说明」表,每个属性一行(认不得的说明写「—」),不再另起「说明」一行。
- 生效条件:只有 `cfg_attr` 有。`debug_assertions` 是「调试构建」,`test` 是「测试构建」,`target_os = "windows"` 是「目标系统 windows」,`feature = "x"` 是「启用特性 x」,`not(x)` 是「非 x」,`all(…)` 用「并且」、`any(…)` 用「或者」连起来,套在别的条件里、有两项以上的加括号(「unix 并且 非 (测试构建 或者 启用特性 mock)」);别的配置项、认不得的写法照原文。「非」「并且」「或者」按 operator 着色,特性名、系统名按 string。
- 作用范围(按 keyword 着色):写在文件顶层时,crate 根文件是「整个 crate」,别的文件是「本模块」,宿主没给文件名(`SourceDocument.fileName`)时是「所在模块」;写在 `mod` 身体里是「本模块」,`impl` 身体里是「本 impl 块」。crate 根文件按 Cargo 默认的认法(`rules/rustTree.ts` 的 `isCrateRoot`):`main.rs`、`lib.rs`,`src/bin/` 下的 `.rs`,包根下 `examples/`、`tests/`、`benches/` 里直接放的 `.rs`(`src/` 下同名目录里的是模块)。
- 说明(按 comment 着色,列出的名字按 variable):认得的属性才有。`windows_subsystem = "windows"`「在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口」,`= "console"`「在 Windows 上编译为控制台程序(console 子系统),启动时打开控制台窗口」;`allow` / `warn` / `deny` / `forbid`「编译检查(lint)级别设为放行 / 警告 / 报错 / 强制报错(内层不能再放宽):」后面列括号里的各项;`feature`「启用实验特性(需 nightly 编译器):」后面列名字;`no_std`「不链接标准库 std,只使用核心库 core」;`no_main`「不使用标准的 main 入口函数,程序入口另行提供」;`recursion_limit = "N"`「宏展开等编译期递归的深度上限设为 N(默认 128)」。
- 挂在函数、结构体上的外部属性 `#[…]` 不变:算进那一项的头部,`derive`、库规则照旧。
- 其余(`let`、宏调用)→ statement。不拆函数体。模块身体里宏调用后面多出来的那个空 `;`(`empty_statement`)不成块。
### 属性表信息(facts)从哪来
@@ -1832,7 +1842,7 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令
- Shell:解释器取 `#!` 后面的整句,第一个词是 `env` 的是按 PATH 找、程序名是它后面那个词,否则是写死的路径、程序名是路径最后一节。选项:每个开关一项,单字母换成 bash 的长名字(e → errexit、u → nounset、x → xtrace……),`-o 名字` 照写,`-` 开、`+` 关。变量:`variable_assignment` 的 name / value,声明里没给值的只有 name。函数:name 是函数名,doc 是紧贴在上面的注释;`positional` 是主体里(不进嵌套的函数)用到的 `$N` / `${N}` / `${N:-…}`,照位置排,`local x="$N"`、`x=$N` 给它起名字;`prints` 是没重定向到别处的 `echo` / `printf`(进 if、循环、分情况,不进 `$(…)`、管道、嵌套的函数);`captured` 看同一文件里有没有 `$(函数名 …)`;`returns` 是主体里的 `return` 命令。
- Dockerfile:解析指令是每行注释 `# 名字=值` 的名字(小写)和值;阶段取 `image_spec` 的 name / tag / digest 字段、`as` 字段(image_alias),stage 看镜像名是不是前面某个阶段的名字(不分大小写),选项是 `param`、`mount_param` 节点的原文 `--名字=值`(值在语法树里没有单独的节点,按原文拆,记下值从哪开始);变量是 `env_pair` 的 name / value、`arg_instruction` 的 name / default;健康检查的 none 看有没有 NONE,检查命令是里面的 `cmd_instruction`;数组写法取 `json_string_array`,shell 写法取 `shell_command` 里各个 `shell_fragment`(`line_continuation` 不算),只有一个 `<<EOT` 的取它下面 `heredoc_block` 从第一行到最后一行。
- Python 变量:`assignment` 的 left / type / right,说明是紧跟在后面的属性 docstring 的字符串节点(见上面代码块树规则);right 是 `dictionary` 时 entries 是它的具名子节点(注释除外):`pair` 的 key / value(value 又是 `dictionary` 时连它的 entries)、`dictionary_splat` 里的表达式。
- Rust:函数(含 `extern` 块里没有函数体的 `function_signature_item`)的 name / parameters / return_type,`async` 只在写了时填;`self` 的类型节点是整个接收者(`&mut self`)。结构体、枚举、特质、实现、模块的名称、说明、可见性见「Rust」(模块用 ClassFacts:name、doc、access)。`const`、`static` 的名称、类型、值是 `const_item` / `static_item` 的 name / type / value(VariableFacts 的一项 declarator),可见性同别的项:`pub` 写在 `facts.access`,其余写在块的 `access` 属性上。字段和变体携带的数据由中文直译读语法树,不另做 facts。
- Rust:函数(含 `extern` 块里没有函数体的 `function_signature_item`)的 name / parameters / return_type,`async` 只在写了时填;`self` 的类型节点是整个接收者(`&mut self`)。结构体、枚举、特质、实现、模块的名称、说明、可见性见「Rust」(模块用 ClassFacts:name、doc、access)。`const`、`static` 的名称、类型、值是 `const_item` / `static_item` 的 name / type / value(VariableFacts 的一项 declarator),可见性同别的项:`pub` 写在 `facts.access`,其余写在块的 `access` 属性上。字段和变体携带的数据由中文直译读语法树,不另做 facts。编译属性(AttributeFacts):`inner_attribute_item` 里的 `attribute` 是名字(路径照写)后面跟 `= 值`(字符串取 `string_content`)或 `token_tree`;`token_tree` 按顶层逗号切成各段(每段是几个词),`cfg_attr` 的第一段读成 CfgPredicate(`all` / `any` / `not` 带括号的、只有一个名字或 `名字 = 值` 的配置项,别的照原文),后面各段同样读成名字、值、括号里各项的原文;placement 看 `#![…]` 的上一层(`source_file`)或上两层(`mod_item` / `impl_item` 的 `declaration_list`)。
## 中文模板直译
@@ -2124,7 +2134,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/rustComments.rs` 开头连着写的 `//` 合成一块注释,预览是去掉 `//` 的三段 Markdown;隔着空行、语句的注释各是一块,行尾 `//` 不单独成块,`//!` 连着几行合成一块,`///` 并进下一个函数当说明,`mod`、`impl` 身体里同一套。`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/rustComments.rs` 开头连着写的 `//` 合成一块注释,预览是去掉 `//` 的三段 Markdown;隔着空行、语句的注释各是一块,行尾 `//` 不单独成块,`//!` 连着几行合成一块,`///` 并进下一个函数当说明,`mod`、`impl` 身体里同一套。编译属性:Tauri `main.rs` 的 `cfg_attr` 卡片逐行(属性的着色、「非 调试构建」、「整个 crate」、windows 子系统的说明),别的文件顶层的 `allow` 是本模块、列出 lint 名,认不得的属性没有说明、不知道文件名时是所在模块,`cfg_attr` 里几个属性是一张表、套着的条件加括号,`isCrateRoot` 的几种路径。`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 着色。
+3 -1
View File
@@ -428,6 +428,7 @@ export interface DocumentViews {
* 命令名的命令规则(SourceDocument.commandRule)。
*/
export interface DocumentExtras {
fileName?: SourceDocument['fileName']
rules?: RuleRun
calleeKind?: SourceDocument['calleeKind']
commandRule?: SourceDocument['commandRule']
@@ -467,7 +468,7 @@ export async function readBlockViews(
parts.set(code.node.startIndex, { code, root, document, translator: language.translator, steps })
}
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
const { calleeKind, commandRule } = extras
const { fileName, calleeKind, commandRule } = extras
const readings = new Map<number, EmbeddedReading>()
const embedded = (node: Node): EmbeddedReading | undefined => {
const part = parts.get(node.startIndex)
@@ -481,6 +482,7 @@ export async function readBlockViews(
source,
language: adapter,
markers,
...(fileName && { fileName }),
...(calleeKind && { calleeKind }),
...(commandRule && { commandRule }),
...(parts.size > 0 && { embedded }),
+37 -3
View File
@@ -12,7 +12,7 @@ export type { Node }
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module'
| 'trait' | 'impl' | 'module' | 'attribute'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
@@ -214,6 +214,35 @@ export interface ClassFacts {
implements: Node[]
}
/** Rust 的内部属性 `#![…]`:`cfg_attr(条件, a, b)` 拆成生效条件和 a、b 两个属性,别的属性就是它自己一个。 */
export interface AttributeFacts {
/** 生效的各个属性,照写的顺序。 */
attributes: AttributeItem[]
/** `cfg_attr` 的生效条件;别的属性没有。 */
condition?: CfgPredicate
/** 写在哪一层:文件顶层、`mod` 身体、`impl` 身体;别处(`extern` 块里)没有。作用范围按它定,文件顶层还要看是不是 crate 根文件。 */
placement?: 'file' | 'module' | 'impl'
}
/** 一个属性:`名字`、`名字 = 值` 或 `名字(各项)`。 */
export interface AttributeItem {
/** 整个属性(名字连参数)在源码里的范围,预览照原文着色。 */
span: SourceSpan
/** 名字,路径照写(`rustfmt::skip`)。 */
name: string
/** `名字 = 值` 的值:字符串去掉引号,别的照原文。 */
value?: string
/** `名字(…)` 括号里逗号隔开的各项,照原文(`clippy::too_many_arguments` 是一项);没写括号是空的。 */
arguments: string[]
}
/** `cfg` 条件:`not(…)`、`all(…)`、`any(…)`,一个配置项(`test`、`target_os = "windows"`),认不出的写法照原文。 */
export type CfgPredicate
= | { kind: 'not', operand: CfgPredicate }
| { kind: 'all' | 'any', operands: CfgPredicate[] }
| { kind: 'option', name: string, value?: string }
| { kind: 'other', text: string }
/** 枚举的一个成员;节点都指向源码。 */
export interface EnumMemberFacts {
name: Node
@@ -480,6 +509,8 @@ export interface TraitBlock extends BlockShape { kind: 'trait', facts: ClassFact
/** Rust 的 impl:标题是「某类型 的方法」,方法是子块,放进方法表的某一行。 */
export interface ImplBlock extends BlockShape { kind: 'impl', facts: ClassFacts }
export interface ModuleBlock extends BlockShape { kind: 'module', facts: ClassFacts }
/** Rust 的内部属性 `#![…]`:写给所在 crate 或模块的编译属性。 */
export interface AttributeBlock extends BlockShape { kind: 'attribute', facts: AttributeFacts }
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
/** 异常块的头部是整个类(类属性都在属性表里讲),留下的方法(含构造函数)等是子块。 */
@@ -497,7 +528,7 @@ export interface StageBlock extends BlockShape { kind: 'stage', facts: StageFact
export interface HealthBlock extends BlockShape { kind: 'health', facts: HealthFacts }
export interface StartBlock extends BlockShape { kind: 'start', facts: StartFacts }
export type FactBlock
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | AttributeBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
| DirectiveBlock | StageBlock | HealthBlock | StartBlock
/** 带说明(facts.doc)的块。 */
@@ -695,6 +726,8 @@ export interface DisplayNameReader {
export interface SourceDocument {
source: string
language: LanguageAdapter
/** 文件路径(VS Code 的 TextDocument.fileName);宿主没给(嵌着的代码、只读一段源码)时没有,按文件名定的说法写成判断不了的那种。 */
fileName?: string
/** 写在注释里给代码阅读看的记号(中文名记号,见 DisplayNameFacts.markers):不是说明文字,翻注释、说明时去掉 */
markers: SourceRange[]
/**
@@ -743,7 +776,7 @@ export interface ThemeColor { dark: string, light: string }
/**
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 26 个给内置种类(几种共用一个:
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置共用 sliders-horizontal,字段、标签共用 tag),
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置、编译属性共用 sliders-horizontal,字段、标签共用 tag),
* 宿主标的入口块也用这组里的 play(entries/entryMarks.ts 的 ENTRY_CONCEPT),
* 后面的给库规则的种类(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出也在里面);库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),
* 没有按任意名字找图标的路。
@@ -804,6 +837,7 @@ export const KIND_ICONS: Record<BlockKind, KindIconName> = {
trait: 'network',
impl: 'square-function',
module: 'layers',
attribute: 'sliders-horizontal',
type: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
@@ -13,6 +13,8 @@ export interface DocumentSnapshot {
languageId: string
version: number
getText: () => string
/** 文件路径,交给翻译插件(SourceDocument.fileName) */
fileName?: string
/** 这份文件是项目写明的入口时,预览里名字对上的块标成入口(见 entries/entryPoints.ts) */
entryNames?: readonly string[]
}
@@ -57,6 +59,7 @@ export async function documentMessage(
try {
const rules = registry.rulesFor(languageId)
const { blocks, displayNames, symbols } = await readBlockViews(runtime, adapter, translator, languageId, source, {
...(document.fileName && { fileName: document.fileName }),
rules: { rules, report, lookup },
calleeKind,
commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report),
@@ -328,7 +328,7 @@ class Preview {
const source = this.document.getText()
const session = this.lookups.session()
const callees = this.callees.session()
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
}, session.lookup, callees.calleeKind)
if (request !== this.latest || !this.ready)
@@ -356,7 +356,7 @@ class Preview {
*/
private async readOld(source: string): Promise<OldDocument | undefined> {
const { message } = await documentMessage(
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) },
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) },
this.registry,
this.runtime,
() => {},
+68 -2
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../blockView'
import { blockMarks } from '../diff/viewDiff'
import { rustImplRule } from '../rules/rustImpl'
import { isCrateRoot } from '../rules/rustTree'
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { createRustAdapter } from './rust'
@@ -10,9 +11,9 @@ import { createRustAdapter } from './rust'
const adapter = createRustAdapter(WASM_DIR)
const sample = readFixture('rustLang.rs')
function views(source: string): Promise<BlockView[]> {
function views(source: string, fileName?: string): Promise<BlockView[]> {
return withBlocks(adapter, 'rust', source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, {
rules: [rustImplRule],
report: () => {},
}))
@@ -270,3 +271,68 @@ describe('rust 注释', () => {
expect(markdown(impl?.children.find(block => block.kind === 'comment'))).toBe('三\n\n四')
})
})
describe('rust 编译属性', () => {
it('tauri 的 main.rs 开头的 cfg_attr:属性、生效条件、作用范围、说明', async () => {
const source = [
'// Prevents additional console window on Windows in release, DO NOT REMOVE!!',
'#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]',
'',
'fn main() {}',
'',
].join('\n')
const list = await views(source, '/repo/apps/desktop/src-tauri/src/main.rs')
expect(list.map(block => block.kind)).toEqual(['comment', 'attribute', 'function'])
expect(viewLines([card(list, 'attribute')])).toEqual([
'attribute 2',
' 属性: windows_subsystem = "windows"',
' 生效条件: 非 调试构建',
' 作用范围: 整个 crate',
' 说明: 在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口',
])
const attribute = card(list, 'attribute')
const value = 'properties' in attribute ? attribute.properties[0]?.value : undefined
expect(Array.isArray(value) && value.map(piece => 'role' in piece && piece.role)).toEqual(['variable', 'plain', 'plain', 'plain', 'string', 'string', 'string'])
})
it('allow 列出各个 lint,别的文件顶层是本模块', async () => {
const list = await views('#![allow(dead_code, unused_imports)]\n', '/repo/src/credentials.rs')
expect(viewLines(list)).toEqual([
'attribute 1',
' 属性: allow(dead_code, unused_imports)',
' 作用范围: 本模块',
' 说明: 编译检查(lint)级别设为放行:dead_code、unused_imports',
])
})
it('认不得的属性没有说明,不知道文件名时作用范围是所在模块', async () => {
expect(viewLines(await views('#![rustfmt::skip]\n'))).toEqual([
'attribute 1',
' 属性: rustfmt::skip',
' 作用范围: 所在模块',
])
})
it('cfg_attr 写了几个属性是一张表;all、any 套着的条件加括号;mod 身体里是本模块', async () => {
const source = [
'mod platform {',
' #![cfg_attr(all(unix, not(any(test, feature = "mock"))), allow(clippy::foo), deny(warnings))]',
'}',
'',
].join('\n')
const mod = card(await views(source, '/repo/src/main.rs'), 'module')
expect(viewLines(mod.children)).toEqual([
'attribute 2',
' 属性: 2 个',
' allow(clippy::foo) | 编译检查(lint)级别设为放行:clippy::foo',
' deny(warnings) | 编译检查(lint)级别设为报错:warnings',
' 生效条件: unix 并且 非 (测试构建 或者 启用特性 mock)',
' 作用范围: 本模块',
])
})
it('crate 根文件:main.rs、lib.rs、src/bin 下的、包根 tests / examples / benches 里直接放的', () => {
expect(['/p/src/main.rs', '/p/src/lib.rs', '/p/src/bin/tool.rs', '/p/tests/api.rs', 'C:\\p\\examples\\demo.rs'].map(isCrateRoot)).toEqual([true, true, true, true, true])
expect(['/p/src/credentials.rs', '/p/src/tests/helpers.rs', '/p/src/bin/tool/args.rs'].map(isCrateRoot)).toEqual([false, false, false])
})
})
+107 -2
View File
@@ -1,8 +1,8 @@
import type { Access, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, TokenRole, TraitBlock } from '../contract'
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named } from './shared'
import { children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
@@ -69,6 +69,8 @@ function itemBlock(item: Node): CodeBlock {
case 'function_item':
case 'function_signature_item':
return functionBlock(item)
case 'inner_attribute_item':
return innerAttributeBlock(item)
default:
return leafBlock('statement', item)
}
@@ -104,6 +106,109 @@ function valueBlock(node: Node, kind: 'const' | 'variable'): CodeBlock {
}
}
const PLACEMENTS: Record<string, AttributeFacts['placement']> = { source_file: 'file', mod_item: 'module', impl_item: 'impl' }
/** `#![…]`:写给所在 crate 或模块的编译属性。`cfg_attr(条件, a, b)` 拆成生效条件和 a、b。 */
function innerAttributeBlock(node: Node): CodeBlock {
const attribute = named(node).find(child => child.type === 'attribute')
if (!attribute)
return leafBlock('statement', node)
const meta = metaItem(tokens(attribute))
const [condition, ...applied] = meta.name === 'cfg_attr' ? meta.list ?? [] : []
const conditional = condition !== undefined && applied.length > 0
const container = node.parent?.type === 'source_file' ? node.parent : node.parent?.parent
const placement = container ? PLACEMENTS[container.type] : undefined
return {
kind: 'attribute',
...leafShape(node),
facts: {
attributes: conditional ? applied.map(item => attributeItem(metaItem(item))) : [attributeItem(meta)],
...(conditional && { condition: cfgPredicate(condition) }),
...(placement && { placement }),
},
}
}
/** 属性的写法:名字(路径),后面 `= 值` 或括号里逗号隔开的各段。tokens 是这一段的各个词。 */
interface MetaItem {
tokens: Node[]
name: string
value?: Node
list?: Node[][]
}
function metaItem(words: Node[]): MetaItem {
const at = words.findIndex(word => word.type === '=' || word.type === 'token_tree')
const path = at < 0 ? words : words.slice(0, at)
const mark = words[at]
return {
tokens: words,
name: textOf(path),
...(mark?.type === '=' && words[at + 1] && { value: words[at + 1] }),
...(mark?.type === 'token_tree' && { list: listItems(mark) }),
}
}
/** 括号里逗号隔开的各段,每段是它的各个词。 */
function listItems(tree: Node): Node[][] {
const items: Node[][] = [[]]
for (const word of tokens(tree).slice(1, -1)) {
if (word.type === ',')
items.push([])
else
items[items.length - 1].push(word)
}
return items.filter(item => item.length > 0)
}
function attributeItem(meta: MetaItem): AttributeItem {
return {
span: spanOf(meta.tokens),
name: meta.name,
...(meta.value && { value: literalText(meta.value) }),
arguments: (meta.list ?? []).map(textOf),
}
}
function cfgPredicate(words: Node[]): CfgPredicate {
const meta = metaItem(words)
if (meta.list && (meta.name === 'all' || meta.name === 'any'))
return { kind: meta.name, operands: meta.list.map(cfgPredicate) }
if (meta.list?.length === 1 && meta.name === 'not')
return { kind: 'not', operand: cfgPredicate(meta.list[0]) }
if (!meta.list && words[0]?.type === 'identifier' && (words.length === 1 || meta.value))
return { kind: 'option', name: meta.name, ...(meta.value && { value: literalText(meta.value) }) }
return { kind: 'other', text: textOf(words) }
}
/** 节点的各个词(含标点),注释不算。 */
function tokens(node: Node): Node[] {
return children(node).filter(child => !isCommentNode(child))
}
/** 连着的几个词在源码里的原文(它们是同一个节点的子节点)。 */
function textOf(words: Node[]): string {
const [first] = words
const last = words[words.length - 1]
const parent = first?.parent
if (!first || !last || !parent)
return ''
return parent.text.slice(first.startIndex - parent.startIndex, last.endIndex - parent.startIndex)
}
function spanOf(words: Node[]): SourceSpan {
const first = words[0]
const last = words[words.length - 1]
return { startIndex: first.startIndex, endIndex: last.endIndex, startRow: first.startPosition.row, endRow: last.endPosition.row }
}
/** 字面量的值:字符串去掉引号,别的照原文。 */
function literalText(node: Node): string {
if (node.type !== 'string_literal')
return node.text
return named(node).find(child => child.type === 'string_content')?.text ?? ''
}
/** 写在前面的 `#[…]` 算进头部。 */
function withAttributes(block: CodeBlock): CodeBlock {
const [attribute] = leadingAttributes(block.node)
+100 -1
View File
@@ -1,4 +1,4 @@
import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, TokenRole, TraitBlock } from '../contract'
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { RustVisibility } from './rustTree'
import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
@@ -72,6 +72,97 @@ export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] {
]
}
const LINT_LEVELS: Record<string, string> = { allow: '放行', warn: '警告', deny: '报错', forbid: '强制报错(内层不能再放宽)' }
/**
* 编译属性 `#![…]`:属性(照原文着色)/ 生效条件(只有 cfg_attr)/ 作用范围 / 说明(认得的属性才有)。
* cfg_attr 里写了几个属性时「属性」是一张「属性 | 说明」表,不再另起「说明」一行。
* original 把源码的一段照原文着色;crateRoot 是这份文件是不是 crate 根文件,不知道文件名时没有。
*/
export function attributeRows(block: AttributeBlock, original: (span: SourceSpan) => Inline[], crateRoot: boolean | undefined): PropertyRow[] {
const { attributes, condition, placement } = block.facts
const single = attributes.length === 1 ? attributes[0] : undefined
const description = single && attributeNote(single)
return [
single
? { label: '属性', value: original(single.span) }
: {
label: '属性',
value: [hint(`${attributes.length} 个`)],
columns: ['属性', '说明'],
items: attributes.map(item => ({ cells: [original(item.span), attributeNote(item) ?? [hint('—')]] })),
},
...(condition ? [{ label: '生效条件', value: cfgText(condition, false) }] : []),
{ label: '作用范围', value: [keyword(attributeScope(placement, crateRoot))] },
...(description ? [{ label: '说明', value: description }] : []),
]
}
function attributeScope(placement: AttributeFacts['placement'], crateRoot: boolean | undefined): string {
switch (placement) {
case 'file':
return crateRoot === undefined ? '所在模块' : crateRoot ? '整个 crate' : '本模块'
case 'module':
return '本模块'
case 'impl':
return '本 impl 块'
default:
return '所在模块'
}
}
/** 认得的属性的书面说明;认不得的没有。 */
function attributeNote(item: AttributeItem): Inline[] | undefined {
const names = item.arguments.flatMap((name, i) => [...(i > 0 ? [note('、')] : []), { text: name, role: 'variable' as const }])
const level = LINT_LEVELS[item.name]
if (level)
return names.length > 0 ? [note(`编译检查(lint)级别设为${level}:`), ...names] : undefined
switch (item.name) {
case 'windows_subsystem':
if (item.value === 'windows')
return [note('在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口')]
return item.value === 'console' ? [note('在 Windows 上编译为控制台程序(console 子系统),启动时打开控制台窗口')] : undefined
case 'feature':
return names.length > 0 ? [note('启用实验特性(需 nightly 编译器):'), ...names] : undefined
case 'no_std':
return [note('不链接标准库 std,只使用核心库 core')]
case 'no_main':
return [note('不使用标准的 main 入口函数,程序入口另行提供')]
case 'recursion_limit':
return item.value ? [note(`宏展开等编译期递归的深度上限设为 ${item.value}(默认 128)`)] : undefined
default:
return undefined
}
}
const CFG_OPTIONS: Record<string, string> = { debug_assertions: '调试构建', test: '测试构建' }
const CFG_VALUES: Record<string, string> = { target_os: '目标系统', feature: '启用特性' }
/** cfg 条件读成中文:not 是「非」,all 用「并且」、any 用「或者」连起来,套在别的条件里的加括号;认不得的照原文。 */
function cfgText(predicate: CfgPredicate, nested: boolean): Inline[] {
switch (predicate.kind) {
case 'not':
return [operator('非'), plain(' '), ...cfgText(predicate.operand, true)]
case 'all':
case 'any': {
if (predicate.operands.length === 0)
return [hint(predicate.kind === 'all' ? '总是成立' : '从不成立')]
const joiner = operator(predicate.kind === 'all' ? '并且' : '或者')
const parts = predicate.operands.flatMap((operand, i) => [...(i > 0 ? [plain(' '), joiner, plain(' ')] : []), ...cfgText(operand, true)])
return nested && predicate.operands.length > 1 ? [plain('('), ...parts, plain(')')] : parts
}
case 'option': {
const { name, value } = predicate
if (value === undefined)
return [plain(CFG_OPTIONS[name] ?? name)]
const label = CFG_VALUES[name]
return label ? [plain(`${label} `), { text: value, role: 'string' }] : [plain(`${name} = `), { text: `"${value}"`, role: 'string' }]
}
case 'other':
return [plain(predicate.text)]
}
}
export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
return [
nameRow(block.facts.name, say, 'type'),
@@ -178,6 +269,14 @@ function keyword(text: string): Inline {
return { text, role: 'keyword' }
}
function operator(text: string): Inline {
return { text, role: 'operator' }
}
function note(text: string): Inline {
return { text, role: 'comment' }
}
function range(node: import('../contract').Node): { startIndex: number, endIndex: number } {
return { startIndex: node.startIndex, endIndex: node.endIndex }
}
+15
View File
@@ -32,6 +32,21 @@ export function oneLine(text: string): string {
return text.replace(/\s+/g, ' ').trim()
}
const TARGET_DIRS = new Set(['examples', 'tests', 'benches'])
/**
* Cargo 默认认作 crate 根的文件:`main.rs`、`lib.rs`,`src/bin/` 下的 `.rs`,
* 包根下 `examples/`、`tests/`、`benches/` 里直接放的 `.rs`(`src/` 下同名目录里的是模块)。
*/
export function isCrateRoot(fileName: string): boolean {
const [base, parent, grandparent] = fileName.split(/[\\/]/).reverse()
if (base === 'main.rs' || base === 'lib.rs')
return true
if (!base?.endsWith('.rs') || parent === undefined)
return false
return parent === 'bin' ? grandparent === 'src' : TARGET_DIRS.has(parent) && grandparent !== 'src'
}
/** 节点上的可见性。pub(in …) 不在这四档里,按本 crate 算(只本包能用)。 */
export function rustVisibility(node: Node): RustVisibility {
const vis = named(node).find(child => child.type === 'visibility_modifier')
+28 -2
View File
@@ -1,5 +1,6 @@
import type { FactBlock, Node, PropertyRow, SourceDocument, Translator } from '../contract'
import { enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
import type { FactBlock, Inline, Node, PropertyRow, SourceDocument, SourceSpan, Translator } from '../contract'
import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
import { isCrateRoot } from '../rules/rustTree'
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
const say = plainSay()
@@ -96,6 +97,8 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
return implRows(block, say)
case 'module':
return moduleRows(block, say)
case 'attribute':
return attributeRows(block, span => original(block.node, span, document), document.fileName === undefined ? undefined : isCrateRoot(document.fileName))
case 'const':
case 'variable':
return template.describe(block, document)
@@ -104,6 +107,29 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
}
}
/** node 里 span 那一段照原文:叶子按源码着色,叶子之间的空白照抄。 */
function original(node: Node, span: SourceSpan, { source, language }: SourceDocument): Inline[] {
const out: Inline[] = []
let cursor = span.startIndex
const visit = (current: Node): void => {
if (current.endIndex <= span.startIndex || current.startIndex >= span.endIndex || current.startIndex === current.endIndex)
return
if (current.childCount === 0) {
if (current.startIndex > cursor)
out.push({ text: source.slice(cursor, current.startIndex), role: 'plain' })
out.push({ text: current.text, role: language.tokenRole(current) })
cursor = current.endIndex
return
}
for (const child of current.children) {
if (child)
visit(child)
}
}
visit(node)
return out
}
/** 行注释去掉 `//`(文档注释连 `/`、`!`)和后面一个空格,缩进留给 Markdown;块注释去掉开头结尾的注释符号。 */
function commentText(node: Node, text: string): string {
if (node.type === 'line_comment')
@@ -207,6 +207,7 @@ export const KIND_ZH: Record<BlockKind, string> = {
trait: '特质',
impl: '实现',
module: '模块',
attribute: '编译属性',
enum: '枚举',
exception: '异常',
type: '类型',
@@ -722,6 +723,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return healthRows(block.facts, phrases)
case 'start':
return startRows(block.facts, phrases)
// Rust 的编译属性由 Rust 的直译排(translators/rustZh.ts)
case 'attribute':
return []
default:
return block.facts.scope ? scopedVariableRows(block.facts.declarators, block.facts.scope, phrases) : variableRows(block, phrases)
}
@@ -69,6 +69,7 @@ body.vscode-high-contrast-light {
[data-kind="function"] { --kind: var(--cr-kind-function); }
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
[data-kind="trait"] { --kind: var(--cr-storage); }
[data-kind="attribute"] { --kind: var(--cr-constant); }
[data-kind="impl"] { --kind: var(--cr-kind-function); }
[data-kind="enum"] { --kind: var(--cr-kind-enum); }
[data-kind="exception"] { --kind: var(--cr-kind-exception); }
@@ -24,6 +24,7 @@ describe('块的种类图标', () => {
trait: 'network',
impl: 'square-function',
module: 'layers',
attribute: 'sliders-horizontal',
type: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
+1 -1
View File
@@ -4,7 +4,7 @@
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module'
| 'trait' | 'impl' | 'module' | 'attribute'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'