✨ 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

@@ -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(['令', '加上', '若', '则', '不然'])
+112 -17
View File
@@ -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)写项数,各项展开成下一层子行;
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。