✨ feat(vscode): 代码阅读的链式调用卡片:语言插件读链、翻译模板排成起点和步骤

契约加 LanguageAdapter.chain、Translator.chain、RuleContext.chain、PropertyRow.chain、TextTranslation.rows;跨行写或实参里有闭包的链排成卡片,连着同名调用合成一步列出每次实参,注释挂在步骤上,闭包收起、子块挂到那一步(BlockView.step);Rust 插件读链、标准库常见方法写中文名;挤成一行时另起一行的 . 贴着前面
This commit is contained in:
cruldra committed 2026-10-15 20:54:59 +11:00
1 parent 80eda7c18e
commit ee595f3da9
16 files changed
+1041 -72

No files matched your search

+245 -4
View File
@@ -1,5 +1,5 @@
import type { BlockView } from '../blockView'
import type { PreviewText } from '../contract'
import type { ChainArgument, ChainFacts, PreviewText } from '../contract'
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { blockCodes, toBlockViews } from '../blockView'
@@ -440,7 +440,7 @@ describe('rust 函数体拆成子块', () => {
{ at: 11, kind: 'statement', kids: [{ at: 12, kind: 'statement' }] },
{ at: 14, kind: 'statement', kids: [{ at: 15, kind: 'statement' }] },
{ at: 17, kind: 'variable', kids: [{ at: 17, kind: 'return' }] },
{ at: 18, kind: 'statement', kids: [{ at: 19, kind: 'statement' }] },
{ at: [18, 20], kind: 'statement', kids: [{ at: 19, kind: 'statement' }] },
{ at: 21, kind: 'return' },
] },
])
@@ -475,8 +475,12 @@ describe('rust 函数体拆成子块', () => {
' 名称: task',
' 初始值: 异步',
' return 17: 返回 1',
' statement 18: items.iter().for_each(|x|',
' statement 19: println!("{x}");',
' statement 18',
' 起点: items',
' 步骤: 2 个',
' ● 遍历',
' ● 逐个执行 | ⟦闭包||x| · 1 句⟧',
' statement 19 @1: println!("{x}");',
' return 21: 返回 add(1, 2)',
])
})
@@ -851,3 +855,240 @@ describe('rust 注释挂到它讲的项上', () => {
])
})
})
/** 链读出的结构写成文字:每步是 [方法名, 泛型实参, 实参, 套在外面的 .await / ?, 注释],收起来的实参写成「种类:句数」。 */
function chainSummary(facts: ChainFacts): unknown {
const argument = ({ node, closure, deferred }: ChainArgument): string => {
if (deferred)
return `${deferred.kind}:${deferred.statements}${deferred.match ? `:match ${deferred.match.value.text}·${deferred.match.arms}` : ''}`
return closure ? `closure:${node.text}` : node.text
}
return {
start: facts.start.text,
steps: facts.steps.map(step => [
step.method.text,
step.typeArguments?.text ?? '',
step.arguments.map(argument),
step.after.map(node => node.type),
step.comments.map(node => node.text.trim()),
]),
}
}
describe('rust 链式调用', () => {
/** 每一句的式子(let 的值、表达式语句的式子)读出的链,不是链的写 none。 */
const chains = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'rust', source, (node) => {
const value = node.type === 'let_declaration' ? node.childForFieldName('value') : node.type === 'expression_statement' ? node.firstNamedChild : null
if (!value)
return undefined
const facts = adapter.chain?.(value)
return facts ? chainSummary(facts) : 'none'
})
it('两步以上的 .方法(…) 从外往里读:.await、? 归它前面那一步,写在 . 前面的注释归这一步、行尾的归上一步;不是方法调用的式子(连同它的 ?)是起点,取字段、一步的不算链', async () => {
const source = [
'fn f() {',
' let body = client',
' // 先发请求',
' .get::<Page>(URL, 3) // 第一页',
' .send()',
' .await?;',
' let p = app.path().app_data_dir()?.join("x");',
' x.a().b.c();',
' one.only();',
' foo()?.bar().baz();',
'}',
].join('\n')
expect(await chains(source)).toEqual([
{ start: 'client', steps: [
['get', '<Page>', ['URL', '3'], [], ['// 先发请求', '// 第一页']],
['send', '', [], ['await_expression', 'try_expression'], []],
] },
{ start: 'app', steps: [
['path', '', [], [], []],
['app_data_dir', '', [], ['try_expression'], []],
['join', '', ['"x"'], [], []],
] },
'none',
'none',
{ start: 'foo()?', steps: [['bar', '', [], [], []], ['baz', '', [], [], []]] },
])
})
it('实参里的闭包:主体是 {}、或只有一个 match 的闭包和 async 块收起来,记下参数、句数、match 的值和几支;主体是一个式子的闭包只标出是闭包', async () => {
const source = [
'fn f() {',
' rows.iter().filter(|r| r.active).for_each(|r| {',
' show(r);',
' // 注释不算一句',
' log(r);',
' });',
' builder.build().run(|app, event| match event { A => 1, _ => 2 });',
' items.into_iter().for_each(|x| { match x { _ => {} } });',
' task.spawn().then(async { 1 });',
'}',
].join('\n')
expect(await chains(source)).toEqual([
{ start: 'rows', steps: [['iter', '', [], [], []], ['filter', '', ['closure:|r| r.active'], [], []], ['for_each', '', ['closure:2'], [], []]] },
'none',
'none',
{ start: 'builder', steps: [['build', '', [], [], []], ['run', '', ['closure:1:match event·2'], [], []]] },
{ start: 'items', steps: [['into_iter', '', [], [], []], ['for_each', '', ['closure:1:match x·1'], [], []]] },
'none',
{ start: 'task', steps: [['spawn', '', [], [], []], ['then', '', ['async:1'], [], []]] },
])
})
it('带收起来的闭包的链是一整块,头部是整句,子块是各个闭包主体里的各句(闭包主体是 match 时是那一块条件),实参是链的那一步取小链里的闭包', async () => {
expect(await tree(readFixture('rustChain.rs'))).toEqual([
{ at: 1, kind: 'import' },
{ at: 2, kind: 'import' },
{ at: 4, kind: 'function', kids: [
{ at: 5, kind: 'variable' },
{ at: [7, 39], kind: 'statement', kids: [
{ at: 21, kind: 'variable' },
{ at: 22, kind: 'statement' },
{ at: 23, kind: 'return' },
{ at: 26, kind: 'statement' },
{ at: 36, kind: 'if', kids: [
{ at: 37, kind: 'if', kids: [{ at: 37, kind: 'return' }] },
{ at: 38, kind: 'if' },
] },
] },
] },
{ at: [42, 43], kind: 'function' },
{ at: [45, 46], kind: 'function' },
])
expect(await tree('fn f() {\n app.plugin(\n b.new()\n .setup(|x| {\n go(x);\n }),\n ).run();\n}\n')).toEqual([
{ at: 1, kind: 'function', kids: [{ at: [2, 7], kind: 'statement', kids: [{ at: 5, kind: 'statement' }] }] },
])
})
it('跨行写的链排成链式调用卡片:起点、步骤两行,一步一个「方法 | 实参」,连着同名的合成一步列出每次的实参,注释挂在步骤上,闭包收起来,子块挂在各自那一步下面', async () => {
const [run] = (await views(readFixture('rustChain.rs'))).filter(block => block.kind === 'function')
expect(viewLines(run ? [run] : [])).toEqual([
'function 4',
' 名称: run',
' 可见性: 公开',
' 返回: ()',
' variable 5',
' 名称: app_state',
' 初始值: AppState::default()',
' statement 7',
' 起点: tauri::Builder::default()',
' 步骤: 8 个',
' ● .plugin | 4 次',
' 注释: 新窗口链接只走主窗口 on_new_window 一条路',
' 实参',
' tauri_plugin_opener::Builder::new().open_js_links_on_click(false).build()',
' tauri_plugin_dialog::init()',
' tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, None)',
' tauri_plugin_updater::Builder::new().build()',
' ● .setup | ⟦闭包||app| · 3 句⟧',
' ● .on_window_event | ⟦闭包||window, event| · 1 句⟧',
' ● .manage | app_state',
' ● .invoke_handler | tauri::generate_handler![ device_resume, settings_get, web::preview_open ]',
' ● .build | tauri::generate_context!()',
' ● .expect | "error while building tauri application"',
' ● .run | ⟦闭包||app, event| · match event · 2 支⟧',
' variable 21 @1',
' 名称: store',
' 初始值: app.path().app_data_dir()?.join("tunnels.json")',
' statement 22 @1: app.manage(Arc::new(Tunnels::new(store)));',
' return 23 @1: 返回 Ok(())',
' statement 26 @2: window.hide();',
' if 36 @7: 匹配 event',
' if 37: RunEvent::Exit',
' return 37: 返回 app.state::<Arc<Host>>().shutdown()',
' if 38: _',
])
})
it('写在一行里、实参里有闭包的链也排;变量的初始值是起点,链接在下面;标准库常见方法写中文名;.await、? 照常翻、跟在那一步后面;返回值同样排;单行又没闭包的照旧一行', async () => {
const source = [
'async fn load(client: Client, rows: Vec<Row>) -> Result<Vec<u8>, Error> {',
' let names: Vec<String> = rows.iter().filter(|r| r.active).map(|r| r.name.clone()).collect();',
' let body = client',
' .get(URL)',
' .send()',
' .await?;',
' let p = app.path().app_data_dir()?.join("x");',
' body.bytes()',
' .await',
' .map(|b| b.to_vec())',
'}',
].join('\n')
expect(viewLines((await views(source))[0]?.children ?? [])).toEqual([
'variable 2',
' 名称: names',
' 类型: Vec<String>',
' 初始值: rows',
' ● 遍历',
' ● 筛选 | |r| r.active',
' ● 映射 | |r| r.name.clone()',
' ● 收集',
'variable 3',
' 名称: body',
' 初始值: client',
' ● .get | URL',
' ● .send | 等待?',
'variable 7',
' 名称: p',
' 初始值: app.path().app_data_dir()?.join("x")',
'return 8: 返回',
' 起点: body',
' 步骤: 2 个',
' ● .bytes | 等待',
' ● 映射 | |b| b.to_vec()',
])
})
it('实参本身是跨行写的链时缩进成一条小链;连着几次同名调用、每次几个实参时按位置分列', async () => {
const source = [
'fn f() {',
' tauri::Builder::default()',
' .plugin(',
' opener::Builder::new()',
' .open(false)',
' .build(),',
' )',
' .manage(state);',
' Router::new()',
' .route("/a", get(list))',
' .route("/b", post(create))',
' .with_state(state);',
'}',
].join('\n')
expect(viewLines((await views(source))[0]?.children ?? [])).toEqual([
'statement 2',
' 起点: tauri::Builder::default()',
' 步骤: 2 个',
' ● .plugin',
' ○ opener::Builder::new()',
' ○ .open(false)',
' ○ .build()',
' ● .manage | state',
'statement 9',
' 起点: Router::new()',
' 步骤: 2 个',
' ● .route | 2 次',
' 第 1 个 | 第 2 个',
' "/a" | get(list)',
' "/b" | post(create)',
' ● .with_state | state',
])
})
it('方法的中文名悬停看原名,⌘ 点击跳到方法名;照抄的实参、起点、方法名都带自己的下标', async () => {
const source = 'fn f() {\n let n = rows.iter().map(|r| r.id).collect();\n}\n'
const [variable] = (await views(source))[0]?.children ?? []
const row = propertyRow(variable, '初始值')
const [iter, map] = row.chain ?? []
const label = iter?.name[0]
expect(label && 'role' in label ? { text: label.text, refers: label.refers, tooltip: label.tooltip } : undefined)
.toEqual({ text: '遍历', refers: { startIndex: source.indexOf('iter'), endIndex: source.indexOf('iter') + 4 }, tooltip: '.iter' })
expect(wordsOf(row.value, 'variable')).toEqual([['rows', source.indexOf('rows')]])
expect(wordsOf(map?.value, 'variable')).toEqual([['r', source.indexOf('|r|') + 1], ['r', source.indexOf('r.id')], ['id', source.indexOf('r.id') + 2]])
expect(map?.range).toEqual({ startIndex: source.indexOf('.map'), endIndex: source.indexOf('.collect') })
})
})
+147 -6
View File
@@ -1,8 +1,8 @@
import type { Access, AttributeFacts, AttributeItem, BlockShape, CallFacts, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TextKind, TokenRole, TraitBlock } from '../contract'
import type { Access, AttributeFacts, AttributeItem, BlockShape, CallFacts, CfgPredicate, ChainArgument, ChainFacts, ChainStepFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeferredFacts, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TextKind, TokenRole, TraitBlock } from '../contract'
import type { CommentRules, ListBlocks } from './shared'
import * as path from 'node:path'
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, isCommentNode, leafBlock, leafShape, listBlocks, named, syntaxChildren } from './shared'
import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, isCommentNode, leafBlock, leafShape, listBlocks, named, nodeSpan, syntaxChildren } from './shared'
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
@@ -17,9 +17,146 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter {
freshDoc,
call: rustCall,
collection: rustCollection,
chain: rustChain,
}
}
/**
* 两步以上的链式调用 `a.b(…).c(…)`:从最外面往里剥,每层 `.方法(…)` 是一步,套在调用外面的 `.await`、`?` 归它里面那一步,
* 剥到不是方法调用的式子就是起点(连同套在它外面的 `.await`、`?`)。宏调用、取字段(`x.a().b.c()` 的 `.b`)不算一步,链从那里断开。
*/
function rustChain(node: Node): ChainFacts | undefined {
const steps: ChainStepFacts[] = []
let current: Node | null = node
let after: Node[] = []
while (current) {
if (current.type === 'await_expression' || current.type === 'try_expression') {
after = [current, ...after]
current = syntaxChildren(current)[0] ?? null
continue
}
const step = methodStep(current, after)
if (!step)
break
steps.unshift(step.facts)
after = []
current = step.receiver
}
const start = after[after.length - 1] ?? current
if (steps.length < 2 || !start)
return undefined
return { start, steps: withTrailingComments(steps, start) }
}
/** 一层 `.方法(实参)`(方法名后面可以带 `::<…>`):这一步和它的接收者;不是方法调用时没有。after 是套在它外面的 `.await`、`?`,从里往外。 */
function methodStep(call: Node, after: Node[]): { facts: ChainStepFacts, receiver: Node } | undefined {
const fn = call.type === 'call_expression' ? call.childForFieldName('function') : null
const argumentList = call.childForFieldName('arguments')
const generic = fn?.type === 'generic_function' ? fn : undefined
const target = generic ? generic.childForFieldName('function') : fn
const method = target?.type === 'field_expression' ? target.childForFieldName('field') : null
const receiver = target?.childForFieldName('value')
const dot = target && children(target).find(child => child.type === '.')
if (!argumentList || argumentList.type !== 'arguments' || method?.type !== 'field_identifier' || !receiver || !dot)
return undefined
const typeArguments = generic?.childForFieldName('type_arguments') ?? undefined
return {
receiver,
facts: {
dot,
method,
...(typeArguments && { typeArguments }),
call,
argumentList,
arguments: syntaxChildren(argumentList).filter(arg => arg.type !== 'attribute_item').map(chainArgument),
after,
comments: children(target).filter(child => isCommentNode(child) && child.endIndex <= dot.startIndex),
},
}
}
function chainArgument(node: Node): ChainArgument {
const deferred = deferredFacts(node)
return { node, closure: node.type === 'closure_expression' || node.type === 'async_block', ...(deferred && { deferred }) }
}
/** 收起来的闭包、async 块(见 [DeferredFacts]):主体是 `{}`、或闭包主体直接是 match;`{}` 里只有一个 match 也算主体只有一个 match。 */
function deferredFacts(node: Node): DeferredFacts | undefined {
const parameters = node.type === 'closure_expression' ? node.childForFieldName('parameters') ?? undefined : undefined
const kind = node.type === 'closure_expression' ? 'closure' : 'async'
const body = deferredBody(node) ?? (node.type === 'closure_expression' ? node.childForFieldName('body') : null)
if (!body || (body.type !== 'block' && body.type !== 'match_expression'))
return undefined
const items = body.type === 'block' ? statementItems(body) : [body]
const [only] = items
const match = items.length === 1 && only ? matchOf(only) : undefined
return {
kind,
...(parameters && { parameters }),
statements: items.length,
...(match && { match: { node: match, value: match.childForFieldName('value') ?? match, arms: named(match.childForFieldName('body')).filter(arm => arm.type === 'match_arm').length } }),
}
}
/** 一句就是一个 match(带不带分号)时是那个 match。 */
function matchOf(item: Node): Node | undefined {
const expression = item.type === 'expression_statement' ? syntaxChildren(item)[0] : item
return expression?.type === 'match_expression' ? expression : undefined
}
/** `{}` 里写的各句:注释、`#[…]`、多出来的空 `;` 不算。 */
function statementItems(block: Node): Node[] {
return blockItems(block).filter(item => !isCommentNode(item) && item.type !== 'attribute_item' && item.type !== 'empty_statement')
}
/** 写在 `.` 前面、和上一步的结尾同一行的注释是上一步的行尾注释,归上一步。 */
function withTrailingComments(steps: ChainStepFacts[], start: Node): ChainStepFacts[] {
return steps.map((step, i) => {
const next = steps[i + 1]
const trailing = next ? next.comments.filter(comment => comment.startPosition.row === stepEnd(step).endPosition.row) : []
const before = i > 0 ? stepEnd(steps[i - 1]).endPosition.row : start.endPosition.row
const own = step.comments.filter(comment => i === 0 || comment.startPosition.row !== before)
return { ...step, comments: [...own, ...trailing] }
})
}
/** 一步的结尾:调用,套着 `.await`、`?` 时是最外面那层。 */
function stepEnd(step: ChainStepFacts): Node {
return step.after[step.after.length - 1] ?? step.call
}
/**
* 链里收起来的闭包、async 块的主体,照源码顺序:各步的实参,只有一个实参、实参本身是链的那一步取那条小链各步的实参
* (翻译插件把它排成小链,见 [链的一步](vscode/src/codeReader/contract.ts#ChainStep) 的 lines)。
*/
function deferredBodies(chain: ChainFacts): Node[] {
return chain.steps.flatMap((step) => {
const [only] = step.arguments
const inner = step.arguments.length === 1 && only ? rustChain(only.node) : undefined
const args = inner ? inner.steps.flatMap(item => item.arguments) : step.arguments
return args.flatMap(arg => arg.deferred ? [arg.node] : [])
})
}
/** 收起来的闭包、async 块主体里的各句 → 块:主体是 `{}` 时同函数体,闭包主体直接是 match 时是那一块条件。 */
function deferredBlocks(node: Node): CodeBlock[] {
const body = deferredBody(node)
if (body)
return statements(blockItems(body), children(body).find(child => child.type === '{')?.startPosition.row ?? body.startPosition.row, true)
const match = node.childForFieldName('body')
return match?.type === 'match_expression' ? [matchBlock(match, match, true)] : []
}
/**
* 值是带收起来的闭包、async 块的链式调用时块的形状:头部是整句(链式调用卡片讲整条链),子块是那几个闭包、async 块主体里的各句,
* 宿主按位置挂到各自那一步下面;别的值没有。
*/
function chainShape(outer: Node, value: Node | null): BlockShape | undefined {
const chain = value ? rustChain(value) : undefined
const bodies = chain ? deferredBodies(chain) : []
return bodies.length > 0 ? { node: outer, header: nodeSpan(outer), children: bodies.flatMap(deferredBlocks) } : undefined
}
/**
* 调用 `f(a)`、`obj.method(a)`、`Type::new(a)`、`parse::<u8>(s)` 的结构,实参都是按位置传的;
* 元组结构体、变体(`Some(x)`)写法和调用一样,不给 construct,翻译插件问名字的定义是哪种块。
@@ -462,6 +599,7 @@ function statementBlocks(item: Node, tail: boolean): CodeBlock[] {
/**
* 当一句写的式子 → 块:outer 是整句(表达式语句,或一层最后那个不带分号的式子本身)。
* 条件、match、循环、`{}` 照控制流拆;是这一层的值(tail)时是 return;别的是 statement,带着闭包、async 块时子块来自它(见 [callbackBody])。
* 式子(return 的值)是带收起来的闭包的链式调用时,子块来自链里那几个闭包(见 [chainShape])。
*/
function expressionBlocks(outer: Node, expression: Node, tail: boolean): CodeBlock[] {
switch (expression.type) {
@@ -483,9 +621,11 @@ function expressionBlocks(outer: Node, expression: Node, tail: boolean): CodeBlo
case 'async_block':
return [bodyBlock('statement', outer, deferredBody(expression), true)]
case 'return_expression':
return [leafBlock('return', outer)]
return [{ kind: 'return', ...(chainShape(outer, syntaxChildren(expression)[0] ?? null) ?? leafShape(outer)) }]
default:
return [tail ? leafBlock('return', outer) : bodyBlock('statement', outer, callbackBody(expression), true)]
if (tail)
return [{ kind: 'return', ...(chainShape(outer, expression) ?? leafShape(outer)) }]
return [{ kind: 'statement', ...(chainShape(outer, expression) ?? bodyShape(outer, callbackBody(expression), true)) }]
}
}
@@ -518,7 +658,8 @@ function armBlock(arm: Node, tail: boolean): CodeBlock {
/**
* `let` → variable,模式是名字;值是主体写成 `{}` 的闭包时是 function,同 TypeScript 把箭头函数赋给变量。
* `let … else` 的 else 是它的子块;值带着闭包、async 块时子块来自它(见 [callbackBody])。
* `let … else` 的 else 是它的子块;值是带收起来的闭包的链式调用时子块来自那几个闭包(见 [chainShape]),
* 别的值带着闭包、async 块时子块来自它(见 [callbackBody])。
*/
function letBlock(node: Node): CodeBlock {
const pattern = node.childForFieldName('pattern')
@@ -530,7 +671,7 @@ function letBlock(node: Node): CodeBlock {
const alternative = node.childForFieldName('alternative')
return {
kind: 'variable',
...(alternative ? letElseShape(node, alternative) : bodyShape(node, value && callbackBody(value), true)),
...(alternative ? letElseShape(node, alternative) : chainShape(node, value) ?? bodyShape(node, value && callbackBody(value), true)),
facts: { declarators: [{ name: pattern, type: node.childForFieldName('type') ?? undefined, value: value ?? undefined }] },
}
}