✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新
This commit is contained in:
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(['令', '加上', '若', '则', '不然'])
|
||||
|
||||
Reference in new issue
Block a user