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

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

No files matched your search

+3 -1
View File
@@ -428,6 +428,7 @@ export interface DocumentViews {
* 命令名的命令规则(SourceDocument.commandRule)。
*/
export interface DocumentExtras {
fileName?: SourceDocument['fileName']
rules?: RuleRun
calleeKind?: SourceDocument['calleeKind']
commandRule?: SourceDocument['commandRule']
@@ -467,7 +468,7 @@ export async function readBlockViews(
parts.set(code.node.startIndex, { code, root, document, translator: language.translator, steps })
}
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
const { calleeKind, commandRule } = extras
const { fileName, calleeKind, commandRule } = extras
const readings = new Map<number, EmbeddedReading>()
const embedded = (node: Node): EmbeddedReading | undefined => {
const part = parts.get(node.startIndex)
@@ -481,6 +482,7 @@ export async function readBlockViews(
source,
language: adapter,
markers,
...(fileName && { fileName }),
...(calleeKind && { calleeKind }),
...(commandRule && { commandRule }),
...(parts.size > 0 && { embedded }),
+37 -3
View File
@@ -12,7 +12,7 @@ export type { Node }
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module'
| 'trait' | 'impl' | 'module' | 'attribute'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
@@ -214,6 +214,35 @@ export interface ClassFacts {
implements: Node[]
}
/** Rust 的内部属性 `#![…]`:`cfg_attr(条件, a, b)` 拆成生效条件和 a、b 两个属性,别的属性就是它自己一个。 */
export interface AttributeFacts {
/** 生效的各个属性,照写的顺序。 */
attributes: AttributeItem[]
/** `cfg_attr` 的生效条件;别的属性没有。 */
condition?: CfgPredicate
/** 写在哪一层:文件顶层、`mod` 身体、`impl` 身体;别处(`extern` 块里)没有。作用范围按它定,文件顶层还要看是不是 crate 根文件。 */
placement?: 'file' | 'module' | 'impl'
}
/** 一个属性:`名字`、`名字 = 值` 或 `名字(各项)`。 */
export interface AttributeItem {
/** 整个属性(名字连参数)在源码里的范围,预览照原文着色。 */
span: SourceSpan
/** 名字,路径照写(`rustfmt::skip`)。 */
name: string
/** `名字 = 值` 的值:字符串去掉引号,别的照原文。 */
value?: string
/** `名字(…)` 括号里逗号隔开的各项,照原文(`clippy::too_many_arguments` 是一项);没写括号是空的。 */
arguments: string[]
}
/** `cfg` 条件:`not(…)`、`all(…)`、`any(…)`,一个配置项(`test`、`target_os = "windows"`),认不出的写法照原文。 */
export type CfgPredicate
= | { kind: 'not', operand: CfgPredicate }
| { kind: 'all' | 'any', operands: CfgPredicate[] }
| { kind: 'option', name: string, value?: string }
| { kind: 'other', text: string }
/** 枚举的一个成员;节点都指向源码。 */
export interface EnumMemberFacts {
name: Node
@@ -480,6 +509,8 @@ export interface TraitBlock extends BlockShape { kind: 'trait', facts: ClassFact
/** Rust 的 impl:标题是「某类型 的方法」,方法是子块,放进方法表的某一行。 */
export interface ImplBlock extends BlockShape { kind: 'impl', facts: ClassFacts }
export interface ModuleBlock extends BlockShape { kind: 'module', facts: ClassFacts }
/** Rust 的内部属性 `#![…]`:写给所在 crate 或模块的编译属性。 */
export interface AttributeBlock extends BlockShape { kind: 'attribute', facts: AttributeFacts }
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
/** 异常块的头部是整个类(类属性都在属性表里讲),留下的方法(含构造函数)等是子块。 */
@@ -497,7 +528,7 @@ export interface StageBlock extends BlockShape { kind: 'stage', facts: StageFact
export interface HealthBlock extends BlockShape { kind: 'health', facts: HealthFacts }
export interface StartBlock extends BlockShape { kind: 'start', facts: StartFacts }
export type FactBlock
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | AttributeBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
| DirectiveBlock | StageBlock | HealthBlock | StartBlock
/** 带说明(facts.doc)的块。 */
@@ -695,6 +726,8 @@ export interface DisplayNameReader {
export interface SourceDocument {
source: string
language: LanguageAdapter
/** 文件路径(VS Code 的 TextDocument.fileName);宿主没给(嵌着的代码、只读一段源码)时没有,按文件名定的说法写成判断不了的那种。 */
fileName?: string
/** 写在注释里给代码阅读看的记号(中文名记号,见 DisplayNameFacts.markers):不是说明文字,翻注释、说明时去掉 */
markers: SourceRange[]
/**
@@ -743,7 +776,7 @@ export interface ThemeColor { dark: string, light: string }
/**
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 26 个给内置种类(几种共用一个:
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置共用 sliders-horizontal,字段、标签共用 tag),
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置、编译属性共用 sliders-horizontal,字段、标签共用 tag),
* 宿主标的入口块也用这组里的 play(entries/entryMarks.ts 的 ENTRY_CONCEPT),
* 后面的给库规则的种类(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出也在里面);库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),
* 没有按任意名字找图标的路。
@@ -804,6 +837,7 @@ export const KIND_ICONS: Record<BlockKind, KindIconName> = {
trait: 'network',
impl: 'square-function',
module: 'layers',
attribute: 'sliders-horizontal',
type: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
@@ -13,6 +13,8 @@ export interface DocumentSnapshot {
languageId: string
version: number
getText: () => string
/** 文件路径,交给翻译插件(SourceDocument.fileName) */
fileName?: string
/** 这份文件是项目写明的入口时,预览里名字对上的块标成入口(见 entries/entryPoints.ts) */
entryNames?: readonly string[]
}
@@ -57,6 +59,7 @@ export async function documentMessage(
try {
const rules = registry.rulesFor(languageId)
const { blocks, displayNames, symbols } = await readBlockViews(runtime, adapter, translator, languageId, source, {
...(document.fileName && { fileName: document.fileName }),
rules: { rules, report, lookup },
calleeKind,
commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report),
@@ -328,7 +328,7 @@ class Preview {
const source = this.document.getText()
const session = this.lookups.session()
const callees = this.callees.session()
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
}, session.lookup, callees.calleeKind)
if (request !== this.latest || !this.ready)
@@ -356,7 +356,7 @@ class Preview {
*/
private async readOld(source: string): Promise<OldDocument | undefined> {
const { message } = await documentMessage(
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) },
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) },
this.registry,
this.runtime,
() => {},
+68 -2
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../blockView'
import { blockMarks } from '../diff/viewDiff'
import { rustImplRule } from '../rules/rustImpl'
import { isCrateRoot } from '../rules/rustTree'
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { createRustAdapter } from './rust'
@@ -10,9 +11,9 @@ import { createRustAdapter } from './rust'
const adapter = createRustAdapter(WASM_DIR)
const sample = readFixture('rustLang.rs')
function views(source: string): Promise<BlockView[]> {
function views(source: string, fileName?: string): Promise<BlockView[]> {
return withBlocks(adapter, 'rust', source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, {
rules: [rustImplRule],
report: () => {},
}))
@@ -270,3 +271,68 @@ describe('rust 注释', () => {
expect(markdown(impl?.children.find(block => block.kind === 'comment'))).toBe('三\n\n四')
})
})
describe('rust 编译属性', () => {
it('tauri 的 main.rs 开头的 cfg_attr:属性、生效条件、作用范围、说明', async () => {
const source = [
'// Prevents additional console window on Windows in release, DO NOT REMOVE!!',
'#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]',
'',
'fn main() {}',
'',
].join('\n')
const list = await views(source, '/repo/apps/desktop/src-tauri/src/main.rs')
expect(list.map(block => block.kind)).toEqual(['comment', 'attribute', 'function'])
expect(viewLines([card(list, 'attribute')])).toEqual([
'attribute 2',
' 属性: windows_subsystem = "windows"',
' 生效条件: 非 调试构建',
' 作用范围: 整个 crate',
' 说明: 在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口',
])
const attribute = card(list, 'attribute')
const value = 'properties' in attribute ? attribute.properties[0]?.value : undefined
expect(Array.isArray(value) && value.map(piece => 'role' in piece && piece.role)).toEqual(['variable', 'plain', 'plain', 'plain', 'string', 'string', 'string'])
})
it('allow 列出各个 lint,别的文件顶层是本模块', async () => {
const list = await views('#![allow(dead_code, unused_imports)]\n', '/repo/src/credentials.rs')
expect(viewLines(list)).toEqual([
'attribute 1',
' 属性: allow(dead_code, unused_imports)',
' 作用范围: 本模块',
' 说明: 编译检查(lint)级别设为放行:dead_code、unused_imports',
])
})
it('认不得的属性没有说明,不知道文件名时作用范围是所在模块', async () => {
expect(viewLines(await views('#![rustfmt::skip]\n'))).toEqual([
'attribute 1',
' 属性: rustfmt::skip',
' 作用范围: 所在模块',
])
})
it('cfg_attr 写了几个属性是一张表;all、any 套着的条件加括号;mod 身体里是本模块', async () => {
const source = [
'mod platform {',
' #![cfg_attr(all(unix, not(any(test, feature = "mock"))), allow(clippy::foo), deny(warnings))]',
'}',
'',
].join('\n')
const mod = card(await views(source, '/repo/src/main.rs'), 'module')
expect(viewLines(mod.children)).toEqual([
'attribute 2',
' 属性: 2 个',
' allow(clippy::foo) | 编译检查(lint)级别设为放行:clippy::foo',
' deny(warnings) | 编译检查(lint)级别设为报错:warnings',
' 生效条件: unix 并且 非 (测试构建 或者 启用特性 mock)',
' 作用范围: 本模块',
])
})
it('crate 根文件:main.rs、lib.rs、src/bin 下的、包根 tests / examples / benches 里直接放的', () => {
expect(['/p/src/main.rs', '/p/src/lib.rs', '/p/src/bin/tool.rs', '/p/tests/api.rs', 'C:\\p\\examples\\demo.rs'].map(isCrateRoot)).toEqual([true, true, true, true, true])
expect(['/p/src/credentials.rs', '/p/src/tests/helpers.rs', '/p/src/bin/tool/args.rs'].map(isCrateRoot)).toEqual([false, false, false])
})
})
+107 -2
View File
@@ -1,8 +1,8 @@
import type { Access, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, TokenRole, TraitBlock } from '../contract'
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named } from './shared'
import { children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
@@ -69,6 +69,8 @@ function itemBlock(item: Node): CodeBlock {
case 'function_item':
case 'function_signature_item':
return functionBlock(item)
case 'inner_attribute_item':
return innerAttributeBlock(item)
default:
return leafBlock('statement', item)
}
@@ -104,6 +106,109 @@ function valueBlock(node: Node, kind: 'const' | 'variable'): CodeBlock {
}
}
const PLACEMENTS: Record<string, AttributeFacts['placement']> = { source_file: 'file', mod_item: 'module', impl_item: 'impl' }
/** `#![…]`:写给所在 crate 或模块的编译属性。`cfg_attr(条件, a, b)` 拆成生效条件和 a、b。 */
function innerAttributeBlock(node: Node): CodeBlock {
const attribute = named(node).find(child => child.type === 'attribute')
if (!attribute)
return leafBlock('statement', node)
const meta = metaItem(tokens(attribute))
const [condition, ...applied] = meta.name === 'cfg_attr' ? meta.list ?? [] : []
const conditional = condition !== undefined && applied.length > 0
const container = node.parent?.type === 'source_file' ? node.parent : node.parent?.parent
const placement = container ? PLACEMENTS[container.type] : undefined
return {
kind: 'attribute',
...leafShape(node),
facts: {
attributes: conditional ? applied.map(item => attributeItem(metaItem(item))) : [attributeItem(meta)],
...(conditional && { condition: cfgPredicate(condition) }),
...(placement && { placement }),
},
}
}
/** 属性的写法:名字(路径),后面 `= 值` 或括号里逗号隔开的各段。tokens 是这一段的各个词。 */
interface MetaItem {
tokens: Node[]
name: string
value?: Node
list?: Node[][]
}
function metaItem(words: Node[]): MetaItem {
const at = words.findIndex(word => word.type === '=' || word.type === 'token_tree')
const path = at < 0 ? words : words.slice(0, at)
const mark = words[at]
return {
tokens: words,
name: textOf(path),
...(mark?.type === '=' && words[at + 1] && { value: words[at + 1] }),
...(mark?.type === 'token_tree' && { list: listItems(mark) }),
}
}
/** 括号里逗号隔开的各段,每段是它的各个词。 */
function listItems(tree: Node): Node[][] {
const items: Node[][] = [[]]
for (const word of tokens(tree).slice(1, -1)) {
if (word.type === ',')
items.push([])
else
items[items.length - 1].push(word)
}
return items.filter(item => item.length > 0)
}
function attributeItem(meta: MetaItem): AttributeItem {
return {
span: spanOf(meta.tokens),
name: meta.name,
...(meta.value && { value: literalText(meta.value) }),
arguments: (meta.list ?? []).map(textOf),
}
}
function cfgPredicate(words: Node[]): CfgPredicate {
const meta = metaItem(words)
if (meta.list && (meta.name === 'all' || meta.name === 'any'))
return { kind: meta.name, operands: meta.list.map(cfgPredicate) }
if (meta.list?.length === 1 && meta.name === 'not')
return { kind: 'not', operand: cfgPredicate(meta.list[0]) }
if (!meta.list && words[0]?.type === 'identifier' && (words.length === 1 || meta.value))
return { kind: 'option', name: meta.name, ...(meta.value && { value: literalText(meta.value) }) }
return { kind: 'other', text: textOf(words) }
}
/** 节点的各个词(含标点),注释不算。 */
function tokens(node: Node): Node[] {
return children(node).filter(child => !isCommentNode(child))
}
/** 连着的几个词在源码里的原文(它们是同一个节点的子节点)。 */
function textOf(words: Node[]): string {
const [first] = words
const last = words[words.length - 1]
const parent = first?.parent
if (!first || !last || !parent)
return ''
return parent.text.slice(first.startIndex - parent.startIndex, last.endIndex - parent.startIndex)
}
function spanOf(words: Node[]): SourceSpan {
const first = words[0]
const last = words[words.length - 1]
return { startIndex: first.startIndex, endIndex: last.endIndex, startRow: first.startPosition.row, endRow: last.endPosition.row }
}
/** 字面量的值:字符串去掉引号,别的照原文。 */
function literalText(node: Node): string {
if (node.type !== 'string_literal')
return node.text
return named(node).find(child => child.type === 'string_content')?.text ?? ''
}
/** 写在前面的 `#[…]` 算进头部。 */
function withAttributes(block: CodeBlock): CodeBlock {
const [attribute] = leadingAttributes(block.node)
+100 -1
View File
@@ -1,4 +1,4 @@
import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, TokenRole, TraitBlock } from '../contract'
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { RustVisibility } from './rustTree'
import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
@@ -72,6 +72,97 @@ export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] {
]
}
const LINT_LEVELS: Record<string, string> = { allow: '放行', warn: '警告', deny: '报错', forbid: '强制报错(内层不能再放宽)' }
/**
* 编译属性 `#![…]`:属性(照原文着色)/ 生效条件(只有 cfg_attr)/ 作用范围 / 说明(认得的属性才有)。
* cfg_attr 里写了几个属性时「属性」是一张「属性 | 说明」表,不再另起「说明」一行。
* original 把源码的一段照原文着色;crateRoot 是这份文件是不是 crate 根文件,不知道文件名时没有。
*/
export function attributeRows(block: AttributeBlock, original: (span: SourceSpan) => Inline[], crateRoot: boolean | undefined): PropertyRow[] {
const { attributes, condition, placement } = block.facts
const single = attributes.length === 1 ? attributes[0] : undefined
const description = single && attributeNote(single)
return [
single
? { label: '属性', value: original(single.span) }
: {
label: '属性',
value: [hint(`${attributes.length} 个`)],
columns: ['属性', '说明'],
items: attributes.map(item => ({ cells: [original(item.span), attributeNote(item) ?? [hint('—')]] })),
},
...(condition ? [{ label: '生效条件', value: cfgText(condition, false) }] : []),
{ label: '作用范围', value: [keyword(attributeScope(placement, crateRoot))] },
...(description ? [{ label: '说明', value: description }] : []),
]
}
function attributeScope(placement: AttributeFacts['placement'], crateRoot: boolean | undefined): string {
switch (placement) {
case 'file':
return crateRoot === undefined ? '所在模块' : crateRoot ? '整个 crate' : '本模块'
case 'module':
return '本模块'
case 'impl':
return '本 impl 块'
default:
return '所在模块'
}
}
/** 认得的属性的书面说明;认不得的没有。 */
function attributeNote(item: AttributeItem): Inline[] | undefined {
const names = item.arguments.flatMap((name, i) => [...(i > 0 ? [note('、')] : []), { text: name, role: 'variable' as const }])
const level = LINT_LEVELS[item.name]
if (level)
return names.length > 0 ? [note(`编译检查(lint)级别设为${level}:`), ...names] : undefined
switch (item.name) {
case 'windows_subsystem':
if (item.value === 'windows')
return [note('在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口')]
return item.value === 'console' ? [note('在 Windows 上编译为控制台程序(console 子系统),启动时打开控制台窗口')] : undefined
case 'feature':
return names.length > 0 ? [note('启用实验特性(需 nightly 编译器):'), ...names] : undefined
case 'no_std':
return [note('不链接标准库 std,只使用核心库 core')]
case 'no_main':
return [note('不使用标准的 main 入口函数,程序入口另行提供')]
case 'recursion_limit':
return item.value ? [note(`宏展开等编译期递归的深度上限设为 ${item.value}(默认 128)`)] : undefined
default:
return undefined
}
}
const CFG_OPTIONS: Record<string, string> = { debug_assertions: '调试构建', test: '测试构建' }
const CFG_VALUES: Record<string, string> = { target_os: '目标系统', feature: '启用特性' }
/** cfg 条件读成中文:not 是「非」,all 用「并且」、any 用「或者」连起来,套在别的条件里的加括号;认不得的照原文。 */
function cfgText(predicate: CfgPredicate, nested: boolean): Inline[] {
switch (predicate.kind) {
case 'not':
return [operator('非'), plain(' '), ...cfgText(predicate.operand, true)]
case 'all':
case 'any': {
if (predicate.operands.length === 0)
return [hint(predicate.kind === 'all' ? '总是成立' : '从不成立')]
const joiner = operator(predicate.kind === 'all' ? '并且' : '或者')
const parts = predicate.operands.flatMap((operand, i) => [...(i > 0 ? [plain(' '), joiner, plain(' ')] : []), ...cfgText(operand, true)])
return nested && predicate.operands.length > 1 ? [plain('('), ...parts, plain(')')] : parts
}
case 'option': {
const { name, value } = predicate
if (value === undefined)
return [plain(CFG_OPTIONS[name] ?? name)]
const label = CFG_VALUES[name]
return label ? [plain(`${label} `), { text: value, role: 'string' }] : [plain(`${name} = `), { text: `"${value}"`, role: 'string' }]
}
case 'other':
return [plain(predicate.text)]
}
}
export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
return [
nameRow(block.facts.name, say, 'type'),
@@ -178,6 +269,14 @@ function keyword(text: string): Inline {
return { text, role: 'keyword' }
}
function operator(text: string): Inline {
return { text, role: 'operator' }
}
function note(text: string): Inline {
return { text, role: 'comment' }
}
function range(node: import('../contract').Node): { startIndex: number, endIndex: number } {
return { startIndex: node.startIndex, endIndex: node.endIndex }
}
+15
View File
@@ -32,6 +32,21 @@ export function oneLine(text: string): string {
return text.replace(/\s+/g, ' ').trim()
}
const TARGET_DIRS = new Set(['examples', 'tests', 'benches'])
/**
* Cargo 默认认作 crate 根的文件:`main.rs`、`lib.rs`,`src/bin/` 下的 `.rs`,
* 包根下 `examples/`、`tests/`、`benches/` 里直接放的 `.rs`(`src/` 下同名目录里的是模块)。
*/
export function isCrateRoot(fileName: string): boolean {
const [base, parent, grandparent] = fileName.split(/[\\/]/).reverse()
if (base === 'main.rs' || base === 'lib.rs')
return true
if (!base?.endsWith('.rs') || parent === undefined)
return false
return parent === 'bin' ? grandparent === 'src' : TARGET_DIRS.has(parent) && grandparent !== 'src'
}
/** 节点上的可见性。pub(in …) 不在这四档里,按本 crate 算(只本包能用)。 */
export function rustVisibility(node: Node): RustVisibility {
const vis = named(node).find(child => child.type === 'visibility_modifier')
+28 -2
View File
@@ -1,5 +1,6 @@
import type { FactBlock, Node, PropertyRow, SourceDocument, Translator } from '../contract'
import { enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
import type { FactBlock, Inline, Node, PropertyRow, SourceDocument, SourceSpan, Translator } from '../contract'
import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
import { isCrateRoot } from '../rules/rustTree'
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
const say = plainSay()
@@ -96,6 +97,8 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
return implRows(block, say)
case 'module':
return moduleRows(block, say)
case 'attribute':
return attributeRows(block, span => original(block.node, span, document), document.fileName === undefined ? undefined : isCrateRoot(document.fileName))
case 'const':
case 'variable':
return template.describe(block, document)
@@ -104,6 +107,29 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
}
}
/** node 里 span 那一段照原文:叶子按源码着色,叶子之间的空白照抄。 */
function original(node: Node, span: SourceSpan, { source, language }: SourceDocument): Inline[] {
const out: Inline[] = []
let cursor = span.startIndex
const visit = (current: Node): void => {
if (current.endIndex <= span.startIndex || current.startIndex >= span.endIndex || current.startIndex === current.endIndex)
return
if (current.childCount === 0) {
if (current.startIndex > cursor)
out.push({ text: source.slice(cursor, current.startIndex), role: 'plain' })
out.push({ text: current.text, role: language.tokenRole(current) })
cursor = current.endIndex
return
}
for (const child of current.children) {
if (child)
visit(child)
}
}
visit(node)
return out
}
/** 行注释去掉 `//`(文档注释连 `/`、`!`)和后面一个空格,缩进留给 Markdown;块注释去掉开头结尾的注释符号。 */
function commentText(node: Node, text: string): string {
if (node.type === 'line_comment')
@@ -207,6 +207,7 @@ export const KIND_ZH: Record<BlockKind, string> = {
trait: '特质',
impl: '实现',
module: '模块',
attribute: '编译属性',
enum: '枚举',
exception: '异常',
type: '类型',
@@ -722,6 +723,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return healthRows(block.facts, phrases)
case 'start':
return startRows(block.facts, phrases)
// Rust 的编译属性由 Rust 的直译排(translators/rustZh.ts)
case 'attribute':
return []
default:
return block.facts.scope ? scopedVariableRows(block.facts.declarators, block.facts.scope, phrases) : variableRows(block, phrases)
}
@@ -69,6 +69,7 @@ body.vscode-high-contrast-light {
[data-kind="function"] { --kind: var(--cr-kind-function); }
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
[data-kind="trait"] { --kind: var(--cr-storage); }
[data-kind="attribute"] { --kind: var(--cr-constant); }
[data-kind="impl"] { --kind: var(--cr-kind-function); }
[data-kind="enum"] { --kind: var(--cr-kind-enum); }
[data-kind="exception"] { --kind: var(--cr-kind-exception); }
@@ -24,6 +24,7 @@ describe('块的种类图标', () => {
trait: 'network',
impl: 'square-function',
module: 'layers',
attribute: 'sliders-horizontal',
type: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
+1 -1
View File
@@ -4,7 +4,7 @@
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module'
| 'trait' | 'impl' | 'module' | 'attribute'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'