🐛 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
+233 -121

No files matched your search

+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四')
})
})
+32 -56
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
}
}
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)),
}
+73 -40
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
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] : []),
]
}
@@ -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))) }
}