🐛 fix(vscode): Rust 连着写的行注释合成一块注释,和别的语言走同一套拆块
This commit is contained in:
1 parent
51da724e68
commit
2f610e9cb8
11 files changed
+224
-112
No files matched your search
@@ -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 着色。
|
||||
|
||||
@@ -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;
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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四')
|
||||
})
|
||||
})
|
||||
@@ -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)),
|
||||
}
|
||||
|
||||
@@ -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']),
|
||||
}
|
||||
|
||||
/** 带语句块主体的函数值(箭头函数 / 函数表达式)的主体。 */
|
||||
|
||||
@@ -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))) }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user