🐛 fix(vscode): Rust 连着写的行注释合成一块注释,和别的语言走同一套拆块

This commit is contained in:
cruldra committed 2026-10-08 07:17:25 +08:00
1 parent 51da724e68
commit 2f610e9cb8
11 files changed
+224 -112

No files matched your search

+11 -4
View File
@@ -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 着色。
+1 -1
View File
@@ -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<B, 'kind' | 'facts'> : never : never
@@ -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;
+2 -1
View File
@@ -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[] {
+1 -1
View File
@@ -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)
+92 -2
View File
@@ -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<String> { 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四')
})
})
+30 -54
View File
@@ -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
/**
* `//` 普通注释、讲所在模块的 `//!`、讲下一项的 `///` 各是一种写法,只和同一种连成一块。
* `///` 和 `/**` 开头的块注释是外部文档注释,讲紧跟着的那一项(中间可以隔着 `#[…]`)。
*/
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 ?? ''
}
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
/** 一层的各项(文件顶层、模块身体、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)),
}
+66 -33
View File
@@ -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<B extends CodeBlock>(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<DocumentedBlock['kind']>
}
/** 函数、类块带上说明:说明进 facts.doc,头部往上延到它开头。 */
/** 块带上说明:说明进 facts.doc,头部往上延到它开头。 */
function documented<B extends DocumentedBlock>(block: B, doc: Node): B {
return { ...extendHeader(block, doc), facts: { ...block.facts, doc } }
}
function documentable(block: CodeBlock, kinds: ReadonlySet<string>): 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
if (run && continues(item)) {
run.block.header = { ...run.block.header, endIndex: span.endIndex, endRow: span.endRow }
}
else {
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
}
else {
const block = leafBlock('comment', item)
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)
lineRun = line ? block : undefined
}
// 文档注释后面紧跟的得是下一条语句或它自己的下一行,隔着别的注释就只是一块注释
const next = items[i + 1]
if (!next || !sameNode(item.nextSibling, next) || (isCommentNode(next) && !continues(next)))
flushDoc()
lastRow = span.endRow
return
}
else {
const blocks = toBlocks(item)
const [head] = blocks
if ((head?.kind === 'function' || head?.kind === 'class' || head?.kind === 'enum') && doc) {
blocks[0] = documented(head, doc)
if (head && doc && documentable(head, comments.documents)) {
blocks[0] = documented(head, doc.node)
doc = undefined
}
else if (head) {
flushDoc()
}
out.push(...blocks)
lineRun = undefined
}
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] : []),
]
}
@@ -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']),
}
/** 带语句块主体的函数值(箭头函数 / 函数表达式)的主体。 */
+4 -4
View File
@@ -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()
}
@@ -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))) }
}