✨ 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

+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)写项数,各项展开成下一层子行;
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。