From 55595e948c869009cb0509c416d68619c0d98eb5 Mon Sep 17 00:00:00 2001 From: cruldra Date: Thu, 15 Oct 2026 19:48:56 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20Rust=20=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E7=85=A7=E6=8A=84=E7=9A=84=E7=B1=BB=E5=9E=8B=E3=80=81?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E9=80=90=E4=B8=AA=E5=90=8D=E5=AD=97=E5=B8=A6?= =?UTF-8?q?=E4=B8=8B=E6=A0=87=EF=BC=8C=E8=BF=94=E5=9B=9E=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=B9=9F=E8=83=BD=E6=82=AC=E5=81=9C=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vscode/src/codeReader/blockView.ts | 1 + vscode/src/codeReader/contract.ts | 7 ++ vscode/src/codeReader/languages/rust.test.ts | 65 +++++++++++++++++- vscode/src/codeReader/registry.test.ts | 2 +- vscode/src/codeReader/rules/axum.ts | 42 +++++------- vscode/src/codeReader/rules/fastapi.test.ts | 2 +- vscode/src/codeReader/rules/rustImpl.ts | 7 +- vscode/src/codeReader/rules/rustLibs.test.ts | 36 +++++++++- vscode/src/codeReader/rules/rustRows.ts | 68 ++++++++----------- vscode/src/codeReader/rules/rustTree.ts | 29 +++----- vscode/src/codeReader/rules/serde.ts | 15 ++-- vscode/src/codeReader/rules/sqlx.ts | 10 +-- vscode/src/codeReader/rules/tauri.ts | 11 ++- vscode/src/codeReader/rules/thiserror.ts | 5 +- vscode/src/codeReader/testSupport.ts | 22 ++++++ vscode/src/codeReader/translators/rustZh.ts | 5 +- .../src/codeReader/translators/zhTemplate.ts | 15 ++-- 17 files changed, 220 insertions(+), 122 deletions(-) diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index 190ff90..613b28d 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -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), } diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index d3c8a7e..76ab6c7 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -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 /** diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index 67fb7f1..187e982 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -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,', + ' >,', + ') -> 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, >') + 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>,', + '}', + 'enum E {', + ' Pair(Option, Box),', + ' 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, Box') + 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 = [ '/// 平台相关。', diff --git a/vscode/src/codeReader/registry.test.ts b/vscode/src/codeReader/registry.test.ts index 4602fd9..3f3d1a6 100644 --- a/vscode/src/codeReader/registry.test.ts +++ b/vscode/src/codeReader/registry.test.ts @@ -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 { diff --git a/vscode/src/codeReader/rules/axum.ts b/vscode/src/codeReader/rules/axum.ts index d6abc73..8165e70 100644 --- a/vscode/src/codeReader/rules/axum.ts +++ b/vscode/src/codeReader/rules/axum.ts @@ -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 写成 id: Uuid。 */ -function pathParams(path: string, fn: Node): string[] { +/** 路径里的 `{名字}` 对上函数的参数:`Path(id): Path` 写成 `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` 取尖括号里的第一个,别的写法就是它自己。 */ +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 { diff --git a/vscode/src/codeReader/rules/fastapi.test.ts b/vscode/src/codeReader/rules/fastapi.test.ts index 55cad53..7ac1a6f 100644 --- a/vscode/src/codeReader/rules/fastapi.test.ts +++ b/vscode/src/codeReader/rules/fastapi.test.ts @@ -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 diff --git a/vscode/src/codeReader/rules/rustImpl.ts b/vscode/src/codeReader/rules/rustImpl.ts index 5180591..a8157ae 100644 --- a/vscode/src/codeReader/rules/rustImpl.ts +++ b/vscode/src/codeReader/rules/rustImpl.ts @@ -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: () => ({ diff --git a/vscode/src/codeReader/rules/rustLibs.test.ts b/vscode/src/codeReader/rules/rustLibs.test.ts index 1926b16..b941e98 100644 --- a/vscode/src/codeReader/rules/rustLibs.test.ts +++ b/vscode/src/codeReader/rules/rustLibs.test.ts @@ -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) -> Result, StatusCode> {}', + '#[derive(Serialize)]', + 'struct Row {', + ' tags: Vec,', + '}', + '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) diff --git a/vscode/src/codeReader/rules/rustRows.ts b/vscode/src/codeReader/rules/rustRows.ts index 6a85f3c..2da7153 100644 --- a/vscode/src/codeReader/rules/rustRows.ts +++ b/vscode/src/codeReader/rules/rustRows.ts @@ -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 const VISIBILITY: Record = { 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('—')] } diff --git a/vscode/src/codeReader/rules/rustTree.ts b/vscode/src/codeReader/rules/rustTree.ts index 079419f..740a364 100644 --- a/vscode/src/codeReader/rules/rustTree.ts +++ b/vscode/src/codeReader/rules/rustTree.ts @@ -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] : [] } /** 写在这条前面、紧挨着的属性(#[…]),从外往里。 */ diff --git a/vscode/src/codeReader/rules/serde.ts b/vscode/src/codeReader/rules/serde.ts index b499723..d573747 100644 --- a/vscode/src/codeReader/rules/serde.ts +++ b/vscode/src/codeReader/rules/serde.ts @@ -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 } } diff --git a/vscode/src/codeReader/rules/sqlx.ts b/vscode/src/codeReader/rules/sqlx.ts index ebbd566..faef4ea 100644 --- a/vscode/src/codeReader/rules/sqlx.ts +++ b/vscode/src/codeReader/rules/sqlx.ts @@ -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: [] } } diff --git a/vscode/src/codeReader/rules/tauri.ts b/vscode/src/codeReader/rules/tauri.ts index 9c54fee..47349e2 100644 --- a/vscode/src/codeReader/rules/tauri.ts +++ b/vscode/src/codeReader/rules/tauri.ts @@ -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 } } diff --git a/vscode/src/codeReader/rules/thiserror.ts b/vscode/src/codeReader/rules/thiserror.ts index c430150..1be152d 100644 --- a/vscode/src/codeReader/rules/thiserror.ts +++ b/vscode/src/codeReader/rules/thiserror.ts @@ -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) } }), diff --git a/vscode/src/codeReader/testSupport.ts b/vscode/src/codeReader/testSupport.ts index 90da38a..ec5e359 100644 --- a/vscode/src/codeReader/testSupport.ts +++ b/vscode/src/codeReader/testSupport.ts @@ -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 => [ diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts index 816670d..2f76f23 100644 --- a/vscode/src/codeReader/translators/rustZh.ts +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -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) diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index e073bc0..e4dc7f9 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -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, }