✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新

This commit is contained in:
cruldra committed 2026-09-27 22:08:16 +08:00
1 parent ebf228cdc7
commit ecc415d763
30 files changed
+1033 -157

No files matched your search

@@ -1,4 +1,4 @@
import type { Inline, LanguageAdapter, PropertyItem, PropertyRow, Segment, TokenRole, Translator } from '../contract'
import type { BlockKind, CallFacts, Inline, LanguageAdapter, PreviewTable, PropertyItem, PropertyRow, Segment, SourceDocument, TokenRole, Translator } from '../contract'
import type { ZhTable } from './zhTemplate'
import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from '../languages/python'
@@ -11,34 +11,37 @@ import { createZhTemplateTranslator } from './zhTemplate'
const ts = createTypeScriptAdapter(WASM_DIR)
const py = createPythonAdapter(WASM_DIR)
/** 一块的译文:一行译文在 segments 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
interface Translated { kind: string, segments: Inline[], markdown?: string }
/** 一块的译文:一行译文在 segments 里,下面摊开实参的表在 table 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
interface Translated { kind: string, segments: Inline[], markdown?: string, table?: PreviewTable }
/** 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
/**
* 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。
* calleeKind 同 SourceDocument.calleeKind(调用的名字定义成哪种块),不给就是宿主不查。
*/
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, calleeKind?: SourceDocument['calleeKind']): Promise<Map<number, Translated>> {
return withBlocks(adapter, languageId, source, (blocks) => {
const byLine = new Map<number, Translated>()
walk(blocks, (block) => {
const line = block.header.startRow + 1
if (byLine.has(line) || 'facts' in block)
return
const translation = translator.translate(block, { source, language: adapter, markers: [] })
const { text: translation, table } = translator.translate(block, { source, language: adapter, markers: [], ...(calleeKind && { calleeKind }) })
byLine.set(line, 'markdown' in translation
? { kind: block.kind, segments: [], markdown: translation.markdown }
: { kind: block.kind, segments: translation })
: { kind: block.kind, segments: translation, ...(table && { table }) })
})
return byLine
})
}
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引。 */
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, PropertyRow[]>> {
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引;calleeKind 同 translateAll。 */
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, calleeKind?: SourceDocument['calleeKind']): Promise<Map<number, PropertyRow[]>> {
return withBlocks(adapter, languageId, source, (blocks) => {
const byLine = new Map<number, PropertyRow[]>()
walk(blocks, (block) => {
const line = block.header.startRow + 1
if (!byLine.has(line) && 'facts' in block)
byLine.set(line, translator.describe(block, { source, language: adapter, markers: [] }))
byLine.set(line, translator.describe(block, { source, language: adapter, markers: [], ...(calleeKind && { calleeKind }) }))
})
return byLine
})
@@ -165,12 +168,12 @@ describe('typeScript 中文直译', () => {
it('多行合成一行、去掉右括号前的尾逗号、行中注释不进译文', async () => {
const source = [
'call(',
'list = [',
' first, // 第一个',
' [],',
')',
']',
].join('\n')
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('call( first, 空数组 )')
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('list = [ first, 空数组 ]')
})
it('as / in / extends / default 按所在语法节点换译法', async () => {
@@ -417,8 +420,10 @@ describe('python 写在表达式里的遍历(推导式)', () => {
5: '⟦集合|遍历 pairs 的每个 n, _,取 n⟧',
6: '⟦列表|遍历 grid 的每个 row,取 ⟦列表|遍历 row 的每个 y,取 y⟧⟧',
7: 'pick(a, ⟦生成器|遍历 b 的每个 y,取 y⟧)',
8: 'sum(⟦生成器|遍历 counts 的每个 n,取 n⟧)',
8: '调用 sum · 1 个参数',
})
// 跨几行写的调用唯一的实参是生成器:共用的那对括号属于调用,实参表里只有框
expect(propertyLines(map.get(8) ?? []).at(-1)).toBe(' 第 1 个 | ⟦生成器|遍历 counts 的每个 n,取 n⟧')
const async = valueOf(map.get(3), '初始值')[0]
expect('segments' in async && async.segments.filter(piece => 'role' in piece && piece.role === 'keyword')).toEqual([
{ text: '异步遍历', role: 'keyword' },
@@ -1028,6 +1033,173 @@ describe('源码下标(跳到定义用)', () => {
})
})
describe('跨几行写的调用', () => {
const routes = readFixture('routes.py')
const calls = readFixture('calls.ts.txt')
/** 调用的名字定义成哪种块(按被调用的表达式原文查,没有的是还没查到),并记下问了哪些。 */
function kinds(known: Record<string, BlockKind>): { asked: string[], calleeKind: (call: CallFacts) => BlockKind | undefined } {
const asked: string[] = []
return {
asked,
calleeKind: (call) => {
asked.push(call.callee.text)
return known[call.callee.text]
},
}
}
function tableLines(table: PreviewTable | undefined): string[] {
return (table?.items ?? []).map(item => item.cells.map(segmentText).join(' | '))
}
const USER_DETAIL = [
'id | user.id',
'username | user.username',
'display_name | user.display_name',
'role | user.role.value',
'email | user.email',
'phone | user.phone',
'bio | user.bio',
'avatar_url | user.avatar_url',
'is_active | user.is_active',
'created_at | user.created_at',
'updated_at | user.updated_at',
]
it('返回里新建对象(名字定义成类):一行「返回 新建 UserDetailResponse · 11 个参数」,实参摊成表(参数 | 值),按名字传的写名字', async () => {
const { asked, calleeKind } = kinds({ UserDetailResponse: 'class' })
const line = (await translateAll(py, 'python', pythonZhTranslator, routes, calleeKind)).get(45)
expect(colors(line?.segments)).toEqual([
{ text: '返回', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: '新建', role: 'storage' },
{ text: ' ', role: 'plain' },
{ text: 'UserDetailResponse', role: 'type' },
{ text: ' ', role: 'plain' },
{ text: '· 11 个参数', role: 'hint' },
])
expect(line?.table?.columns).toEqual(['参数', '值'])
expect(tableLines(line?.table)).toEqual(USER_DETAIL)
expect(asked).toContain('UserDetailResponse')
// 被调用的名字、按名字传的名字、值里的名字都带源码下标:能跳到定义、显示中文名
const call = routes.indexOf('UserDetailResponse(')
expect(line?.segments.find(piece => 'role' in piece && piece.role === 'type')).toMatchObject({ offset: call })
expect(line?.table?.items[0].cells).toEqual([
[{ text: 'id', role: 'variable', offset: routes.indexOf('id=user.id', call) }],
[
{ text: 'user', role: 'variable', offset: routes.indexOf('user.id', call) },
{ text: '.', role: 'plain' },
{ text: 'id', role: 'variable', offset: routes.indexOf('user.id', call) + 5 },
],
])
})
it('名字的定义还没查到、查不到、是函数都按调用算:被调用的名字按函数着色', async () => {
for (const calleeKind of [undefined, kinds({}).calleeKind, kinds({ UserDetailResponse: 'function' }).calleeKind]) {
const line = (await translateAll(py, 'python', pythonZhTranslator, routes, calleeKind)).get(45)
expect(text(line?.segments)).toBe('返回 调用 UserDetailResponse · 11 个参数')
expect(line?.segments.find(piece => 'role' in piece && piece.text === 'UserDetailResponse')).toMatchObject({ role: 'function' })
}
const exception = (await translateAll(py, 'python', pythonZhTranslator, routes, kinds({ UserDetailResponse: 'exception' }).calleeKind)).get(96)
expect(text(exception?.segments)).toBe('返回 新建 UserDetailResponse · 11 个参数')
})
it('变量的初始值里跨几行调用:初始值一行「等待 调用 user_service.update_user · 10 个参数」,实参表展开在这一行下面,按位置传的写「第 N 个」', async () => {
const rows = (await describeAll(py, 'python', pythonZhTranslator, routes, kinds({}).calleeKind)).get(83) ?? []
expect(propertyLines(rows)).toEqual([
'名称: user',
'初始值: 等待 调用 user_service.update_user · 10 个参数',
' 第 1 个 | db',
' user_id | user_id',
' display_name | body.display_name',
' email | body.email',
' phone | body.phone',
' bio | body.bio',
' avatar_url | body.avatar_url',
' role | body.role',
' is_active | body.is_active',
' password | body.password',
])
const value = rows.find(row => row.label === '初始值')
expect(value?.columns).toEqual(['参数', '值'])
expect(colors(value?.value)).toEqual([
{ text: '等待', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: '调用', role: 'storage' },
{ text: ' ', role: 'plain' },
{ text: 'user_service', role: 'variable' },
{ text: '.', role: 'plain' },
{ text: 'update_user', role: 'function' },
{ text: ' ', role: 'plain' },
{ text: '· 10 个参数', role: 'hint' },
])
expect(value?.items?.[0].cells[0]).toEqual([{ text: '第 1 个', role: 'hint' }])
})
it('一行写完的调用照旧一行译文,不问定义', async () => {
const { asked, calleeKind } = kinds({})
const map = await describeAll(py, 'python', pythonZhTranslator, routes, calleeKind)
expect(propertyLines(map.get(43) ?? [])).toEqual(['名称: user', '初始值: 等待 user_service.get_user(db, user_id=user_id)'])
expect(propertyLines(map.get(118) ?? [])).toEqual(['名称: payload', '初始值: 等待 file.read()'])
expect(asked).not.toContain('user_service.get_user')
expect(asked).not.toContain('file.read')
})
it('*x 写「展开」、**x 写「展开关键字」,「第 N 个」只数按位置传的;实参里的调用不再排成表', async () => {
const source = [
'run(',
' first,',
' *rest,',
' second,',
' key=Inner(',
' x=1,',
' ),',
' **options,',
')',
].join('\n')
const line = (await translateAll(py, 'python', pythonZhTranslator, source)).get(1)
expect(text(line?.segments)).toBe('调用 run · 5 个参数')
expect(tableLines(line?.table)).toEqual(['第 1 个 | first', '展开 | rest', '第 2 个 | second', 'key | Inner( x=1 )', '展开关键字 | options'])
})
it('typeScript:new 写法就是新建,不带 new 的是调用,都不问定义;...x 写「展开」', async () => {
const { asked, calleeKind } = kinds({})
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, calls, calleeKind)
expect(text(texts.get(18)?.segments)).toBe('this.definitions = 新建 DefinitionSources · 1 个参数')
expect(tableLines(texts.get(18)?.table)).toEqual([
'第 1 个 | { rule: 异步 reference => ruleDefinitions(等待 owner.resolveReference(reference)), language: offset => languageDefinitions(document.uri, document.positionAt(offset)) }',
])
expect(text(texts.get(33)?.segments)).toBe('返回 调用 concatAll · 2 个参数')
expect(tableLines(texts.get(33)?.table)).toEqual(['第 1 个 | base', '展开 | extra'])
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, calls, calleeKind)
// 实参是模板字符串:照抄
expect(propertyLines(rows.get(7) ?? []).slice(1)).toEqual([
'初始值: 等待 调用 window.showWarningMessage · 3 个参数',
expect.stringMatching(/^ {2}第 1 个 \| 如果 branch 则 `确认删除 worktree [^`]+` 否则 `确认删除 worktree [^`]+`$/),
' 第 2 个 | { modal: 真 }',
' 第 3 个 | \'删除\'',
])
expect(propertyLines(rows.get(26) ?? []).slice(1)).toEqual(['初始值: 新建 URL · 1 个参数', expect.stringMatching(/^ {2}第 1 个 \| `[^`]+\/comments`$/)])
expect(asked).toEqual([])
})
it('链式调用跨了几行、实参列表各自写在一行里的不排成表;回调主体不在块头部里的调用也不排', async () => {
const source = [
'const ids = items',
' .filter(a => a.ok)',
' .map(a => a.id)',
'describe(\'x\', () => {',
' run()',
'})',
].join('\n')
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
expect(propertyLines(rows.get(1) ?? []).at(-1)).toBe('值: items .filter(a => a.ok) .map(a => a.id)')
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
expect(texts.get(4)).toEqual(expect.not.objectContaining({ table: expect.anything() }))
})
})
describe('中文模板引擎', () => {
function testTranslator(table: Partial<ZhTable>): Translator {
return createZhTemplateTranslator({
@@ -1091,14 +1263,14 @@ describe('中文模板引擎', () => {
const source = [
'n += a if k in seen else b',
'print(a if c else b if d else e)',
'print((a # 注释不占序号',
' if k == 1 else b))',
'return (a # 注释不占序号',
' if k == 1 else b)',
].join('\n')
const map = await translateAll(pythonColored('variable'), 'python', translator, source)
expect(texts(map)).toEqual({
1: '令 n 加上 若 k 在 seen 则 a 不然 b',
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
3: 'print((若 k 等于 1 则 a 不然 b))',
3: 'return (若 k 等于 1 则 a 不然 b)',
})
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword').map(segment => segment.text))
.toEqual(['令', '加上', '若', '则', '不然'])