✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新
This commit is contained in:
1 parent
ebf228cdc7
commit
ecc415d763
30 files changed
+1033
-157
No files matched your search
@@ -53,8 +53,8 @@ const translator = createZhTemplateTranslator({
|
||||
|
||||
function translateFirst(language: LanguageAdapter, source: string): Promise<string> {
|
||||
return withBlocks(language, 'fake', source, ([block]) => {
|
||||
const translation = translator.translate(block as TextBlock, { source, language, markers: [] })
|
||||
return 'markdown' in translation ? translation.markdown : segmentText(translation)
|
||||
const { text } = translator.translate(block as TextBlock, { source, language, markers: [] })
|
||||
return 'markdown' in text ? text.markdown : segmentText(text)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(['令', '加上', '若', '则', '不然'])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewTable, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -68,12 +68,23 @@ interface Token {
|
||||
group?: SegmentGroup
|
||||
}
|
||||
|
||||
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
|
||||
/** 跨几行写的调用排成一行、实参摊成表(见 emitCall):只排遇到的第一个,它的表记在 table 里。 */
|
||||
interface CallSlot {
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
|
||||
* calls 是这一段里跨几行写的调用要不要排成一行、实参摊成表;没有就照常翻。
|
||||
*/
|
||||
interface Collector {
|
||||
document: SourceDocument
|
||||
/** 要翻的那个节点 */
|
||||
root: Node
|
||||
tokens: Token[]
|
||||
cursor: number
|
||||
gap: boolean
|
||||
calls?: CallSlot
|
||||
}
|
||||
|
||||
const CONTEXT_SEPARATOR = ' > '
|
||||
@@ -165,6 +176,9 @@ const KIND_ZH: Record<BlockKind, string> = {
|
||||
}
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
|
||||
/** 被调用的名字定义成这几种块时,调用就是新建对象。 */
|
||||
const CONSTRUCTED = new Set<BlockKind | undefined>(['class', 'exception', 'enum'])
|
||||
const ARGUMENT_COLUMNS = ['参数', '值']
|
||||
const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' }
|
||||
|
||||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||||
@@ -302,9 +316,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { style: 'iteration', label: ITERATION_ZH[facts.result], segments }
|
||||
}
|
||||
|
||||
/** 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 */
|
||||
/**
|
||||
* 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。
|
||||
* 只翻这个遍历(它是调用的实参,排在实参表里)时外面的调用不在译文里,这对括号也不写。
|
||||
*/
|
||||
function emitIteration(out: Collector, node: Node, facts: IterationFacts, span: SourceSpan): void {
|
||||
const shared = facts.sharedParentheses
|
||||
const alone = node.startIndex === out.root.startIndex && node.endIndex === out.root.endIndex
|
||||
const shared = alone ? undefined : facts.sharedParentheses
|
||||
const bracket = (paren: Node): Omit<Token, 'gap' | 'raw'> => ({ text: paren.text, role: out.document.language.tokenRole(paren), translated: false })
|
||||
if (shared)
|
||||
emitSource(out, shared.open, bracket(shared.open))
|
||||
@@ -318,6 +336,60 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
|
||||
function expandedCall(node: Node, span: SourceSpan, calls: CallSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
|
||||
if (calls.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
return undefined
|
||||
const facts = call(node)
|
||||
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
|
||||
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs);值照常翻,里面的调用不再排成表。
|
||||
*/
|
||||
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
|
||||
const value = (node: Node): Inline[] => joinTokens(tokensIn(node, span, document))
|
||||
let position = 0
|
||||
const label = (arg: CallArgument): Inline[] => {
|
||||
switch (arg.kind) {
|
||||
case 'keyword':
|
||||
return [segment(arg.name.text, 'variable', offsetOf(arg.name, document.language))]
|
||||
case 'positional':
|
||||
position++
|
||||
return [hint(`第 ${position} 个`)]
|
||||
case 'spread':
|
||||
return [hint('展开')]
|
||||
case 'keywordSpread':
|
||||
return [hint('展开关键字')]
|
||||
}
|
||||
}
|
||||
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(arg => ({ cells: [label(arg), value(arg.value)] })) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 calls 里。被调用的名字照常翻、带源码下标,
|
||||
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
|
||||
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
|
||||
*/
|
||||
function emitCall(out: Collector, calls: CallSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
|
||||
calls.table = argumentTable(facts, span, out.document)
|
||||
const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts))
|
||||
advance(out, node.startIndex)
|
||||
emit(out, { text: construct ? '新建' : '调用', role: 'storage', translated: true, raw: '' })
|
||||
out.gap = true
|
||||
out.cursor = facts.callee.startIndex
|
||||
const from = out.tokens.length
|
||||
collect(facts.callee, span, out)
|
||||
for (const token of out.tokens.slice(from)) {
|
||||
if (token.offset !== undefined && NAME_ROLES.has(token.role))
|
||||
token.role = token.offset !== facts.name?.startIndex ? 'variable' : construct ? 'type' : 'function'
|
||||
}
|
||||
out.gap = true
|
||||
emit(out, { text: `· ${facts.arguments.length} 个参数`, role: 'hint', translated: true, raw: '' })
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Collector): void {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
|
||||
return
|
||||
@@ -331,6 +403,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
emitIteration(out, node, iteration, span)
|
||||
return
|
||||
}
|
||||
const { calls } = out
|
||||
const call = calls && expandedCall(node, span, calls, out.document)
|
||||
if (calls && call) {
|
||||
emitCall(out, calls, node, call, span)
|
||||
return
|
||||
}
|
||||
const parts = templateParts(node, span)
|
||||
if (parts) {
|
||||
advance(out, node.startIndex)
|
||||
@@ -362,9 +440,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
|
||||
/** 按译文顺序收集 node 落在 span 里的词。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument): Token[] {
|
||||
const out: Collector = { document, tokens: [], cursor: span.startIndex, gap: false }
|
||||
/** 按译文顺序收集 node 落在 span 里的词;给了 calls,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, calls?: CallSlot): Token[] {
|
||||
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(calls && { calls }) }
|
||||
collect(node, span, out)
|
||||
return out.tokens
|
||||
}
|
||||
@@ -404,15 +482,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { markdown: docText(nodes.map(node => commentOf(node, document))) }
|
||||
}
|
||||
|
||||
function translate(block: TextBlock, document: SourceDocument): PreviewText {
|
||||
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block, document)
|
||||
const tokens = tokensIn(block.node, block.header, document)
|
||||
return { text: translateComment(block, document) }
|
||||
const calls: CallSlot = {}
|
||||
const tokens = tokensIn(block.node, block.header, document, calls)
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
const last = tokens.at(-1)
|
||||
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
|
||||
tokens.pop()
|
||||
return joinTokens(tokens)
|
||||
return { text: joinTokens(tokens), ...(calls.table && { table: calls.table }) }
|
||||
}
|
||||
|
||||
function expression(node: Node, document: SourceDocument): Inline[] {
|
||||
@@ -428,13 +507,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const { header } = block
|
||||
const { language } = document
|
||||
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => {
|
||||
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
return joinTokens(tokensIn(node, span, document))
|
||||
}
|
||||
const clipped = (part: SourceSpan): SourceSpan => part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => joinTokens(tokensIn(node, clipped(part), document))
|
||||
const phrases: Phrases = {
|
||||
translated: node => within(node, nodeSpan(node)),
|
||||
within,
|
||||
expanded: (node) => {
|
||||
const calls: CallSlot = {}
|
||||
const text = joinTokens(tokensIn(node, clipped(nodeSpan(node)), document, calls))
|
||||
return { text, ...(calls.table && { table: calls.table }) }
|
||||
},
|
||||
entryCount: (literal, size) => {
|
||||
const name = table.mapLiterals[literal.type]
|
||||
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
|
||||
@@ -533,6 +615,8 @@ interface Phrases {
|
||||
translated: (node: Node) => Inline[]
|
||||
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
|
||||
within: (node: Node, span: SourceSpan) => Inline[]
|
||||
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
|
||||
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
|
||||
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
|
||||
entryCount: (literal: Node, size: number) => Segment[]
|
||||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||||
@@ -760,7 +844,7 @@ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,字典 / 对象字面量见 literalValue);
|
||||
* 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,见 declaredValue);
|
||||
* 说明(有才显示)是整条语句的,排在第一个名称后面。
|
||||
*/
|
||||
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
@@ -770,10 +854,21 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
...(i === 0 ? docRows(block.facts.doc, say) : []),
|
||||
...accessRows(block.facts.access),
|
||||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||||
{ label: valueLabel, ...(declarator.value ? literalValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明的值:字典 / 对象字面量见 literalValue;别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,
|
||||
* 实参摊成表(参数 | 值)展开在这一行下面。
|
||||
*/
|
||||
function declaredValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
|
||||
if (entries?.length)
|
||||
return literalValue(value, entries, say)
|
||||
const { text, table } = say.expanded(value)
|
||||
return table ? { value: text, ...table } : { value: text }
|
||||
}
|
||||
|
||||
/**
|
||||
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
|
||||
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
|
||||
|
||||
Reference in new issue
Block a user