🐛 fix(vscode): Rust 卡片照抄的类型、签名逐个名字带下标,返回类型也能悬停跳转

This commit is contained in:
cruldra committed 2026-10-15 19:48:56 +11:00
1 parent ecb6dc89c6
commit 55595e948c
17 files changed
+220 -122

No files matched your search

+1
View File
@@ -335,6 +335,7 @@ export function toBlockViews(
imports,
...(layerTranslator && { displayLanguage: layerTranslator.displayLanguage }),
translate: node => layerTranslator ? layerTranslator.expression(node, layerDocument) : [{ text: node.text, role: 'plain' }],
verbatim: (node, role) => layerTranslator ? layerTranslator.verbatim(node, layerDocument, role) : [{ text: node.text, role: role ?? 'plain' }],
doc: node => layerTranslator?.doc(node, layerDocument),
lookup: layerRules.lookup ?? (() => undefined),
}
+7
View File
@@ -866,6 +866,11 @@ export interface Translator {
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
/** 一个表达式或类型节点整个翻成一行译文,译法同属性表里的值:库规则翻默认值、类型时借它。 */
expression: (node: Node, document: SourceDocument) => Inline[]
/**
* 一个类型、签名节点照抄成一行原文,不翻关键字:叶子按源码着色(给了 `role` 就都按它),名字各自带源码下标,
* 跨几行写的压成一行、连着的空白收成一个空格。库规则照抄类型、签名时借它。
*/
verbatim: (node: Node, document: SourceDocument, role?: TokenRole) => Inline[]
/**
* 一段说明文字(注释、docstring、当说明用的字符串)读成说明(见 Doc):去掉注释符号、引号和共同缩进后的 Markdown 原文,
* 拆出块标签;正文同属性表的「说明」。几条行文档注释连成一段的(Rust 的 `///`),node 是第一条,翻译插件自己往下连。没有文字时没有。
@@ -1086,6 +1091,8 @@ export interface RuleContext {
displayLanguage?: string
/** 用当前翻译插件把一个表达式或类型节点翻成一行译文(Translator.expression):名字带源码下标,能跳到定义、高亮各处出现;没有翻译插件时照抄源码。 */
translate: (node: Node) => Inline[]
/** 用当前翻译插件把一个类型、签名节点照抄成一行原文([Translator.verbatim]):名字各自带源码下标,能跳到定义、高亮各处出现;没有翻译插件时照抄源码。 */
verbatim: (node: Node, role?: TokenRole) => Inline[]
/** 用当前翻译插件读一段说明文字(Translator.doc):正文和块标签;没有翻译插件、没有文字时没有。 */
doc: (node: Node) => Doc | undefined
/**
+64 -1
View File
@@ -1,11 +1,12 @@
import type { BlockView } from '../blockView'
import type { PreviewText } from '../contract'
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { blockCodes, toBlockViews } from '../blockView'
import { blockMarks } from '../diff/viewDiff'
import { rustImplRule } from '../rules/rustImpl'
import { isCrateRoot } from '../rules/rustTree'
import { commentText, nodeFacts, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { commentText, nodeFacts, outline, previewText, propertyLines, propertyRow, readFixture, typeNames, viewLines, WASM_DIR, withBlocks, wordsOf } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { createRustAdapter } from './rust'
@@ -136,6 +137,68 @@ describe('rust 语言卡片', () => {
})
})
it('照抄的类型逐个名字带自己的下标、按词性着色:参数类型、返回;跨行写的压成一行;没写返回类型的 () 是普通文字', async () => {
const source = [
'fn f(state: State<\'_, AppState>) -> Result<(), String> {}',
'fn g(',
' map: HashMap<',
' String,',
' Vec<u8>,',
' >,',
') -> Option<',
' String,',
'> {}',
'fn h() {}',
'',
].join('\n')
const [f, g, h] = await views(source)
const paramType = (block: BlockView | undefined): PreviewText => propertyRow(block, '输入参数').items?.[0]?.cells[1] ?? []
const returned = (block: BlockView | undefined): PreviewText => propertyRow(block, '返回').value
expect(previewText(paramType(f))).toBe('State<\'_, AppState>')
expect(wordsOf(paramType(f), 'type')).toEqual([['State', source.indexOf('State')], ['AppState', source.indexOf('AppState')]])
expect(previewText(returned(f))).toBe('Result<(), String>')
expect(wordsOf(returned(f), 'type')).toEqual([['Result', source.indexOf('Result')], ['String', source.indexOf('String')]])
expect(previewText(paramType(g))).toBe('HashMap< String, Vec<u8>, >')
expect(previewText(returned(g))).toBe('Option< String, >')
expect([...typeNames(paramType(g), source), ...typeNames(returned(g), source)]).toEqual(['HashMap', 'String', 'Vec', 'u8', 'Option', 'String'])
expect(returned(h)).toEqual([{ text: '()', role: 'plain' }])
})
it('方法表的参数和返回、字段类型、变体携带的数据同样逐个名字带下标,导入来源逐节带下标', async () => {
const source = [
'use std::collections::HashMap;',
'struct S {',
' map: HashMap<String, Vec<u8>>,',
'}',
'enum E {',
' Pair(Option<String>, Box<S>),',
' Named { id: Uuid },',
'}',
'impl S {',
' fn m(',
' &self,',
' state: State<\'_, AppState>,',
' ) -> Result<(), String> {}',
'}',
'',
].join('\n')
const list = await views(source)
const from = propertyRow(card(list, 'import'), '来源').value
expect(previewText(from)).toBe('std::collections')
expect(wordsOf(from, 'type')).toEqual([['std', source.indexOf('std')], ['::', undefined], ['collections', source.indexOf('collections')]])
expect(typeNames(propertyRow(card(list, 'class'), '字段').items?.[0]?.cells[1], source)).toEqual(['HashMap', 'String', 'Vec', 'u8'])
const [pair, named] = propertyRow(card(list, 'enum'), '变体').items ?? []
expect(previewText(pair?.cells[1] ?? [])).toBe('Option<String>, Box<S>')
expect(typeNames(pair?.cells[1], source)).toEqual(['Option', 'String', 'Box', 'S'])
expect(previewText(named?.cells[1] ?? [])).toBe('{ id: Uuid }')
expect(typeNames(named?.cells[1], source)).toEqual(['Uuid'])
const [method] = propertyRow(card(list, 'rust.impl'), '方法').items ?? []
expect(previewText(method?.cells[1] ?? [])).toBe('&self, state: State<\'_, AppState>')
expect(typeNames(method?.cells[1], source)).toEqual(['State', 'AppState'])
expect(previewText(method?.cells[2] ?? [])).toBe('Result<(), String>')
expect(typeNames(method?.cells[2], source)).toEqual(['Result', 'String'])
})
it('带身体的模块拆成子块,常量和外部函数是卡片', async () => {
const source = [
'/// 平台相关。',
+1 -1
View File
@@ -17,7 +17,7 @@ function adapter(id: string, languageIds: string[]): LanguageAdapter {
}
function translator(id: string, languageIds: string[]): Translator {
return { id, label: id, languageIds, displayLanguage: 'zh', translate: () => ({ text: [] }), describe: () => [], expression: () => [], doc: () => undefined }
return { id, label: id, languageIds, displayLanguage: 'zh', translate: () => ({ text: [] }), describe: () => [], expression: () => [], verbatim: () => [], doc: () => undefined }
}
function rule(id: string, languageIds: string[]): LibraryRule {
+18 -24
View File
@@ -1,6 +1,7 @@
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
import { returnNote, throwsRows, writtenParamRows } from './docRows'
import { attributeName, docSummary, named, oneLine, returnText, stringContent } from './rustTree'
import { returnValue } from './rustRows'
import { attributeName, docSummary, named, stringContent } from './rustTree'
/** 和 FastAPI 接口同一个蓝。 */
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
@@ -52,14 +53,14 @@ function attributeRoute(block: FunctionBlock): Route | undefined {
function card(block: FunctionBlock, route: Route, context: RuleContext): RuleCard {
const doc = block.facts.doc && context.doc(block.facts.doc)
const summary = docSummary(doc)
const params = pathParams(route.path, block.node)
const params = pathParams(route.path, block.node, context)
const rows: PropertyRow[] = [
nameRow(block.facts.name),
...(summary ? [{ label: '说明', value: { markdown: summary } }] : []),
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
requestRow(route, params),
...writtenParamRows(doc?.params ?? [], '参数'),
{ label: '返回', value: [plain(returnText(block.node))], ...returnNote(doc) },
{ label: '返回', value: returnValue(block.facts.returnType, context), ...returnNote(doc) },
...throwsRows(doc),
]
return {
@@ -86,7 +87,7 @@ function handled(node: Node, handler: string | undefined): boolean {
function routerCard(node: Node, route: Route & { handlerNode?: Node }, context: RuleContext): RuleCard {
const fn = route.handler ? functionNamed(node, route.handler) : undefined
const params = fn ? pathParams(route.path, fn) : []
const params = fn ? pathParams(route.path, fn, context) : []
const comment = fn && leadingDoc(fn)
const doc = docSummary(comment && context.doc(comment))
return {
@@ -96,17 +97,17 @@ function routerCard(node: Node, route: Route & { handlerNode?: Node }, context:
{ label: '名称', value: route.handler ? [plain(route.handler)] : [hint('路由')] },
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
requestRow(route, params),
{ label: '返回', value: [plain(fn ? returnText(fn) : '—')] },
{ label: '返回', value: fn ? returnValue(fn.childForFieldName('return_type'), context) : [plain('—')] },
],
absorbs: [],
}
}
function requestRow(route: Route, params: string[]): PropertyRow {
function requestRow(route: Route, params: Inline[][]): PropertyRow {
const items: PropertyItem[] = [
{ cells: [[plain('方法')], [keyword(route.method)]] },
{ cells: [[plain('路径')], [{ text: route.path, role: 'string' }]] },
...(params.length > 0 ? [{ cells: [[plain('路径参数')], params.map(text => plain(text)).flatMap((part, index) => index === 0 ? [part] : [plain(', '), part])] }] : []),
...(params.length > 0 ? [{ cells: [[plain('路径参数')], params.flatMap((part, index) => index === 0 ? part : [plain(', '), ...part])] }] : []),
]
return {
label: '请求',
@@ -141,15 +142,12 @@ function routesOn(node: Node, found: (Route & { handlerNode?: Node })[] = []): (
return found
}
/** 路径里的 {名字} 对上参数。Path(id): Path<Uuid> 写成 id: Uuid。 */
function pathParams(path: string, fn: Node): string[] {
/** 路径里的 `{名字}` 对上函数的参数:`Path(id): Path<Uuid>` 写成 `id: Uuid`,类型照抄成一行、名字各自带源码下标。 */
function pathParams(path: string, fn: Node, context: RuleContext): Inline[][] {
const names = [...path.matchAll(/\{(\w+)\}/g)].map(match => match[1] ?? '')
return names.flatMap((name) => {
const param = parameterNamed(fn, name)
if (!param)
return [`${name}`]
const type = innerType(param.childForFieldName('type'))
return [type ? `${name}: ${type}` : name]
return names.map((name) => {
const type = innerType(parameterNamed(fn, name)?.childForFieldName('type'))
return type ? [plain(`${name}: `), ...context.verbatim(type)] : [plain(name)]
})
}
@@ -163,15 +161,11 @@ function parameterNamed(fn: Node, name: string): Node | undefined {
})
}
function innerType(type: Node | null): string | undefined {
if (!type)
return undefined
if (type.type === 'generic_type' && type.childForFieldName('type')?.text === 'Path') {
const arg = named(type.childForFieldName('type_arguments'))[0]
if (arg)
return oneLine(arg.text)
}
return oneLine(type.text)
/** 路径参数的类型:`Path<Uuid>` 取尖括号里的第一个,别的写法就是它自己。 */
function innerType(type: Node | null | undefined): Node | undefined {
if (type?.type === 'generic_type' && type.childForFieldName('type')?.text === 'Path')
return named(type.childForFieldName('type_arguments'))[0] ?? type
return type ?? undefined
}
function itemAttributesHasRoute(fn: Node): boolean {
+1 -1
View File
@@ -537,7 +537,7 @@ describe('整份文件的扫描', () => {
await withBlocks(py, 'python', ROUTES, (blocks) => {
const imports = importsIn(blocks)
let reads = 0
const base = { document: { source: ROUTES, language: py, markers: [] }, translate: () => [], doc: () => undefined, lookup: () => undefined }
const base = { document: { source: ROUTES, language: py, markers: [] }, translate: () => [], verbatim: () => [], doc: () => undefined, lookup: () => undefined }
const shared: RuleContext = { ...base, get imports() {
reads++
return imports
+3 -4
View File
@@ -1,5 +1,5 @@
import type { ImplBlock, KindBadge, LibraryRule, PropertyRow, RuleCard, TraitBlock } from '../contract'
import { implRows, plainSay, traitRows } from './rustRows'
import { implRows, traitRows } from './rustRows'
/** 和函数卡同一个黄,实现块看起来仍是函数那一挂。 */
const IMPL: KindBadge = { label: '实现', color: { dark: '#dcdcaa', light: '#795e26' }, icon: 'square-function' }
@@ -16,11 +16,10 @@ export const rustImplRule: LibraryRule = {
languageIds: ['rust'],
libraries: [],
claim: (block, context) => {
const say = plainSay(context.doc)
if (block.kind === 'impl')
return methodsCard(block, 'rust.impl', IMPL, implRows(block, say))
return methodsCard(block, 'rust.impl', IMPL, implRows(block, context))
if (block.kind === 'trait')
return methodsCard(block, 'rust.trait', TRAIT, traitRows(block, say))
return methodsCard(block, 'rust.trait', TRAIT, traitRows(block, context))
return undefined
},
attributeNames: () => ({
+35 -1
View File
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../blockView'
import { blockMarks } from '../diff/viewDiff'
import { createRustAdapter } from '../languages/rust'
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { previewText, propertyRow, readFixture, typeNames, viewLines, WASM_DIR, withBlocks, wordsOf } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { axumRule } from './axum'
import { rustImplRule } from './rustImpl'
@@ -97,6 +97,40 @@ describe('serde 与 thiserror', () => {
])
})
it('库卡片里照抄的类型逐个名字带自己的下标:tauri 的参数类型和返回、axum 的路径参数和返回、serde 的字段类型、sqlx 的返回类型', async () => {
const source = [
'use tauri::{AppHandle, State};',
'use axum::routing::get;',
'use serde::Serialize;',
'use sqlx::query_as;',
'#[tauri::command]',
'async fn rename(app: AppHandle, state: State<\'_, AppState>) -> Result<(), String> {}',
'#[get("/agents/{id}")]',
'async fn get_agent(Path(id): Path<Uuid>) -> Result<Json<AgentRow>, StatusCode> {}',
'#[derive(Serialize)]',
'struct Row {',
' tags: Vec<Tag>,',
'}',
'let row = query_as!(AgentRow, "SELECT 1");',
'',
].join('\n')
const list = await views(source)
const card = (kind: string): BlockView | undefined => list.find(block => block.kind === kind)
const [app, state] = propertyRow(card('tauri.command'), '参数').items ?? []
expect(typeNames(app?.cells[1], source)).toEqual(['AppHandle'])
expect(previewText(state?.cells[1] ?? [])).toBe('State<\'_, AppState>')
expect(typeNames(state?.cells[1], source)).toEqual(['State', 'AppState'])
const tauriReturned = propertyRow(card('tauri.command'), '返回').value
expect(previewText(tauriReturned)).toBe('Result<(), String>')
expect(wordsOf(tauriReturned, 'type')).toEqual([['Result', source.indexOf('Result')], ['String', source.indexOf('String')]])
const pathParams = propertyRow(card('axum.route'), '请求').items?.[2]?.cells[1]
expect(previewText(pathParams ?? [])).toBe('id: Uuid')
expect(typeNames(pathParams, source)).toEqual(['Uuid'])
expect(typeNames(propertyRow(card('axum.route'), '返回').value, source)).toEqual(['Result', 'Json', 'AgentRow', 'StatusCode'])
expect(typeNames(propertyRow(card('serde.struct'), '字段').items?.[0]?.cells[1], source)).toEqual(['Vec', 'Tag'])
expect(typeNames(propertyRow(card('sqlx.query'), '返回类型').value, source)).toEqual(['AgentRow'])
})
it('没导入 serde 时,带 derive 的结构体仍是结构体', async () => {
const source = '#[derive(Serialize)]\npub struct AgentRow {\n name: String,\n}\n'
const [card] = await views(source)
+29 -39
View File
@@ -1,15 +1,14 @@
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, RuleContext, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { RustVisibility } from './rustTree'
import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
import { docSummary, isFnItem, isMethod, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
import { docSummary, isFnItem, isMethod, signatureParams, structFields, variantPayload } from './rustTree'
import { hint, joined, plain, range, name as sourceName } from './segments'
/** 把类型、名字画成原文,读说明。翻译插件传入时,名字带源码下标。 */
export interface RustSay {
name: (node: Node, role: TokenRole) => Inline[]
source: (node: Node, role?: TokenRole) => Inline[]
/** 读一项的文档注释(连着几行的 `///` 是一段),见 Translator.doc;没有时没有。 */
doc: (node: Node) => Doc | undefined
}
/**
* 属性表照抄类型、读说明的办法,同 [RuleContext] 的 `verbatim`、`doc`:库规则直接交 [RuleContext],
* 中文直译用自己的模板拼一份。
*/
export type RustSay = Pick<RuleContext, 'verbatim' | 'doc'>
const VISIBILITY: Record<RustVisibility, string> = {
public: '公开',
@@ -31,7 +30,7 @@ export function visibilityWord(block: CodeBlock): string {
export function importRows(block: ImportBlock, say: RustSay): PropertyRow[] {
const [source] = block.facts.sources
return [
{ label: '来源', value: source ? say.source(source, 'type') : [hint('—')] },
{ label: '来源', value: source ? say.verbatim(source, 'type') : [hint('—')] },
{
label: '导入项',
value: [hint(`${block.facts.items.length} 个`)],
@@ -42,7 +41,7 @@ export function importRows(block: ImportBlock, say: RustSay): PropertyRow[] {
if (item.kind !== 'named')
return { cells: [[hint('—')], [hint('—')]] }
return {
cells: [say.name(item.name, 'type'), item.alias ? say.name(item.alias, 'type') : [hint('—')]],
cells: [[sourceName(item.name, 'type')], item.alias ? [sourceName(item.alias, 'type')] : [hint('—')]],
defines: range(item.name),
}
}),
@@ -62,8 +61,8 @@ export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] {
visibilityRow(block),
...tableRow('字段', ['字段', '类型', '可见性', ...(described ? ['说明'] : [])], fields.map(field => ({
cells: [
say.name(field.name, 'variable'),
say.source(field.type, 'type'),
[sourceName(field.name, 'variable')],
say.verbatim(field.type),
[keyword(VISIBILITY[field.visibility])],
...(described ? [paramDocCell(docs, field.name.text)] : []),
],
@@ -176,7 +175,7 @@ export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
nameRow(block.facts.name, say, 'type'),
...docRow(block.facts.doc && say.doc(block.facts.doc)),
...tableRow('变体', ['变体', '携带的数据'], block.facts.members.map(member => ({
cells: [say.name(member.name, 'type'), [hint(variantPayload(member.value) ?? '—')]],
cells: [[sourceName(member.name, 'type')], payloadValue(member.value, say)],
defines: range(member.name),
}))),
]
@@ -200,13 +199,13 @@ export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[]
...(visible ? [visibilityRow(block)] : []),
...tableRow(isMethod(block.node) ? '参数' : '输入参数', ['名称', '类型', ...(described ? ['说明'] : [])], parameters.map(param => ({
cells: [
say.name(param.name, 'variable'),
param.type ? say.source(param.type, 'type') : [hint('—')],
[sourceName(param.name, 'variable')],
param.type ? say.verbatim(param.type) : [hint('—')],
...(described ? [paramDocCell(docs, param.name.text)] : []),
],
defines: range(param.name),
}))),
...(returnType || isFnItem(block.node) ? [{ label: '返回', value: [plain(returnType ? oneLine(returnType.text) : '()')], ...returnNote(doc) }] : []),
...(returnType || isFnItem(block.node) ? [{ label: '返回', value: returnValue(returnType, say), ...returnNote(doc) }] : []),
...throwsRows(doc),
]
}
@@ -221,7 +220,7 @@ export function traitRows(block: TraitBlock, say: RustSay): PropertyRow[] {
export function implRows(block: ImplBlock, say: RustSay): PropertyRow[] {
const title: Inline[] = block.facts.name
? [...say.name(block.facts.name, 'type'), plain(' 的方法')]
? [sourceName(block.facts.name, 'type'), plain(' 的方法')]
: [hint('实现')]
return [
{ label: '名称', value: title, ...(block.facts.name && { defines: range(block.facts.name) }) },
@@ -237,9 +236,9 @@ function methodRows(block: TraitBlock | ImplBlock, say: RustSay): PropertyRow[]
const name = method.facts.name
return {
cells: [
name ? say.name(name, 'function') : [hint('匿名')],
[plain(signatureParams(method.node))],
[plain(returnText(method.node))],
name ? [sourceName(name, 'function')] : [hint('匿名')],
joined(signatureParams(method.node).map(param => say.verbatim(param)), plain(', ')),
returnValue(method.facts.returnType, say),
],
...(name && { defines: range(name) }),
}
@@ -249,7 +248,7 @@ function methodRows(block: TraitBlock | ImplBlock, say: RustSay): PropertyRow[]
function nameRow(name: Node | undefined, say: RustSay, role: TokenRole): PropertyRow {
return {
label: '名称',
value: name ? say.name(name, role) : [hint('匿名')],
value: name ? [sourceName(name, role)] : [hint('匿名')],
...(name && { defines: range(name) }),
}
}
@@ -270,14 +269,6 @@ function tableRow(label: string, columns: string[], items: PropertyItem[]): Prop
return [{ label, value: [hint(`${items.length} 个`)], columns, items }]
}
function hint(text: string): Inline {
return { text, role: 'hint' }
}
function plain(text: string): Inline {
return { text, role: 'plain' }
}
function keyword(text: string): Inline {
return { text, role: 'keyword' }
}
@@ -290,14 +281,13 @@ function note(text: string): Inline {
return { text, role: 'comment' }
}
function range(node: Node): { startIndex: number, endIndex: number } {
return { startIndex: node.startIndex, endIndex: node.endIndex }
/** 函数的返回类型 `returned` 照抄成一行,名字各自带源码下标;没写返回类型时是普通文字 `()`。 */
export function returnValue(returned: Node | null | undefined, say: RustSay): Inline[] {
return returned ? say.verbatim(returned) : [plain('()')]
}
/** 名字、类型照抄源码,名字带上下标,点了能跳到定义。 */
export function plainSay(doc: RustSay['doc']): RustSay {
return {
name: (node, role) => [{ text: node.text, role, offset: node.startIndex }],
source: (node, role = 'type') => [{ text: oneLine(node.text), role, offset: node.startIndex }],
doc,
}
/** 变体携带的数据照抄成一行:元组变体的各个类型用逗号隔开,结构变体是字段表原文;没带数据写「—」。`body` 是变体的 body。 */
export function payloadValue(body: Node | undefined, say: RustSay): Inline[] {
const parts = variantPayload(body)
return parts.length > 0 ? joined(parts.map(part => say.verbatim(part)), plain(', ')) : [hint('—')]
}
+8 -21
View File
@@ -69,29 +69,16 @@ export function visibilityFacts(node: Node): { access?: Access, attributes?: Blo
return visibility === 'public' ? { access: 'public' } : { attributes: { access: visibility } }
}
/** 参数表括号里的原文,空白收成一行。 */
export function signatureParams(fn: Node): string {
const params = fn.childForFieldName('parameters')
if (!params)
return ''
return oneLine(params.text.replace(/^\(/, '').replace(/\)$/, ''))
/** 参数表括号里的各个参数(含 `self`),属性、注释不算。 */
export function signatureParams(fn: Node): Node[] {
return named(fn.childForFieldName('parameters')).filter(child => child.type !== 'attribute_item' && !child.isExtra)
}
/** 返回类型原文;没写是 ()。 */
export function returnText(fn: Node): string {
const returned = fn.childForFieldName('return_type')
return returned ? oneLine(returned.text) : '()'
}
/** 变体携带的数据:没带是空的,元组变体是类型,结构变体是字段原文。body 是变体的 body。 */
export function variantPayload(body: Node | undefined): string | undefined {
if (!body)
return undefined
if (body.type === 'ordered_field_declaration_list') {
const types = named(body).filter(child => child.type !== 'attribute_item').map(child => oneLine(child.text))
return types.join(', ') || undefined
}
return oneLine(body.text)
/** 变体携带的数据:元组变体是各个类型,结构变体是整个字段表 `{ … }`,没带数据是空的。`body` 是变体的 body。 */
export function variantPayload(body: Node | undefined): Node[] {
if (body?.type === 'ordered_field_declaration_list')
return named(body).filter(child => child.type !== 'attribute_item' && !child.isExtra)
return body ? [body] : []
}
/** 写在这条前面、紧挨着的属性(#[…]),从外往里。 */
+6 -9
View File
@@ -1,8 +1,9 @@
import type { ClassBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor, TokenRole } from '../contract'
import type { ClassBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract'
import type { rustVisibility } from './rustTree'
import { paramDocCell, paramDocs, unmatchedParamRows } from './docRows'
import { visibilityWord } from './rustRows'
import { attributeName, deriveNames, docSummary, itemAttributes, leadingAttributes, named, oneLine, stringContent, structFields } from './rustTree'
import { attributeName, deriveNames, docSummary, itemAttributes, leadingAttributes, named, stringContent, structFields } from './rustTree'
import { name as sourceName } from './segments'
/** 和结构体同一个青,序列化只是名称后的标签,不另换边框色。 */
const COLOR: ThemeColor = { dark: '#4ec9b0', light: '#267f99' }
@@ -36,7 +37,7 @@ function card(block: ClassBlock, context: RuleContext): RuleCard {
const rows: PropertyRow[] = [
{
label: '名称',
value: block.facts.name ? [...copied(block.facts.name, 'type'), plain(' '), tag('可序列化')] : [tag('可序列化')],
value: block.facts.name ? [sourceName(block.facts.name, 'type'), plain(' '), tag('可序列化')] : [tag('可序列化')],
...(block.facts.name && { defines: range(block.facts.name) }),
},
...(summary ? [{ label: '说明', value: { markdown: summary } }] : []),
@@ -50,8 +51,8 @@ function card(block: ClassBlock, context: RuleContext): RuleCard {
columns: ['字段', '类型', '可见性', '序列化', ...(described ? ['说明'] : [])],
items: fields.map(field => ({
cells: [
copied(field.name, 'variable'),
copied(field.type, 'type'),
[sourceName(field.name, 'variable')],
context.verbatim(field.type),
[keyword(visibilityWordOf(field.visibility))],
[serdeCell(field.name)],
...(described ? [paramDocCell(docs, field.name.text)] : []),
@@ -125,10 +126,6 @@ function firstString(tree: Node | undefined): Node | undefined {
return tree?.children.find(child => child?.type === 'string_literal') ?? undefined
}
function copied(node: Node, role: TokenRole): Inline[] {
return [{ text: oneLine(node.text), role, offset: node.startIndex }]
}
function tag(label: string): SegmentGroup {
return { style: 'tag', label, segments: [], color: COLOR }
}
+5 -5
View File
@@ -1,4 +1,4 @@
import type { Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, ThemeColor } from '../contract'
import type { Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
import { macroInvocation, named, oneLine, stringContent } from './rustTree'
/** 查询卡用字符串那种橙。 */
@@ -15,14 +15,14 @@ export const sqlxRule: LibraryRule = {
label: 'sqlx 查询',
languageIds: ['rust'],
libraries: ['sqlx'],
claim: (block) => {
claim: (block, context) => {
const macro = block.kind === 'statement' ? findQuery(block.node) : undefined
return macro ? card(macro) : undefined
return macro ? card(macro, context) : undefined
},
attributeNames: () => ({ attributes: {}, values: { kind: { 'sqlx.query': '查询' } } }),
}
function card(macro: Node): RuleCard {
function card(macro: Node, context: RuleContext): RuleCard {
const call = macroInvocation(macro)
const args = call?.args ?? []
const typed = call?.name.split('::').pop() !== 'query'
@@ -35,7 +35,7 @@ function card(macro: Node): RuleCard {
...(params.length > 0
? [{ label: '参数', value: [{ text: params.map((param, index) => `$${index + 1} = ${oneLine(param.text)}`).join('、'), role: 'plain' as const }] }]
: []),
{ label: '返回类型', value: returned ? [{ text: oneLine(returned.text), role: 'type', offset: returned.startIndex }] : [hint('—')] },
{ label: '返回类型', value: returned ? context.verbatim(returned, 'type') : [hint('—')] },
]
return { kind: 'sqlx.query', badge: BADGE, rows, absorbs: [] }
}
+4 -7
View File
@@ -1,6 +1,7 @@
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract'
import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
import { attributeName, docSummary, oneLine, returnText } from './rustTree'
import { returnValue } from './rustRows'
import { attributeName, docSummary } from './rustTree'
/** 桌面命令的边框用这个橙。 */
const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' }
@@ -49,7 +50,7 @@ function card(block: FunctionBlock, context: RuleContext): RuleCard {
items: parameters.map(param => ({
cells: [
[variable(param.name)],
[param.type ? typed(param.type) : hint('—')],
param.type ? context.verbatim(param.type) : [hint('—')],
[hint(isWindowState(param.type) ? '窗口状态' : '调用方传入')],
...(described ? [paramDocCell(docs, param.name.text)] : []),
],
@@ -57,7 +58,7 @@ function card(block: FunctionBlock, context: RuleContext): RuleCard {
})),
}]
: []),
{ label: '返回', value: [plain(returnText(block.node))], ...returnNote(doc) },
{ label: '返回', value: returnValue(block.facts.returnType, context), ...returnNote(doc) },
...throwsRows(doc),
]
return {
@@ -81,10 +82,6 @@ function variable(node: Node): Inline {
return { text: node.text, role: 'variable', offset: node.startIndex }
}
function typed(node: Node): Inline {
return { text: oneLine(node.text), role: 'type', offset: node.startIndex }
}
function tag(label: string): SegmentGroup {
return { style: 'tag', label, segments: [], color: COLOR }
}
+3 -2
View File
@@ -1,5 +1,6 @@
import type { EnumBlock, Inline, KindBadge, LibraryRule, Node, PreviewText, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract'
import { attributeName, commentPlain, deriveNames, docSummary, firstSentence, named, stringContent, variantPayload } from './rustTree'
import { payloadValue } from './rustRows'
import { attributeName, commentPlain, deriveNames, docSummary, firstSentence, named, stringContent } from './rustTree'
/** 错误卡用异常那种红。 */
const COLOR: ThemeColor = { dark: '#f14c4c', light: '#cd3131' }
@@ -39,7 +40,7 @@ function card(block: EnumBlock, context: RuleContext): RuleCard {
[hint(whenText(variant) ?? '—')],
display
? [plain(display), ...(passesSource(variant) ? [plain(' '), tag('透传来源')] : [])]
: [hint(variantPayload(member.value) ?? '—')],
: payloadValue(member.value, context),
]
return { cells, defines: range(member.name) }
}),
+22
View File
@@ -122,6 +122,28 @@ export function inlineOf(text: PreviewText): Inline[] {
return 'markdown' in text ? [] : text
}
/** 卡片上叫 `label` 的那一行;没有这一行就抛错。 */
export function propertyRow(block: BlockView | undefined, label: string): PropertyRow {
const row = block && 'properties' in block ? block.properties.find(item => item.label === label) : undefined
if (!row)
throw new Error(`没有「${label}」这一行`)
return row
}
/** 预览文字里按 `role` 着色的词:各自的文字和源码下标(没带下标的是 `undefined`)。 */
export function wordsOf(text: PreviewText | undefined, role: TokenRole): [string, number | undefined][] {
return flatSegments(text ? inlineOf(text) : []).filter(segment => segment.role === role).map(segment => [segment.text, segment.offset])
}
/** 预览文字里按类型着色的词的文字;每个词都要带下标,下标在 `source` 里正好是它自己,否则抛错。 */
export function typeNames(text: PreviewText | undefined, source: string): string[] {
return wordsOf(text, 'type').map(([word, offset]) => {
if (offset === undefined || source.slice(offset, offset + word.length) !== word)
throw new Error(`「${word}」的下标 ${offset} 对不上源码`)
return word
})
}
/** 子行写成文字:各列用 ` | ` 隔开,下一层子行、排在最后一格里的表(PropertyItem.table)的各行比这一层多缩进两格。 */
function itemLines(items: PropertyItem[] | undefined, indent: string): string[] {
return (items ?? []).flatMap(item => [
+3 -2
View File
@@ -1,5 +1,6 @@
import type { FactBlock, Inline, Node, PropertyRow, SourceDocument, SourceSpan, TextBlock, TextTranslation, Translator } from '../contract'
import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
import type { RustSay } from '../rules/rustRows'
import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows, structRows, traitRows } from '../rules/rustRows'
import { isCrateRoot } from '../rules/rustTree'
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
@@ -93,7 +94,7 @@ function translate(block: TextBlock, document: SourceDocument): TextTranslation
}
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
const say = plainSay(node => template.doc(node, document))
const say: RustSay = { verbatim: (node, role) => template.verbatim(node, document, role), doc: node => template.doc(node, document) }
switch (block.kind) {
case 'import':
return importRows(block, say)
@@ -717,8 +717,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return out.tokens
}
/** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */
function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole): Segment[] {
/**
* `node` 的源码原样照抄:叶子按源码着色(给了 `role` 就都按它),名字带源码下标,叶子之间的空白、标点照抄;
* `flat` 为真时压成一行,连着的空白(含换行)收成一个空格。
*/
function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole, flat = false): Segment[] {
const copy = (text: string): string => flat ? text.replace(/\s+/g, ' ') : text
const segments: Segment[] = []
let cursor = node.startIndex
const visit = (current: Node): void => {
@@ -731,12 +735,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
return
}
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain')
pushSegment(segments, current.text, role ?? language.tokenRole(current), offsetOf(current, language))
pushSegment(segments, copy(source.slice(cursor, current.startIndex)), 'plain')
pushSegment(segments, copy(current.text), role ?? language.tokenRole(current), offsetOf(current, language))
cursor = current.endIndex
}
visit(node)
pushSegment(segments, source.slice(cursor, node.endIndex), 'plain')
pushSegment(segments, copy(source.slice(cursor, node.endIndex)), 'plain')
return segments
}
@@ -843,6 +847,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
translate,
describe,
expression,
verbatim: (node, document, role) => verbatim(node, document, role, true),
doc,
attributeNames: ZH_ATTRIBUTE_NAMES,
}