diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 847098a..474c4df 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -1796,9 +1796,16 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 ### Rust -语法包是 `@vscode/tree-sitter-wasm` 里的 `tree-sitter-rust.wasm`(和 python 的 wasm 同一层:包入口就在 `wasm/`)。`languageIds: ['rust']`。样例 `fixtures/rustLang.rs`、`fixtures/rustLibs.rs`。原型是 `docs/prototype/code-reader/openpencil/exports/` 的 `rust-lang.png`、`rust-lib.png`、`rust-notes.png`。中文直译 `translators/rustZh.ts`:结构体不叫「类」,可见性不叫「访问级别」,`pub` 是「公开」。 +语法包是 `@vscode/tree-sitter-wasm` 里的 `tree-sitter-rust.wasm`(和 python 的 wasm 同一层:包入口就在 `wasm/`)。`languageIds: ['rust']`。样例 `fixtures/rustLang.rs`、`fixtures/rustLibs.rs`、`fixtures/rustComments.rs`。原型是 `docs/prototype/code-reader/openpencil/exports/` 的 `rust-lang.png`、`rust-lib.png`、`rust-notes.png`。中文直译 `translators/rustZh.ts`:结构体不叫「类」,可见性不叫「访问级别」,`pub` 是「公开」。 -可见性:`pub` 公开,没写是本模块,`pub(super)` 父模块,`pub(crate)` 和 `pub(in …)` 本 crate。公开写在 `facts.access`,其余写在块的 `access` 属性上(筛选值 `module` / `super` / `crate`)。说明是紧挨在前面的文档注释(`///`、`/**`)的第一句,句号留着。 +可见性:`pub` 公开,没写是本模块,`pub(super)` 父模块,`pub(crate)` 和 `pub(in …)` 本 crate。公开写在 `facts.access`,其余写在块的 `access` 属性上(筛选值 `module` / `super` / `crate`)。说明是紧挨在前面的文档注释(`///`、`/**`)的第一句,句号留着;连着几行的 `///` 算一段,中间可以隔着 `#[…]`。 + +注释和别的语言走同一条路(`listBlocks`,Rust 的 `CommentRules` 在 `languages/rust.ts`),文件顶层、`mod` 身体、`impl` 身体都一样: + +- 连续的行注释合成一块注释:相邻行、写法相同才合,`//`、讲所在模块或 crate 的 `//!`、讲下一项的 `///` 各是一种写法,不混着合。中间空的 `//` 不打断,是 Markdown 里的空行、分段;隔着空行、隔着别的语句就断开。 +- 和上一条语句同一行的 `//` 是行尾注释,不单独成块。 +- `///`(和 `/**` 开头的块注释)后面紧跟的是函数、结构体、枚举、特质、实现、模块、常量、`static` 时,是这一项的说明:进 `facts.doc`(第一条注释节点),头部往上延到它开头,不单独成块;后面是 `use`、宏调用、`let` 这类不带说明的项,或隔着别的注释时,照旧是注释块。 +- 注释块的预览是去掉 `//`(`///`、`//!` 连那个 `/`、`!`)和后面一个空格的原文,按「注释的 Markdown 排版」显示。语法包把 `///`、`//!` 行尾的换行算进注释节点,块的范围不算这个换行。 - `use` → import。来源是路径去掉最后一节(`std::sync::Arc` 的来源是 `std::sync`)。导入项表「名字 | 别名」,没起别名是「—」,`*` 是「*」。 - `struct` → class(中文直译把这个种类叫「结构体」)。字段不单独成块,表「字段 | 类型 | 可见性」。元组结构体没有这张表。 @@ -1809,7 +1816,7 @@ 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)的子块。 -- 其余(`let`、宏调用)→ statement。不拆函数体。`//` 注释单独成块。模块身体里宏调用后面多出来的那个空 `;`(`empty_statement`)不成块。 +- 其余(`let`、宏调用)→ statement。不拆函数体。模块身体里宏调用后面多出来的那个空 `;`(`empty_statement`)不成块。 ### 属性表信息(facts)从哪来 @@ -2117,7 +2124,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/rustComments.rs` 开头连着写的 `//` 合成一块注释,预览是去掉 `//` 的三段 Markdown;隔着空行、语句的注释各是一块,行尾 `//` 不单独成块,`//!` 连着几行合成一块,`///` 并进下一个函数当说明,`mod`、`impl` 身体里同一套。`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 着色。 diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index dcc97b3..834b85d 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -501,7 +501,7 @@ export type FactBlock | DirectiveBlock | StageBlock | HealthBlock | StartBlock /** 带说明(facts.doc)的块。 */ -export type DocumentedBlock = FunctionBlock | ClassBlock | TraitBlock | ImplBlock | EnumBlock | ExceptionBlock | VariableBlock +export type DocumentedBlock = FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | ExceptionBlock | VariableBlock /** 属性表块的种类和 facts,不带块的形状(节点、头部、子块):库规则把一块认成现成的一种属性表块时给它(RuleFacts)。 */ export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick : never : never diff --git a/vscode/src/codeReader/fixtures/rustComments.rs b/vscode/src/codeReader/fixtures/rustComments.rs new file mode 100644 index 0000000..addd4af --- /dev/null +++ b/vscode/src/codeReader/fixtures/rustComments.rs @@ -0,0 +1,11 @@ +// apps/desktop/src-tauri/src/credentials.rs +// +// 设备凭证按账号分开存 (键见 `device::account_key`: 后端地址 + 用户 id, 值为凭证 JSON)。 +// 执行器重启或同账号重登后拿它直接重连, 平台上还是同一台设备: 不必重新注册, 设备列表 +// 不多出一条, 应用的工作区也不因"换了设备"被重建。 +// +// 正式版存系统钥匙串 (macOS 钥匙串 / Windows 凭据管理器 / Linux Secret Service)。开发版 +// 存执行器数据目录下的文件: macOS 钥匙串按程序的确切构建认"谁能读", 没有苹果开发者身份的 +// 程序每重编一次就被当成新程序, 反复弹授权框。 + +use std::collections::HashMap; diff --git a/vscode/src/codeReader/languages/bash.ts b/vscode/src/codeReader/languages/bash.ts index d7f2ab6..b86b670 100644 --- a/vscode/src/codeReader/languages/bash.ts +++ b/vscode/src/codeReader/languages/bash.ts @@ -41,12 +41,13 @@ const SIMPLE_STATEMENTS = new Set(['command', 'redirected_statement', 'variable_ const EXPANSIONS = new Set(['simple_expansion', 'expansion', 'command_substitution', 'arithmetic_expansion', 'process_substitution']) const COMMENTS: CommentRules = { - isLine: () => true, + lineStyle: () => '#', // 紧贴在函数上面一行的注释是函数的说明 isDoc: (comment) => { const next = comment.nextSibling return next?.type === 'function_definition' && next.startPosition.row === comment.endPosition.row + 1 }, + documents: new Set(['function']), } function statements(items: Node[], afterRow: number, top: boolean): CodeBlock[] { diff --git a/vscode/src/codeReader/languages/python.ts b/vscode/src/codeReader/languages/python.ts index 67c4c75..a90dd8f 100644 --- a/vscode/src/codeReader/languages/python.ts +++ b/vscode/src/codeReader/languages/python.ts @@ -13,7 +13,7 @@ const PATTERNS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern']) /** 字符串开头(string_start):普通字符串的前缀只有 r / u 或没有;f、t、b 开头的不是。 */ const STRING_PREFIX = /^[ru]?["']/i // 只有 # 行注释;函数、类的说明是主体开头的 docstring,不是写在前面的注释 -const COMMENTS: CommentRules = { isLine: () => true, isDoc: () => false } +const COMMENTS: CommentRules = { lineStyle: () => '#', isDoc: () => false, documents: new Set() } function statements(items: Node[], scope: Scope, afterRow: number): CodeBlock[] { const docs = attributeDocs(items, scope) diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index bff2019..46dcdb7 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -25,6 +25,10 @@ function card(list: BlockView[], kind: string): BlockView { return found } +function markdown(block: BlockView | undefined): string | undefined { + return block && 'translation' in block && 'markdown' in block.translation ? block.translation.markdown : undefined +} + describe('rust 语言卡片', () => { it('use、结构体、枚举、实现、函数、特质', async () => { expect(viewLines(await views(sample))).toEqual([ @@ -139,13 +143,14 @@ describe('rust 语言卡片', () => { expect(mod?.kind).toBe('module') expect(list.some(block => block.kind === 'statement' && block.code.includes('const LIMIT'))).toBe(false) expect(list.filter(block => block.kind === 'statement').map(block => block.code)).toEqual(['mod other;']) - expect(mod?.children.map(block => block.kind)).toEqual(['import', 'const', 'variable', 'function', 'function', 'statement', 'statement']) + expect(mod?.children.map(block => block.kind)).toEqual(['comment', 'import', 'const', 'variable', 'function', 'function', 'statement', 'statement']) expect(viewLines(mod ? [mod] : [])).toEqual([ 'module 1', ' 名称: platform', ' 说明: 平台相关。', ' 可见性: 公开', - ' import 3', + ' comment 3: 读入。', + ' import 4', ' 来源: std::io', ' 导入项: 1 个', ' Read | —', @@ -180,3 +185,88 @@ describe('rust 语言卡片', () => { expect(viewLines(inner ? [inner] : []).join('\n')).toMatch(/类型: u8|值: 2/) }) }) + +describe('rust 注释', () => { + it('连着写的 // 合成一块注释,空的 // 是分段,预览是去掉 // 的 Markdown;后面的 use 照常是导入', async () => { + const list = await views(readFixture('rustComments.rs')) + expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([['comment', 1, 9], ['import', 11, 11]]) + expect(markdown(list[0])).toBe([ + 'apps/desktop/src-tauri/src/credentials.rs', + '', + '设备凭证按账号分开存 (键见 `device::account_key`: 后端地址 + 用户 id, 值为凭证 JSON)。', + '执行器重启或同账号重登后拿它直接重连, 平台上还是同一台设备: 不必重新注册, 设备列表', + '不多出一条, 应用的工作区也不因"换了设备"被重建。', + '', + '正式版存系统钥匙串 (macOS 钥匙串 / Windows 凭据管理器 / Linux Secret Service)。开发版', + '存执行器数据目录下的文件: macOS 钥匙串按程序的确切构建认"谁能读", 没有苹果开发者身份的', + '程序每重编一次就被当成新程序, 反复弹授权框。', + ].join('\n')) + }) + + it('隔着空行、隔着语句的注释各是一块,行尾的 // 不单独成块', async () => { + const source = [ + '// 第一段', + '// 缩进四格的代码', + '', + '// 第二段', + 'use std::io; // 读写', + '// 第三段', + 'const N: u8 = 1;', + '', + ].join('\n') + const list = await views(source) + expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([ + ['comment', 1, 2], + ['comment', 4, 4], + ['import', 5, 5], + ['comment', 6, 6], + ['const', 7, 7], + ]) + expect(markdown(list[0])).toBe('第一段\n 缩进四格的代码') + }) + + it('/// 并进下一个函数当说明,//! 连着几行合成一块,// 和 /// 不合在一起', async () => { + const source = [ + '//! 本模块管凭证。', + '//!', + '//! 第二段。', + '', + '// 普通注释', + '/// 读出这个账号的凭证。', + '/// 没存过时是 None。', + '#[inline]', + 'pub fn load() -> Option { None }', + '', + ].join('\n') + const list = await views(source) + expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([ + ['comment', 1, 3], + ['comment', 5, 5], + ['function', 6, 9], + ]) + expect(markdown(list[0])).toBe('本模块管凭证。\n\n第二段。') + expect(viewLines([list[2]!])).toContain(' 说明: 读出这个账号的凭证。') + }) + + it('在 mod、impl 身体里同一套', async () => { + const source = [ + 'mod store {', + ' // 一', + ' // 二', + ' fn a() {}', + '}', + 'impl Store {', + ' // 三', + ' //', + ' // 四', + ' fn b(&self) {}', + '}', + '', + ].join('\n') + const list = await views(source) + const mod = list.find(block => block.kind === 'module') + const impl = list.find(block => block.kind === 'rust.impl') + expect(markdown(mod?.children[0])).toBe('一\n二') + expect(markdown(impl?.children.find(block => block.kind === 'comment'))).toBe('三\n\n四') + }) +}) diff --git a/vscode/src/codeReader/languages/rust.ts b/vscode/src/codeReader/languages/rust.ts index b3f1f35..bbf1342 100644 --- a/vscode/src/codeReader/languages/rust.ts +++ b/vscode/src/codeReader/languages/rust.ts @@ -1,7 +1,8 @@ -import type { Access, ClassBlock, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, ModuleBlock, Node, ParameterFacts, TokenRole, TraitBlock, VariableBlock } from '../contract' +import type { Access, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, 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, named } from './shared' +import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named } from './shared' const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let']) @@ -10,7 +11,7 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter { id: 'rust', languageIds: ['rust'], grammarPath: () => path.join(wasmDir, 'tree-sitter-rust.wasm'), - toBlocks: root => topBlocks(named(root)), + toBlocks: root => topBlocks(named(root), -1), tokenRole: rustTokenRole, } } @@ -23,37 +24,28 @@ function unwrapExtern(item: Node): Node[] { return body ? named(body).flatMap(unwrapExtern) : [] } -function topBlocks(items: Node[]): CodeBlock[] { - const list = items.flatMap(unwrapExtern) - const blocks: CodeBlock[] = [] - let index = 0 - while (index < list.length) { - const item = list[index] - if (isDoc(item)) { - const docs = [item] - let next = index + 1 - while (next < list.length && isDoc(list[next]) && list[next].startPosition.row <= docs[docs.length - 1].endPosition.row + 1) - docs.push(list[next++]) - while (next < list.length && list[next].type === 'attribute_item') - next++ - const target = list[next] - if (target && target.type !== 'line_comment' && target.type !== 'block_comment' && target.type !== 'empty_statement') { - blocks.push(withPreamble(itemBlock(target), docs)) - index = next + 1 - continue - } - } - if (item.type === 'attribute_item' || item.type === 'empty_statement') { - index++ - continue - } - if (item.type === 'line_comment' || item.type === 'block_comment') - blocks.push(leafBlock('comment', item)) - else - blocks.push(withPreamble(itemBlock(item), [])) - index++ - } - return blocks +/** + * `//` 普通注释、讲所在模块的 `//!`、讲下一项的 `///` 各是一种写法,只和同一种连成一块。 + * `///` 和 `/**` 开头的块注释是外部文档注释,讲紧跟着的那一项(中间可以隔着 `#[…]`)。 + */ +const COMMENTS: CommentRules = { + lineStyle: comment => comment.type === 'line_comment' ? `//${docMarker(comment)}` : undefined, + isDoc: comment => named(comment).some(child => child.type === 'outer_doc_comment_marker'), + documents: new Set(['function', 'class', 'trait', 'impl', 'module', 'enum', 'const', 'variable']), +} + +/** 文档注释记号:外部的 `/`(块注释是 `*`)、内部的 `!`;普通注释没有。 */ +function docMarker(comment: Node): string { + return named(comment).find(child => child.type === 'outer_doc_comment_marker' || child.type === 'inner_doc_comment_marker')?.text ?? '' +} + +/** 一层的各项(文件顶层、模块身体、impl 身体)→ 块;afterRow 同 listBlocks。`#[…]` 和多出来的空 `;` 不出块。 */ +function topBlocks(items: Node[], afterRow: number): CodeBlock[] { + return listBlocks(items.flatMap(unwrapExtern), afterRow, (item) => { + if (item.type === 'attribute_item' || item.type === 'empty_statement') + return [] + return [withAttributes(itemBlock(item))] + }, COMMENTS) } function itemBlock(item: Node): CodeBlock { @@ -90,7 +82,7 @@ function moduleBlock(node: Node): CodeBlock { const { access, attributes } = visibilityFacts(node) return { kind: 'module', - ...compoundShape(node, body.startIndex, named(body), items => topBlocks(items)), + ...compoundShape(node, body.startIndex, named(body), topBlocks), ...(attributes && { attributes }), facts: classFacts(node, access), } @@ -112,25 +104,10 @@ function valueBlock(node: Node, kind: 'const' | 'variable'): CodeBlock { } } -/** 文档注释和写在前面的属性算进头部。文档注释的第一句是说明。 */ -function withPreamble(block: CodeBlock, docs: Node[]): CodeBlock { - const [doc] = docs - const attribute = leadingAttributes(block.node)[0] - let next = block - if (doc && isDocumentedKind(next)) - next = { ...extendHeader(next, doc), facts: { ...next.facts, doc } } as typeof next - const from = doc ?? attribute - if (from && next.header.startIndex > from.startIndex) - next = extendHeader(next, from) - return next -} - -function isDocumentedKind(block: CodeBlock): block is FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | VariableBlock { - return block.kind === 'function' || block.kind === 'class' || block.kind === 'trait' || block.kind === 'impl' || block.kind === 'module' || block.kind === 'enum' || block.kind === 'const' || block.kind === 'variable' -} - -function isDoc(node: Node): boolean { - return (node.type === 'line_comment' || node.type === 'block_comment') && named(node).some(child => child.type === 'outer_doc_comment_marker') +/** 写在前面的 `#[…]` 算进头部。 */ +function withAttributes(block: CodeBlock): CodeBlock { + const [attribute] = leadingAttributes(block.node) + return attribute && block.header.startIndex > attribute.startIndex ? extendHeader(block, attribute) : block } function useBlock(node: Node): ImportBlock { @@ -224,10 +201,9 @@ function traitBlock(node: Node): TraitBlock { function implBlock(node: Node): ImplBlock { const { access, attributes } = visibilityFacts(node) const body = node.childForFieldName('body') - const children = body ? topBlocks(named(body)) : [] return { kind: 'impl', - ...compoundShape(node, body?.startIndex ?? node.endIndex, [], () => children), + ...compoundShape(node, body?.startIndex ?? node.endIndex, named(body), topBlocks), ...(attributes && { attributes }), facts: classFacts(node, access, implType(node)), } diff --git a/vscode/src/codeReader/languages/shared.ts b/vscode/src/codeReader/languages/shared.ts index c61fa93..75b9aae 100644 --- a/vscode/src/codeReader/languages/shared.ts +++ b/vscode/src/codeReader/languages/shared.ts @@ -24,6 +24,20 @@ export function nodeSpan(node: Node): SourceSpan { } } +const COMMENT_TYPES = new Set(['comment', 'line_comment', 'block_comment']) + +/** 注释节点:多数语法包叫 comment,Rust 分行注释 line_comment 和块注释 block_comment。 */ +export function isCommentNode(node: Node): boolean { + return COMMENT_TYPES.has(node.type) +} + +/** 注释节点的范围,不算末尾的换行:Rust 的 `///`、`//!` 把行尾换行也算进节点,不去掉就多占下一行。 */ +function commentNodeSpan(comment: Node): SourceSpan { + const span = nodeSpan(comment) + const newline = /\r?\n$/.exec(comment.text)?.[0].length ?? 0 + return newline ? { ...span, endIndex: span.endIndex - newline, endRow: span.endRow - 1 } : span +} + export function leafShape(node: Node): BlockShape { return { node, header: nodeSpan(node), children: [] } } @@ -40,7 +54,7 @@ export function extendHeader(block: B, from: Node): B { function lastLeafBefore(node: Node, limit: number): Node | undefined { for (let i = node.childCount - 1; i >= 0; i--) { const child = node.child(i) - if (!child || child.startIndex >= limit || child.type === 'comment' || child.startIndex === child.endIndex) + if (!child || child.startIndex >= limit || isCommentNode(child) || child.startIndex === child.endIndex) continue if (child.childCount === 0) return child @@ -68,7 +82,7 @@ export function headerSpan(outer: Node, bodyStart: number): SourceSpan { function commentsIn(node: Node, from: number, to: number): Node[] { if (node.endIndex <= from || node.startIndex >= to) return [] - if (node.type === 'comment') + if (isCommentNode(node)) return node.startIndex >= from && node.endIndex <= to ? [node] : [] return children(node).flatMap(child => commentsIn(child, from, to)) } @@ -87,21 +101,33 @@ export function compoundShape(outer: Node, bodyStart: number, items: Node[], lis /** 语言怎么认注释。 */ export interface CommentRules { - /** 行注释:相邻几行合成一块。 */ - isLine: (comment: Node) => boolean - /** 文档注释(JSDoc):紧贴在函数、类块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc。 */ + /** + * 行注释的写法(`//`、`#`),块注释没有:相邻行同一写法的行注释合成一块。 + * 写法不同的不合,例如 Rust 的 `//`、讲下一项的 `///`、讲所在模块的 `//!` 各是一种。 + */ + lineStyle: (comment: Node) => string | undefined + /** + * 文档注释(JSDoc、Rust 的 `///`):紧贴在 documents 这几种块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc; + * 连着几行的行文档注释合成一段,第一条注释节点是说明。 + */ isDoc: (comment: Node) => boolean + /** 文档注释能当哪几种块的说明。 */ + documents: ReadonlySet } -/** 函数、类块带上说明:说明进 facts.doc,头部往上延到它开头。 */ +/** 块带上说明:说明进 facts.doc,头部往上延到它开头。 */ function documented(block: B, doc: Node): B { return { ...extendHeader(block, doc), facts: { ...block.facts, doc } } } +function documentable(block: CodeBlock, kinds: ReadonlySet): block is DocumentedBlock { + return kinds.has(block.kind) +} + /** - * 一串语句 → 块。注释单独成块,相邻行的行注释合成一块; + * 一串语句 → 块。注释单独成块,相邻行同一写法的行注释合成一块(见 CommentRules.lineStyle); * 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。 - * 文档注释和下一条语句之间只有空白、下一条语句是函数或类块时,它是这个函数、类的说明,不单独成块; + * 文档注释(连着几行的算一段)和下一条语句之间只有空白、下一条语句是 CommentRules.documents 里的块时,它是这一块的说明,不单独成块; * 中间隔着的装饰器(不出块的语句)不算隔开。 * afterRow 是这串语句之前的最后一行(头部结尾行),模块顶层传 -1; * toBlocks 可以返回空数组:这条语句不出块(装饰器、并进上一块的属性 docstring),和它同一行的注释照样算行尾注释。 @@ -114,46 +140,53 @@ export function listBlocks( ): CodeBlock[] { const out: CodeBlock[] = [] let lastRow = afterRow - let lineRun: CodeBlock | undefined - let doc: Node | undefined + let run: { block: CodeBlock, style: string } | undefined + /** 还没定归属的文档注释:下一条语句能带说明就是它的说明,不然单独成块。 */ + let doc: CodeBlock | undefined const flushDoc = (): void => { if (doc) - out.push(leafBlock('comment', doc)) + out.push(doc) doc = undefined } + /** 接在 run 下一行、写法相同的行注释。 */ + const continues = (comment: Node): boolean => + !!run && comments.lineStyle(comment) === run.style && comment.startPosition.row === run.block.header.endRow + 1 items.forEach((item, i) => { - if (item.type === 'comment') { - if (item.startPosition.row === lastRow) + if (isCommentNode(item)) { + const span = commentNodeSpan(item) + if (span.startRow === lastRow) return - flushDoc() - const line = comments.isLine(item) - const next = items[i + 1] - if (line && lineRun && item.startPosition.row === lineRun.header.endRow + 1) { - lineRun.header = { ...lineRun.header, endIndex: item.endIndex, endRow: item.endPosition.row } - } - else if (comments.isDoc(item) && next && next.type !== 'comment' && sameNode(item.nextSibling, next)) { - doc = item - lineRun = undefined + if (run && continues(item)) { + run.block.header = { ...run.block.header, endIndex: span.endIndex, endRow: span.endRow } } else { - const block = leafBlock('comment', item) - out.push(block) - lineRun = line ? block : undefined - } - } - else { - const blocks = toBlocks(item) - const [head] = blocks - if ((head?.kind === 'function' || head?.kind === 'class' || head?.kind === 'enum') && doc) { - blocks[0] = documented(head, doc) - doc = undefined - } - else if (head) { flushDoc() + const block: CodeBlock = { kind: 'comment', node: item, header: span, children: [] } + const style = comments.lineStyle(item) + run = style === undefined ? undefined : { block, style } + if (comments.isDoc(item)) + doc = block + else + out.push(block) } - out.push(...blocks) - lineRun = undefined + // 文档注释后面紧跟的得是下一条语句或它自己的下一行,隔着别的注释就只是一块注释 + const next = items[i + 1] + if (!next || !sameNode(item.nextSibling, next) || (isCommentNode(next) && !continues(next))) + flushDoc() + lastRow = span.endRow + return } + const blocks = toBlocks(item) + const [head] = blocks + if (head && doc && documentable(head, comments.documents)) { + blocks[0] = documented(head, doc.node) + doc = undefined + } + else if (head) { + flushDoc() + } + out.push(...blocks) + run = undefined lastRow = item.endPosition.row }) flushDoc() @@ -194,10 +227,10 @@ function leadingComments(children: readonly CodeBlock[], child: CodeBlock): Code /** 注释块里的各条注释(几条行注释连成的一块是好几个节点);Python 当注释用的字符串是那个字符串。 */ function commentNodes(block: CodeBlock): Node[] { - if (block.node.type !== 'comment') + if (!isCommentNode(block.node)) return syntaxChildren(block.node).slice(0, 1) const nodes: Node[] = [] - for (let node: Node | null = block.node; node?.type === 'comment' && node.endIndex <= block.header.endIndex; node = node.nextSibling) + for (let node: Node | null = block.node; node && isCommentNode(node) && node.startIndex < block.header.endIndex; node = node.nextSibling) nodes.push(node) return nodes } @@ -238,7 +271,7 @@ function fieldDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] { const doc = ownDoc(block) return [ ...leadingComments(children, block).flatMap(commentNodes), - ...(trailing?.type === 'comment' && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []), + ...(trailing && isCommentNode(trailing) && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []), ...(doc ? [doc] : []), ] } diff --git a/vscode/src/codeReader/languages/typescript.ts b/vscode/src/codeReader/languages/typescript.ts index 70e66aa..6542591 100644 --- a/vscode/src/codeReader/languages/typescript.ts +++ b/vscode/src/codeReader/languages/typescript.ts @@ -19,9 +19,10 @@ const LOOPS = new Set(['for_statement', 'for_in_statement', 'while_statement', ' const CLASSES = new Set(['class_declaration', 'abstract_class_declaration', 'class']) const COMMENTS: CommentRules = { - isLine: comment => comment.text.startsWith('//'), + lineStyle: comment => comment.text.startsWith('//') ? '//' : undefined, // `/**/` 是空的块注释,不是 JSDoc isDoc: comment => comment.text.startsWith('/**') && !comment.text.startsWith('/**/'), + documents: new Set(['function', 'class', 'enum']), } /** 带语句块主体的函数值(箭头函数 / 函数表达式)的主体。 */ diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts index 0b89988..b73462c 100644 --- a/vscode/src/codeReader/translators/rustZh.ts +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -104,9 +104,9 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] { } } +/** 行注释去掉 `//`(文档注释连 `/`、`!`)和后面一个空格,缩进留给 Markdown;块注释去掉开头结尾的注释符号。 */ function commentText(node: Node, text: string): string { - const doc = node.childForFieldName('doc') - if (doc) - return doc.text.trim() - return text.replace(/^\/\/[/!]?\s?/, '').replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '').trim() + if (node.type === 'line_comment') + return text.replace(/^\/\/[/!]?\s?/, '').replace(/\r?\n$/, '') + return text.replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '').trim() } diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index f25840e..23cded6 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -133,14 +133,6 @@ function firstLeaf(node: Node): Node { return first ? firstLeaf(first) : node } -function commentsWithin(node: Node, span: SourceSpan): Node[] { - if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex) - return [] - if (node.type === 'comment') - return [node] - return node.children.flatMap(child => child ? commentsWithin(child, span) : []) -} - /** 节点的源码去掉整个落在它里面的记号(中文名记号 `@Comment 当前用户` 不是说明文字)。 */ function withoutMarkers(node: Node, markers: readonly SourceRange[]): string { let text = '' @@ -639,10 +631,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator { return table.commentText(node, withoutMarkers(node, markers)) } + /** 注释块的原文:几条行注释连成的一块是从 block.node 起、开头落在头部里的各个兄弟节点。 */ function translateComment(block: TextBlock, document: SourceDocument): Markdown { - const { startIndex, endIndex } = block.header - const root = block.node.tree.rootNode - const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node] + const nodes: Node[] = [] + for (let node: Node | null = block.node; node && node.startIndex < block.header.endIndex; node = node.nextSibling) + nodes.push(node) return { markdown: docText(nodes.map(node => commentOf(node, document))) } }