diff --git a/vscode/esbuild.mjs b/vscode/esbuild.mjs index cdc189b..bed9774 100644 --- a/vscode/esbuild.mjs +++ b/vscode/esbuild.mjs @@ -32,6 +32,8 @@ const TREE_SITTER_WASM = [ 'tree-sitter-javascript.wasm', 'tree-sitter-python.wasm', 'tree-sitter-bash.wasm', + // 包入口就在 wasm/(main 指向 wasm/tree-sitter.js),rust 的语法包和 python 的在同一层 + 'tree-sitter-rust.wasm', ] // @vscode/tree-sitter-wasm 没有的语法包,从 tree-sitter-wasm(预编译、签过名的各家语法包)拿:语言名 → 它的 wasm diff --git a/vscode/package.json b/vscode/package.json index 1b08c76..12f4b92 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -379,7 +379,7 @@ "editor/title": [ { "command": "superpowers.codeReader.showPreview", - "when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile)$/ || resourceLangId in superpowers.codeReader.languageIds)", + "when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile|rust)$/ || resourceLangId in superpowers.codeReader.languageIds)", "group": "navigation" }, { @@ -411,7 +411,7 @@ }, { "command": "superpowers.codeReader.showPreview", - "when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile)$/ || resourceLangId in superpowers.codeReader.languageIds)" + "when": "activeEditor == workbench.editors.files.textFileEditor && (resourceLangId =~ /^(typescript|typescriptreact|javascript|javascriptreact|python|shellscript|dockerfile|rust)$/ || resourceLangId in superpowers.codeReader.languageIds)" }, { "command": "superpowers.codeReader.showSource", diff --git a/vscode/src/codeReader/attributes.ts b/vscode/src/codeReader/attributes.ts index d9bf73b..b8ecae8 100644 --- a/vscode/src/codeReader/attributes.ts +++ b/vscode/src/codeReader/attributes.ts @@ -26,6 +26,9 @@ function factAttributes(block: CodeBlock): Partial : never : never @@ -790,6 +795,8 @@ export const KIND_ICONS: Record = { import: 'import', function: 'square-function', class: 'box', + trait: 'network', + impl: 'square-function', type: 'braces', enum: 'list-ordered', exception: 'triangle-alert', diff --git a/vscode/src/codeReader/fixtures/rustLang.rs b/vscode/src/codeReader/fixtures/rustLang.rs new file mode 100644 index 0000000..c418602 --- /dev/null +++ b/vscode/src/codeReader/fixtures/rustLang.rs @@ -0,0 +1,45 @@ +use serde::Serialize; +use std::sync::Arc; +use foo::{Bar as Baz, Qux}; + +/// 桌面端列表里的一行代理。 +/// 后面还有一句。 +pub struct AgentRow { + pub id: Uuid, + name: String, +} + +pub(crate) struct CrateOnly { + pub(super) up: u8, +} + +/// 这个代理现在处在哪一步。 +pub enum AgentState { + Idle, + Running { pid: u32 }, + Failed(String), +} + +impl AgentRow { + fn rename(&mut self, name: String) {} +} + +/// 在本机拉起接口服务。 +pub async fn start_server(addr: SocketAddr) -> Result<(), StartError> { + match result { + Ok(listener) => { + // 记下监听地址 + self.listener = Some(listener); + } + Err(err) => { + // 返回 StartError + return Err(StartError); + } + } +} + +/// 按 id 读写代理。 +pub trait Store { + fn get(&self, id: Uuid) -> Result; + fn save(&mut self, row: AgentRow) -> Result<(), StoreError>; +} diff --git a/vscode/src/codeReader/index.ts b/vscode/src/codeReader/index.ts index c5a6510..4e1bdfe 100644 --- a/vscode/src/codeReader/index.ts +++ b/vscode/src/codeReader/index.ts @@ -19,6 +19,7 @@ import { TreeFilters } from './explorer/treeFilters' import { createBashAdapter } from './languages/bash' import { createDockerfileAdapter } from './languages/dockerfile' import { createPythonAdapter } from './languages/python' +import { createRustAdapter } from './languages/rust' import { createTypeScriptAdapter } from './languages/typescript' import { resolveReference } from './libraryRules' import { indexedNames } from './nameResolution' @@ -33,11 +34,13 @@ import { enumRule } from './rules/enum' import { fastapiRule } from './rules/fastapi' import { pydanticRule } from './rules/pydantic' import { reactRule } from './rules/react' +import { rustImplRule } from './rules/rustImpl' import { sqlalchemyRule } from './rules/sqlalchemy' import { bundledWasmDir, TreeSitterRuntime } from './runtime' import { bashZhTranslator } from './translators/bashZh' import { dockerfileZhTranslator } from './translators/dockerfileZh' import { pythonZhTranslator } from './translators/pythonZh' +import { rustZhTranslator } from './translators/rustZh' import { typescriptZhTranslator } from './translators/typescriptZh' import { UncommittedMode } from './uncommitted/uncommittedMode' import { workspaceIndexHost } from './workspaceFiles' @@ -147,10 +150,12 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi { api.registerLanguage(createPythonAdapter(wasmDir)), api.registerLanguage(createBashAdapter(wasmDir)), api.registerLanguage(createDockerfileAdapter(wasmDir)), + api.registerLanguage(createRustAdapter(wasmDir)), api.registerTranslator(typescriptZhTranslator), api.registerTranslator(pythonZhTranslator), api.registerTranslator(bashZhTranslator), api.registerTranslator(dockerfileZhTranslator), + api.registerTranslator(rustZhTranslator), ...[...shellCommandRules, ...systemCommandRules, ...packageCommandRules].map(rule => api.registerCommandRule(rule)), api.registerLibraryRule(sqlalchemyRule), api.registerLibraryRule(celeryRule), @@ -159,6 +164,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi { api.registerLibraryRule(pydanticRule), api.registerLibraryRule(fastapiRule), api.registerLibraryRule(reactRule), + api.registerLibraryRule(rustImplRule), ) return api } diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts new file mode 100644 index 0000000..0808462 --- /dev/null +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -0,0 +1,112 @@ +import type { BlockView } from '../blockView' +import { describe, expect, it } from 'vitest' +import { toBlockViews } from '../blockView' +import { blockMarks } from '../diff/viewDiff' +import { rustImplRule } from '../rules/rustImpl' +import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport' +import { rustZhTranslator } from '../translators/rustZh' +import { createRustAdapter } from './rust' + +const adapter = createRustAdapter(WASM_DIR) +const sample = readFixture('rustLang.rs') + +function views(source: string): Promise { + return withBlocks(adapter, 'rust', source, blocks => + toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, { + rules: [rustImplRule], + report: () => {}, + })) +} + +function card(list: BlockView[], kind: string): BlockView { + const found = list.find(block => block.kind === kind) + if (!found) + throw new Error(`没有 ${kind}`) + return found +} + +describe('rust 语言卡片', () => { + it('use、结构体、枚举、实现、函数、特质', async () => { + expect(viewLines(await views(sample))).toEqual([ + 'import 1', + ' 来源: serde', + ' 导入项: 1 个', + ' Serialize | —', + 'import 2', + ' 来源: std::sync', + ' 导入项: 1 个', + ' Arc | —', + 'import 3', + ' 来源: foo', + ' 导入项: 2 个', + ' Bar | Baz', + ' Qux | —', + 'class 5', + ' 名称: AgentRow', + ' 说明: 桌面端列表里的一行代理。', + ' 可见性: 公开', + ' 字段: 2 个', + ' id | Uuid | 公开', + ' name | String | 本模块', + 'class 12', + ' 名称: CrateOnly', + ' 可见性: 本 crate', + ' 字段: 1 个', + ' up | u8 | 父模块', + 'enum 16', + ' 名称: AgentState', + ' 说明: 这个代理现在处在哪一步。', + ' 变体: 3 个', + ' Idle | —', + ' Running | { pid: u32 }', + ' Failed | String', + 'rust.impl 23', + ' 名称: AgentRow 的方法', + ' 方法: 1 个', + ' rename | &mut self, name: String | ()', + ' function 24', + ' 名称: rename', + ' 可见性: 本模块', + ' 参数: 2 个', + ' self | &mut self', + ' name | String', + ' 返回: ()', + 'function 27', + ' 名称: start_server', + ' 说明: 在本机拉起接口服务。', + ' 异步: 是', + ' 可见性: 公开', + ' 输入参数: 1 个', + ' addr | SocketAddr', + ' 返回: Result<(), StartError>', + ' 分情况: 2 支', + ' Ok(listener) | 记下监听地址', + ' Err(err) | 返回 StartError', + 'trait 41', + ' 名称: Store', + ' 说明: 按 id 读写代理。', + ' 方法: 2 个', + ' get | &self, id: Uuid | Result', + ' save | &mut self, row: AgentRow | Result<(), StoreError>', + ]) + }) + + it('实现的方法放进方法表那一行', async () => { + const impl = card(await views(sample), 'rust.impl') + const [method] = impl.children + expect(method?.slot).toEqual({ row: 1, item: 0 }) + }) + + it('给结构体加一个字段,多出来的那一行标成新增', async () => { + const before = 'pub struct AgentRow {\n name: String,\n}\n' + const after = 'pub struct AgentRow {\n name: String,\n id: Uuid,\n}\n' + const [oldCard] = await views(before) + const [newCard] = await views(after) + const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined + const field = newCard && 'properties' in newCard ? newCard.properties.findIndex(row => row.label === '字段') : -1 + expect(field).toBeGreaterThanOrEqual(0) + expect(marks?.rows?.changed[field]).toEqual({ + items: { changed: { 1: { added: true } }, removed: [] }, + }) + }) +}) diff --git a/vscode/src/codeReader/languages/rust.ts b/vscode/src/codeReader/languages/rust.ts new file mode 100644 index 0000000..8d84367 --- /dev/null +++ b/vscode/src/codeReader/languages/rust.ts @@ -0,0 +1,503 @@ +import type { Access, BlockAttributes, ClassBlock, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, TokenRole, TraitBlock } from '../contract' +import * as path from 'node:path' +import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, named } from './shared' + +/** 可见性:pub 是公开,没写是本模块,pub(super) 是父模块,pub(crate) 是本 crate。 */ +export type RustVisibility = 'public' | 'module' | 'super' | 'crate' + +const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let']) + +/** 一条说明的第一句,句号留着。 */ +export function firstSentence(text: string): string { + const trimmed = text.trim() + const sentence = trimmed.match(/^[\s\S]*?[。!?.!?]/) + return (sentence ? sentence[0] : trimmed.split('\n')[0] ?? '').trim() +} + +/** 文档注释去掉记号后的第一句;普通注释是整段去掉记号。 */ +export function commentPlain(node: Node): string { + const doc = node.childForFieldName('doc') + const raw = doc ? doc.text : node.text.replace(/^\/\/[/!]?\s?/, '').replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '') + return raw.replace(/\n/g, ' ').trim() +} + +/** 说明行用的第一句。没有文字时没有。 */ +export function docSentence(node: Node | undefined): string | undefined { + if (!node) + return undefined + const sentence = firstSentence(commentPlain(node)) + return sentence || undefined +} + +/** 空白收成一个空格,方便属性表里的类型、签名。 */ +export function oneLine(text: string): string { + return text.replace(/\s+/g, ' ').trim() +} + +/** 节点上的可见性。pub(in …) 不在这四档里,按本 crate 算(只本包能用)。 */ +export function rustVisibility(node: Node): RustVisibility { + const vis = named(node).find(child => child.type === 'visibility_modifier') + if (!vis) + return 'module' + const words = new Set(vis.children.map(child => child?.type)) + if (words.has('super')) + return 'super' + if (words.has('crate') || words.has('in')) + return 'crate' + return 'public' +} + +/** 公开写在 facts.access 上;其余写在块的 access 属性上,盖过 facts。 */ +export function visibilityFacts(node: Node): { access?: Access, attributes?: BlockAttributes } { + const visibility = rustVisibility(node) + 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(/\)$/, '')) +} + +/** 返回类型原文;没写是 ()。 */ +export function returnText(fn: Node): string { + const returned = fn.childForFieldName('return_type') + return returned ? oneLine(returned.text) : '()' +} + +/** 函数体里直接写的 match,每一支的模式和「做什么」。 */ +export function matchArms(fn: Node): { pattern: string, does: string }[] { + const body = fn.childForFieldName('body') + if (!body) + return [] + const arms: { pattern: string, does: string }[] = [] + for (const statement of named(body)) { + const expr = statement.type === 'expression_statement' ? named(statement)[0] : statement + if (expr?.type !== 'match_expression') + continue + const block = expr.childForFieldName('body') + for (const arm of named(block).filter(child => child.type === 'match_arm')) { + const pattern = arm.childForFieldName('pattern') + if (pattern) + arms.push({ pattern: oneLine(pattern.text), does: armDoes(arm) }) + } + } + return arms +} + +/** 变体携带的数据:没带是空的,元组变体是类型,结构变体是字段原文。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) +} + +/** 写在这条前面、紧挨着的属性(#[…]),从外往里。 */ +export function leadingAttributes(node: Node): Node[] { + const attrs: Node[] = [] + let prev = node.previousNamedSibling + while (prev?.type === 'attribute_item') { + attrs.unshift(prev) + prev = prev.previousNamedSibling + } + return attrs +} + +/** 属性的名字:get、serde、axum::post、tauri::command。 */ +export function attributeName(attributeItem: Node): string | undefined { + const attribute = named(attributeItem).find(child => child.type === 'attribute') + const head = attribute ? named(attribute)[0] : undefined + return head?.text +} + +/** 这条自己的属性,含写在它前面的。 */ +export function itemAttributes(node: Node): Node[] { + return [...leadingAttributes(node), ...named(node).filter(child => child.type === 'attribute_item')] +} + +/** derive(...) 里列出的名字。 */ +export function deriveNames(node: Node): string[] { + const names: string[] = [] + for (const item of itemAttributes(node)) { + if (attributeName(item) !== 'derive') + continue + const attribute = named(item).find(child => child.type === 'attribute') + const tree = attribute?.children.find(child => child?.type === 'token_tree') + if (!tree) + continue + for (const name of named(tree)) { + if (name.type === 'identifier') + names.push(name.text) + } + } + return names +} + +/** 字符串字面量里的原文,不含引号。 */ +export function stringContent(node: Node | undefined): string | undefined { + if (!node) + return undefined + if (node.type === 'string_content') + return node.text + const content = node.children.find(child => child?.type === 'string_content') + return content?.text +} + +/** 宏调用的名字和括号里的具名实参。 */ +export function macroInvocation(node: Node): { name: string, args: Node[] } | undefined { + if (node.type !== 'macro_invocation') + return undefined + const macro = node.childForFieldName('macro') + const args = node.children.find(child => child?.type === 'token_tree') + if (!macro || !args) + return undefined + return { name: macro.text, args: named(args) } +} + +/** 函数写在 impl 或 trait 里面,参数表叫「参数」而不是「输入参数」。 */ +export function isMethod(node: Node): boolean { + const parent = node.parent + return parent?.type === 'declaration_list' && (parent.parent?.type === 'impl_item' || parent.parent?.type === 'trait_item') +} + +/** 结构体的字段:名字、类型、可见性。元组结构体没有名字,不进这张表。 */ +export function structFields(node: Node): { name: Node, type: Node, visibility: RustVisibility }[] { + const body = node.childForFieldName('body') + if (body?.type !== 'field_declaration_list') + return [] + return named(body).flatMap((field) => { + if (field.type !== 'field_declaration') + return [] + const name = field.childForFieldName('name') + const type = field.childForFieldName('type') + return name && type ? [{ name, type, visibility: rustVisibility(field) }] : [] + }) +} + +/** trait 或 impl 主体里的函数(含只写签名的)。 */ +export function bodyFunctions(node: Node): Node[] { + const body = node.childForFieldName('body') + return named(body).filter(child => child.type === 'function_item' || child.type === 'function_signature_item') +} + +export function createRustAdapter(wasmDir: string): LanguageAdapter { + return { + id: 'rust', + languageIds: ['rust'], + grammarPath: () => path.join(wasmDir, 'tree-sitter-rust.wasm'), + toBlocks: root => topBlocks(named(root)), + tokenRole: rustTokenRole, + } +} + +function topBlocks(items: Node[]): CodeBlock[] { + const blocks: CodeBlock[] = [] + let index = 0 + while (index < items.length) { + const item = items[index] + if (isDoc(item)) { + const docs = [item] + let next = index + 1 + while (next < items.length && isDoc(items[next]) && items[next].startPosition.row <= docs[docs.length - 1].endPosition.row + 1) + docs.push(items[next++]) + while (next < items.length && items[next].type === 'attribute_item') + next++ + const target = items[next] + if (target && target.type !== 'line_comment' && target.type !== 'block_comment') { + blocks.push(withPreamble(itemBlock(target), docs)) + index = next + 1 + continue + } + } + if (item.type === 'attribute_item') { + index++ + continue + } + if (item.type === 'line_comment' || item.type === 'block_comment') + blocks.push(leafBlock('comment', item)) + else + blocks.push(withPreamble(itemBlock(item), [])) + index++ + } + return blocks +} + +function itemBlock(item: Node): CodeBlock { + switch (item.type) { + case 'use_declaration': + return useBlock(item) + case 'struct_item': + return structBlock(item) + case 'enum_item': + return enumBlock(item) + case 'impl_item': + return implBlock(item) + case 'trait_item': + return traitBlock(item) + case 'function_item': + return functionBlock(item) + default: + return leafBlock('statement', item) + } +} + +/** 文档注释和写在前面的属性算进头部。文档注释的第一句是说明。 */ +function withPreamble(block: CodeBlock, docs: Node[]): CodeBlock { + const [doc] = docs + const attribute = leadingAttributes(block.node)[0] + let next = block + if (doc && isDocumentedKind(next)) + next = { ...extendHeader(next, doc), facts: { ...next.facts, doc } } as typeof next + const from = doc ?? attribute + if (from && next.header.startIndex > from.startIndex) + next = extendHeader(next, from) + return next +} + +function isDocumentedKind(block: CodeBlock): block is FunctionBlock | ClassBlock | TraitBlock | ImplBlock | EnumBlock { + return block.kind === 'function' || block.kind === 'class' || block.kind === 'trait' || block.kind === 'impl' || block.kind === 'enum' +} + +function isDoc(node: Node): boolean { + return (node.type === 'line_comment' || node.type === 'block_comment') && named(node).some(child => child.type === 'outer_doc_comment_marker') +} + +function useBlock(node: Node): ImportBlock { + const argument = node.childForFieldName('argument') + const { source, items } = argument ? useParts(argument) : { items: [] } + return { kind: 'import', ...leafShape(node), facts: { sources: source ? [source] : [], items } } +} + +function useParts(node: Node): { source?: Node, items: ImportItem[] } { + switch (node.type) { + case 'scoped_identifier': { + const source = node.childForFieldName('path') ?? undefined + const name = node.childForFieldName('name') + return { source, items: name ? [{ kind: 'named', name }] : [] } + } + case 'scoped_use_list': { + const source = node.childForFieldName('path') ?? undefined + const list = node.childForFieldName('list') + return { source, items: list ? useList(list) : [] } + } + case 'use_list': + return { items: useList(node) } + case 'use_as_clause': + return { items: asItem(node) } + case 'use_wildcard': + return { items: [{ kind: 'namespace' }] } + case 'identifier': + return { items: [{ kind: 'named', name: node }] } + default: + return { items: [] } + } +} + +function useList(list: Node): ImportItem[] { + return named(list).flatMap((child) => { + if (child.type === 'identifier') + return [{ kind: 'named' as const, name: child }] + if (child.type === 'use_as_clause') + return asItem(child) + if (child.type === 'use_wildcard') + return [{ kind: 'namespace' as const }] + if (child.type === 'scoped_identifier') { + const name = child.childForFieldName('name') + return name ? [{ kind: 'named' as const, name }] : [] + } + return [] + }) +} + +function asItem(node: Node): ImportItem[] { + const name = node.childForFieldName('path') + const alias = node.childForFieldName('alias') ?? undefined + return name ? [{ kind: 'named', name, alias }] : [] +} + +function structBlock(node: Node): CodeBlock { + const { access, attributes } = visibilityFacts(node) + return { + kind: 'class', + ...shape(node), + ...(attributes && { attributes }), + facts: classFacts(node, access), + } +} + +function enumBlock(node: Node): EnumBlock { + const { access, attributes } = visibilityFacts(node) + const body = node.childForFieldName('body') + const members = named(body).flatMap((variant) => { + if (variant.type !== 'enum_variant') + return [] + const name = variant.childForFieldName('name') + if (!name) + return [] + const payload = variant.childForFieldName('body') ?? undefined + return [{ name, ...(payload && { value: payload }), docs: [] }] + }) + return { + kind: 'enum', + ...shape(node), + ...(attributes && { attributes }), + facts: { name: node.childForFieldName('name') ?? node, doc: undefined, access, decorators: [], bases: [], members }, + } +} + +function traitBlock(node: Node): TraitBlock { + const { access, attributes } = visibilityFacts(node) + return { kind: 'trait', ...shape(node), ...(attributes && { attributes }), facts: classFacts(node, access) } +} + +function implBlock(node: Node): ImplBlock { + const { access, attributes } = visibilityFacts(node) + const body = node.childForFieldName('body') + const children: CodeBlock[] = [] + let docs: Node[] = [] + for (const child of named(body)) { + if (isDoc(child)) { + docs.push(child) + continue + } + if (child.type === 'attribute_item') + continue + if (child.type === 'function_item') + children.push(withPreamble(functionBlock(child), docs)) + else if (child.type === 'const_item' || child.type === 'type_item') + children.push(leafBlock('statement', child)) + docs = [] + } + return { + kind: 'impl', + ...compoundShape(node, body?.startIndex ?? node.endIndex, [], () => children), + ...(attributes && { attributes }), + facts: classFacts(node, access, implType(node)), + } +} + +function functionBlock(node: Node): FunctionBlock { + const { access, attributes } = visibilityFacts(node) + const body = node.childForFieldName('body') + const headerEnd = body?.startIndex ?? node.endIndex + return { + kind: 'function', + node, + header: headerSpan(node, headerEnd), + children: [], + ...(attributes && { attributes }), + facts: { + name: node.childForFieldName('name') ?? undefined, + access, + ...(isAsync(node) && { async: true }), + decorators: leadingAttributes(node), + parameters: parametersOf(node.childForFieldName('parameters')), + returnType: node.childForFieldName('return_type') ?? undefined, + }, + } +} + +function classFacts(node: Node, access: Access | undefined, name = node.childForFieldName('name') ?? undefined): ClassFacts { + return { name, access, decorators: leadingAttributes(node), typeParameters: [], bases: [], implements: [] } +} + +/** impl 的自己的类型:impl Trait for Type 取 Type,impl Type 取 Type。 */ +function implType(node: Node): Node | undefined { + return node.childForFieldName('type') ?? node.childForFieldName('trait') ?? undefined +} + +function shape(node: Node): { node: Node, header: { startIndex: number, endIndex: number, startRow: number, endRow: number }, children: CodeBlock[] } { + const body = node.childForFieldName('body') + return body ? { node, header: headerSpan(node, body.startIndex), children: [] } : { ...leafShape(node) } +} + +function parametersOf(list: Node | null): ParameterFacts[] { + if (!list) + return [] + return named(list).flatMap((child) => { + if (child.type === 'self_parameter') { + const name = [...child.children].reverse().find(token => token?.type === 'self') + return name ? [{ name, optional: false, type: child }] : [] + } + if (child.type !== 'parameter') + return [] + const name = child.childForFieldName('pattern') + const type = child.childForFieldName('type') ?? undefined + return name ? [{ name, optional: false, type }] : [] + }) +} + +function isAsync(node: Node): boolean { + const modifiers = named(node).find(child => child.type === 'function_modifiers') + return modifiers?.children.some(child => child?.type === 'async') ?? false +} + +function armDoes(arm: Node): string { + const comment = firstComment(arm) + if (comment) + return firstSentence(commentPlain(comment)) || '—' + const value = arm.childForFieldName('value') + if (!value) + return '—' + if (value.type === 'return_expression') { + const returned = named(value)[0] + return returned ? `返回 ${oneLine(returned.text)}` : '返回' + } + if (value.type === 'block') { + const inner = firstComment(value) + if (inner) + return firstSentence(commentPlain(inner)) || '—' + const statement = named(value)[0] + return statement ? oneLine(statement.text) : '—' + } + return oneLine(value.text) +} + +function firstComment(node: Node): Node | undefined { + if (node.type === 'line_comment' || node.type === 'block_comment') + return node + for (const child of node.children) { + if (!child) + continue + const found = firstComment(child) + if (found) + return found + } + return undefined +} + +function rustTokenRole(leaf: Node): TokenRole { + if (leaf.type === 'line_comment' || leaf.type === 'block_comment' || inside(leaf, 'line_comment') || inside(leaf, 'block_comment')) + return 'comment' + if (inside(leaf, 'string_literal') || leaf.type === 'string_content') + return 'string' + if (leaf.type === 'integer_literal' || leaf.type === 'float_literal' || leaf.type === 'char_literal') + return 'number' + if (leaf.type === 'primitive_type' || leaf.type === 'type_identifier') + return 'type' + if (leaf.type === 'mutable_specifier') + return 'keyword' + if (!leaf.isNamed) + return STORAGE.has(leaf.type) ? 'storage' : /^[a-z]+$/.test(leaf.type) ? 'keyword' : 'plain' + if (leaf.type === 'identifier' || leaf.type === 'field_identifier' || leaf.type === 'self') { + const parent = leaf.parent?.type + return parent === 'function_item' || parent === 'function_signature_item' ? 'function' : 'variable' + } + return 'plain' +} + +function inside(node: Node, type: string): boolean { + let current = node.parent + while (current) { + if (current.type === type) + return true + current = current.parent + } + return false +} diff --git a/vscode/src/codeReader/languages/shared.ts b/vscode/src/codeReader/languages/shared.ts index 0fd8d1a..2335b45 100644 --- a/vscode/src/codeReader/languages/shared.ts +++ b/vscode/src/codeReader/languages/shared.ts @@ -209,6 +209,8 @@ function ownDoc(block: CodeBlock): Node | undefined { switch (block.kind) { case 'function': case 'class': + case 'trait': + case 'impl': case 'enum': case 'exception': case 'const': diff --git a/vscode/src/codeReader/rules/rustImpl.ts b/vscode/src/codeReader/rules/rustImpl.ts new file mode 100644 index 0000000..8d6dfeb --- /dev/null +++ b/vscode/src/codeReader/rules/rustImpl.ts @@ -0,0 +1,36 @@ +import type { ImplBlock, KindBadge, LibraryRule, RuleCard, ThemeColor } from '../contract' +import { implRows, plainSay } from '../rustRows' + +/** 和函数卡同一个黄,实现块看起来仍是函数那一挂。 */ +const COLOR: ThemeColor = { dark: '#dcdcaa', light: '#795e26' } +const BADGE: KindBadge = { label: '实现', color: COLOR, icon: 'square-function' } + +/** + * impl 的方法表:点方法名展开那张函数卡。 + * 子块放进属性表某一行,和 React 事件处理同一条路。不看导入,Rust 文件都启用。 + */ +export const rustImplRule: LibraryRule = { + id: 'rust', + label: 'Rust 实现', + languageIds: ['rust'], + libraries: [], + claim: block => block.kind === 'impl' ? implCard(block) : undefined, + attributeNames: () => ({ + attributes: {}, + values: { kind: { 'rust.impl': '实现' } }, + }), +} + +function implCard(block: ImplBlock): RuleCard { + const rows = implRows(block, plainSay()) + const row = rows.findIndex(item => item.label === '方法') + const methods = block.children.filter(child => child.kind === 'function') + return { + kind: 'rust.impl', + badge: BADGE, + rows, + ...(block.facts.name && { defines: { startIndex: block.facts.name.startIndex, endIndex: block.facts.name.endIndex } }), + absorbs: [], + ...(row >= 0 && { slots: methods.map((child, item) => ({ block: child, row, item })) }), + } +} diff --git a/vscode/src/codeReader/rustRows.ts b/vscode/src/codeReader/rustRows.ts new file mode 100644 index 0000000..23900d0 --- /dev/null +++ b/vscode/src/codeReader/rustRows.ts @@ -0,0 +1,182 @@ +import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, PropertyItem, PropertyRow, TokenRole, TraitBlock } from './contract' +import type { RustVisibility } from './languages/rust' +import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './languages/rust' + +/** 把类型、名字画成原文。翻译插件传入时,名字带源码下标。 */ +export interface RustSay { + name: (node: import('./contract').Node, role: TokenRole) => Inline[] + source: (node: import('./contract').Node, role?: TokenRole) => Inline[] +} + +const VISIBILITY: Record = { + public: '公开', + module: '本模块', + super: '父模块', + crate: '本 crate', +} + +/** 块上的可见性:属性盖过 facts,没写是本模块。 */ +export function visibilityWord(block: CodeBlock): string { + const overridden = block.attributes?.access + if (overridden && overridden in VISIBILITY) + return VISIBILITY[overridden as RustVisibility] + if ('facts' in block && 'access' in block.facts && block.facts.access === 'public') + return VISIBILITY.public + return VISIBILITY.module +} + +export function importRows(block: ImportBlock, say: RustSay): PropertyRow[] { + const [source] = block.facts.sources + return [ + { label: '来源', value: source ? say.source(source, 'type') : [hint('—')] }, + { + label: '导入项', + value: [hint(`${block.facts.items.length} 个`)], + columns: ['名字', '别名'], + items: block.facts.items.map((item) => { + if (item.kind === 'namespace') + return { cells: [[hint('*')], [hint('—')]] } + if (item.kind !== 'named') + return { cells: [[hint('—')], [hint('—')]] } + return { + cells: [say.name(item.name, 'type'), item.alias ? say.name(item.alias, 'type') : [hint('—')]], + defines: range(item.name), + } + }), + }, + ] +} + +export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] { + const fields = structFields(block.node) + return [ + nameRow(block.facts.name, say, 'type'), + ...docRow(block.facts.doc), + visibilityRow(block), + ...tableRow('字段', ['字段', '类型', '可见性'], fields.map(field => ({ + cells: [ + say.name(field.name, 'variable'), + say.source(field.type, 'type'), + [keyword(VISIBILITY[field.visibility])], + ], + defines: range(field.name), + }))), + ] +} + +export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] { + return [ + nameRow(block.facts.name, say, 'type'), + ...docRow(block.facts.doc), + ...tableRow('变体', ['变体', '携带的数据'], block.facts.members.map(member => ({ + cells: [say.name(member.name, 'type'), [hint(variantPayload(member.value) ?? '—')]], + defines: range(member.name), + }))), + ] +} + +export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] { + const arms = matchArms(block.node) + return [ + nameRow(block.facts.name, say, 'function'), + ...docRow(block.facts.doc), + ...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []), + visibilityRow(block), + ...tableRow(isMethod(block.node) ? '参数' : '输入参数', ['名称', '类型'], block.facts.parameters.map(param => ({ + cells: [say.name(param.name, 'variable'), param.type ? say.source(param.type, 'type') : [hint('—')]], + defines: range(param.name), + }))), + { label: '返回', value: [plain(returnText(block.node))] }, + ...tableRow('分情况', ['分支', '做什么'], arms.map(arm => ({ + cells: [[plain(arm.pattern)], [hint(arm.does)]], + })), '支'), + ] +} + +export function traitRows(block: TraitBlock, say: RustSay): PropertyRow[] { + const methods = bodyFunctions(block.node) + return [ + nameRow(block.facts.name, say, 'type'), + ...docRow(block.facts.doc), + ...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => { + const name = method.childForFieldName('name') + return { + cells: [ + name ? say.name(name, 'function') : [hint('匿名')], + [plain(signatureParams(method))], + [plain(returnText(method))], + ], + ...(name && { defines: range(name) }), + } + })), + ] +} + +export function implRows(block: ImplBlock, say: RustSay): PropertyRow[] { + const methods = block.children.filter((child): child is FunctionBlock => child.kind === 'function') + const title: Inline[] = block.facts.name + ? [...say.name(block.facts.name, 'type'), plain(' 的方法')] + : [hint('实现')] + return [ + { label: '名称', value: title, ...(block.facts.name && { defines: range(block.facts.name) }) }, + ...docRow(block.facts.doc), + ...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => { + const name = method.facts.name + return { + cells: [ + name ? say.name(name, 'function') : [hint('匿名')], + [plain(signatureParams(method.node))], + [plain(returnText(method.node))], + ], + ...(name && { defines: range(name) }), + } + })), + ] +} + +function nameRow(name: import('./contract').Node | undefined, say: RustSay, role: TokenRole): PropertyRow { + return { + label: '名称', + value: name ? say.name(name, role) : [hint('匿名')], + ...(name && { defines: range(name) }), + } +} + +function docRow(doc: import('./contract').Node | undefined): PropertyRow[] { + const sentence = docSentence(doc) + return sentence ? [{ label: '说明', value: { markdown: sentence } }] : [] +} + +function visibilityRow(block: CodeBlock): PropertyRow { + return { label: '可见性', value: [keyword(visibilityWord(block))] } +} + +/** 没有行就不占一行。countUnit 默认「个」,分情况用「支」。 */ +function tableRow(label: string, columns: string[], items: PropertyItem[], countUnit = '个'): PropertyRow[] { + if (items.length === 0) + return [] + return [{ label, value: [hint(`${items.length} ${countUnit}`)], 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' } +} + +function range(node: import('./contract').Node): { startIndex: number, endIndex: number } { + return { startIndex: node.startIndex, endIndex: node.endIndex } +} +/** 名字、类型照抄源码,名字带上下标,点了能跳到定义。 */ +export function plainSay(): RustSay { + return { + name: (node, role) => [{ text: node.text, role, offset: node.startIndex }], + source: (node, role = 'type') => [{ text: oneLine(node.text), role, offset: node.startIndex }], + } +} diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts new file mode 100644 index 0000000..bc0db4d --- /dev/null +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -0,0 +1,105 @@ +import type { FactBlock, Node, PropertyRow, SourceDocument, Translator } from '../contract' +import { enumRows, functionRows, implRows, importRows, plainSay, structRows, traitRows } from '../rustRows' +import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate' + +const say = plainSay() + +/** + * Rust 的中文直译。属性表的用词按代码阅读的 Rust 原型:结构体、可见性、变体、分情况。 + * 关键字和注释仍走中文直译那一套。 + */ +export const rustZhTranslator: Translator = { + ...createZhTemplateTranslator({ + id: 'rust.zh-template', + label: '中文直译', + languageIds: ['rust'], + keywords: { + fn: '函数', + let: '令', + mut: '可变', + pub: '公开', + struct: '结构体', + enum: '枚举', + impl: '实现', + trait: '特质', + use: '导入', + mod: '模块', + match: '匹配', + if: '如果', + else: '否则', + for: '遍历', + while: '当', + loop: '循环', + return: '返回', + async: '异步', + await: '等待', + break: '跳出', + continue: '继续', + where: '其中', + type: '类型', + const: '常量', + static: '静态', + unsafe: '不安全', + as: '作为', + in: '在', + ref: '引用', + move: '移动', + dyn: '动态', + crate: '本 crate', + super: '父模块', + self: '自己', + Self: '本类型', + }, + literals: { true: '真', false: '假' }, + builtinTypes: {}, + isTypePosition: () => false, + operators: {}, + operatorParents: [], + emptyLiterals: {}, + mapLiterals: {}, + stringNodes: ['string_literal'], + commentText, + nodeTemplates: {}, + }), + attributeNames: { + attributes: { ...ZH_ATTRIBUTE_NAMES.attributes, access: '可见性' }, + values: { + ...ZH_ATTRIBUTE_NAMES.values, + kind: { ...KIND_ZH, class: '结构体' }, + access: { + ...ZH_ATTRIBUTE_NAMES.values.access, + public: '公开', + module: '本模块', + super: '父模块', + crate: '本 crate', + }, + }, + }, + describe, +} + +function describe(block: FactBlock, _document: SourceDocument): PropertyRow[] { + switch (block.kind) { + case 'import': + return importRows(block, say) + case 'class': + return structRows(block, say) + case 'enum': + return enumRows(block, say) + case 'function': + return functionRows(block, say) + case 'trait': + return traitRows(block, say) + case 'impl': + return implRows(block, say) + default: + return [] + } +} + +function commentText(node: Node, text: string): string { + const doc = node.childForFieldName('doc') + if (doc) + return doc.text.trim() + return text.replace(/^\/\/[/!]?\s?/, '').replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '').trim() +} diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index c69e126..edb2d77 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -208,10 +208,12 @@ function slotChild(node: Node, slot: Exclude): Node | null } const ATTRIBUTE_ZH: Record = { kind: '种类', access: '访问级别', async: '异步', abstract: '抽象', typeOnly: '仅类型' } -const KIND_ZH: Record = { +export const KIND_ZH: Record = { import: '导入', function: '函数', class: '类', + trait: '特质', + impl: '实现', enum: '枚举', exception: '异常', type: '类型', @@ -249,7 +251,7 @@ const ARGUMENT_COLUMNS = ['参数', '值'] const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' } /** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */ -const ZH_ATTRIBUTE_NAMES: AttributeNames = { +export const ZH_ATTRIBUTE_NAMES: AttributeNames = { attributes: ATTRIBUTE_ZH, values: { kind: KIND_ZH, access: ACCESS_ZH, async: YES_NO, abstract: YES_NO, typeOnly: YES_NO }, } @@ -704,6 +706,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator { case 'function': return functionRows(block.facts, phrases) case 'class': + case 'trait': + case 'impl': return classRows(block.facts, phrases) case 'enum': return enumRows(block.facts, phrases) diff --git a/vscode/webview-ui/src/codeReader/codeReader.css b/vscode/webview-ui/src/codeReader/codeReader.css index 42a72c0..82765a5 100644 --- a/vscode/webview-ui/src/codeReader/codeReader.css +++ b/vscode/webview-ui/src/codeReader/codeReader.css @@ -68,6 +68,8 @@ body.vscode-high-contrast-light { :is([data-kind="import"], [data-kind="copy"]) { --kind: var(--cr-kind-import); } [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="impl"] { --kind: var(--cr-kind-function); } [data-kind="enum"] { --kind: var(--cr-kind-enum); } [data-kind="exception"] { --kind: var(--cr-kind-exception); } :is([data-kind="field"], [data-kind="const"], [data-kind="variable"], [data-kind="label"]) { --kind: var(--cr-kind-data); } diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 0f0801e..6c3d405 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -4,6 +4,7 @@ export type BlockKind = | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable' + | 'trait' | 'impl' | 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment' | 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' | 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'