✨ feat(vscode): 代码阅读预览里实参值、return 这类一行译文里跨几行写的字典 / 列表按项拆开,各项带自己的实参表

This commit is contained in:
cruldra committed 2026-10-13 13:57:22 +11:00
1 parent b2c5309d1f
commit 907c4092c8
18 files changed
+515 -102

No files matched your search

+75 -35
View File
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, Doc, ElementFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CollectionFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, Doc, ElementFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { ParamDocs } from '../rules/docRows'
import { readDoc } from '../docTags'
import { commonIndent, splitLines } from '../editText'
@@ -107,16 +107,17 @@ interface Token {
}
/**
* 排在这一段译文下面的那张表:跨几行写的调用的实参表(见 emitCall),或读法给的表(分情况的「情况 / 做什么」)。
* 一段只排一张,排了的记在 table 里。
* 排在这一段译文下面的那一样:跨几行写的调用的实参表(见 emitCall)、读法给的表(分情况的「情况 / 做什么」),
* 或跨几行写的字面量拆开的各项(见 emitCollection)。一段只排一样,排了的记在这里。
*/
interface TableSlot {
interface Slot {
items?: PropertyItem[]
table?: PreviewTable
}
/**
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
* slot 是这一段下面能不能排一张表(跨几行写的调用、读法给的表);没有就照常翻。
* slot 是这一段下面能不能排一样东西(跨几行写的调用、字面量,读法给的表);没有就照常翻。
*/
interface Collector {
document: SourceDocument
@@ -125,7 +126,7 @@ interface Collector {
tokens: Token[]
cursor: number
gap: boolean
slot?: TableSlot
slot?: Slot
}
const CONTEXT_SEPARATOR = ' > '
@@ -171,6 +172,11 @@ function continued(node: Node, root: Node): boolean {
return false
}
/** 这一段下面已经排了一样东西(见 Slot)。 */
function taken(slot: Slot): boolean {
return slot.items !== undefined || slot.table !== undefined
}
function isComment(token: Token): boolean {
return token.text === '' && !token.group
}
@@ -412,47 +418,58 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
out.cursor = node.endIndex
}
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
function expandedCall(node: Node, span: SourceSpan, slot: TableSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
if (slot.table || !call || !inside(node, span))
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的。 */
function expandedCall(node: Node, span: SourceSpan, slot: Slot, { language: { call } }: SourceDocument): CallFacts | undefined {
if (taken(slot) || !call || !inside(node, span))
return undefined
const facts = call(node)
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
}
/** 要按项拆开的字面量:语言插件给了各项(LanguageAdapter.collection)、不是空的,跨了几行,整个落在要翻的这一段里,这一段还没排过别的。 */
function expandedCollection(node: Node, span: SourceSpan, slot: Slot, { language: { collection } }: SourceDocument): CollectionFacts | undefined {
if (taken(slot) || !collection || !inside(node, span))
return undefined
const facts = collection(node)
if (!facts || (facts.kind === 'map' ? facts.entries : facts.elements).length === 0)
return undefined
return node.startPosition.row !== node.endPosition.row ? facts : undefined
}
/**
* node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall);
* 给了 call(node 整个就是这次调用)时不管写成几行都这样排。一行译文的块、属性表里每一格的值各翻各的,各自排自己里面的那个调用。
* node 落在 span 里的那一段翻成一行译文,里面先遇到的(先外后里)跨几行写的调用排成一行「新建 / 调用 …」、实参摊成表(见 emitCall),
* 或跨几行写的字面量排成一行「列表 2 项」、各项拆成子行(见 emitCollection);同一段里后面再有的照常一行。
* 给了 call(node 整个就是这次调用)时不管写成几行都这样排。一行译文的块、属性表里每一格的值各是一段,各自排自己里面的那一个。
*/
function expandedText(node: Node, span: SourceSpan, document: SourceDocument, call?: CallFacts): CellValue {
const slot: TableSlot = {}
const slot: Slot = {}
const out = collector(node, span, document, slot)
if (call)
emitCall(out, slot, node, call, span)
else
collect(node, span, out)
return { text: joinTokens(out.tokens), ...(slot.table && { table: slot.table }) }
return { text: joinTokens(out.tokens), ...slot }
}
/**
* 属性表里一格的值(常量 / 变量的值、字典一项的值、列表的一项、实参的值),只翻落在 span 里的部分:
* 语言插件给了各项的字面量(LanguageAdapter.collection)写叫法和项数,各项拆成下一层子行(见 entryItem、elementItems)——
* 字典 / 对象一直拆(maps 为假时照旧一行:实参的值),列表 / 元组 / 集合 / 数组跨几行写才拆;
* 空字面量和别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,实参表跟着这一格(见 expandedText)。
* 属性表里一格的值(常量 / 变量的值、字典一项的值、列表的一项、实参的值),只翻落在 span 里的部分,见 expandedText;
* maps 为真时(实参的值以外)值整个是字典 / 对象字面量的写在一行里也拆,空字面量照旧一行(「空字典」)。
*/
function cellValue(node: Node, span: SourceSpan, document: SourceDocument, maps = true): CellValue {
const collection = document.language.collection?.(node)
if (collection?.kind === 'map' && maps && collection.entries.length > 0)
return { text: itemCount(node, collection.entries.length), items: collection.entries.map(entry => entryItem(entry, span, document)) }
if (collection?.kind === 'sequence' && collection.elements.length > 0 && node.startPosition.row !== node.endPosition.row)
return { text: itemCount(node, collection.elements.length), items: elementItems(collection.elements, span, document) }
const collection = maps ? document.language.collection?.(node) : undefined
if (collection?.kind === 'map' && collection.entries.length > 0)
return collectionValue(node, collection, span, document)
return expandedText(node, span, document)
}
/** 拆开的字面量写叫法和项数(展开的也算一项):「字典 3 项」,叫法按字面量的节点类型查表(ZhTable.collections)。 */
function itemCount(literal: Node, size: number): Segment[] {
/**
* 拆开的字面量:写叫法和项数(展开的也算一项),叫法按字面量的节点类型查表(ZhTable.collections):「字典 3 项」;
* 各项是下一层子行,字典 / 对象见 entryItem,列表 / 元组 / 集合 / 数组见 elementItems。
*/
function collectionValue(literal: Node, facts: CollectionFacts, span: SourceSpan, document: SourceDocument): { text: Segment[], items: PropertyItem[] } {
const items = facts.kind === 'map' ? facts.entries.map(entry => entryItem(entry, span, document)) : elementItems(facts.elements, span, document)
const name = table.collections[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
return { text: [hint(name ? `${name} ${items.length} 项` : `${items.length} 项`)], items }
}
/**
@@ -492,8 +509,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/**
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值见 cellValue(字典 / 对象照旧一行):值里有跨几行写的调用时
* 同样排成一行,它的实参表排在值那一格里(PropertyItem.table);值是跨几行写的列表时各项拆在那一格里(PropertyItem.items)。一层层往下套。
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值见 cellValue(写在一行里的字典 / 对象照旧一行):值里有跨几行写的调用时
* 同样排成一行,它的实参表排在值那一格里(PropertyItem.table);值里有跨几行写的字典 / 列表时各项拆在那一格里(PropertyItem.items)。一层层往下套。
*/
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
let position = 0
@@ -523,7 +540,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
* 调用后面还紧跟着同一个式子的别的部分(见 continued)时整行括起来:「(新建 X · 3 个参数).model_dump()」。
*/
function emitCall(out: Collector, slot: TableSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
function emitCall(out: Collector, slot: Slot, node: Node, facts: CallFacts, span: SourceSpan): void {
slot.table = argumentTable(facts, span, out.document)
const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts))
const wrapped = continued(node, out.root)
@@ -546,6 +563,24 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
out.cursor = node.endIndex
}
/**
* 跨几行写的字面量排成一行「列表 2 项」「字典 3 项」,各项拆成子行记在 slot 里(见 collectionValue)。
* 后面还紧跟着同一个式子的别的部分(见 continued)时整个括起来:「(列表 2 项) + rest」。
*/
function emitCollection(out: Collector, slot: Slot, node: Node, facts: CollectionFacts, span: SourceSpan): void {
const { text, items } = collectionValue(node, facts, span, out.document)
slot.items = items
const wrapped = continued(node, out.root)
advance(out, node.startIndex)
if (wrapped)
emit(out, { text: '(', role: 'plain', translated: true, raw: '' })
for (const piece of text)
emit(out, { text: piece.text, role: piece.role, translated: true, raw: '' })
if (wrapped)
emit(out, { text: ')', role: 'plain', translated: true, raw: '' })
out.cursor = node.endIndex
}
/** 读法用的:照常翻、看节点在不在这一段里、照抄名字、读命令。 */
function readerContext(out: Collector, span: SourceSpan): ReaderContext {
const { document } = out
@@ -561,10 +596,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
/** 读出来的几段接着前面的词输出;读法给了表、这一段还没排表时表排在这一段下面。 */
/** 读出来的几段接着前面的词输出;读法给了表、这一段下面还空着时表排在这一段下面。 */
function emitReading(out: Collector, node: Node, reading: Inline[] | { text: Inline[], table?: PreviewTable }): void {
const { text, table: below } = Array.isArray(reading) ? { text: reading, table: undefined } : reading
if (below && out.slot && !out.slot.table)
if (below && out.slot && !taken(out.slot))
out.slot.table = below
emitPieces(out, node, text)
}
@@ -624,6 +659,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
emitCall(out, slot, node, call, span)
return
}
const collection = slot && expandedCollection(node, span, slot, out.document)
if (slot && collection) {
emitCollection(out, slot, node, collection, span)
return
}
const reading = table.readers?.[node.type]?.(node, readerContext(out, span))
if (reading) {
emitReading(out, node, reading)
@@ -665,13 +705,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
/** 翻 node 落在 span 里的那一段用的收集器;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
function collector(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Collector {
/** 翻 node 落在 span 里的那一段用的收集器;给了 slot,先遇到的跨几行写的调用、字面量排开(见 expandedText)。 */
function collector(node: Node, span: SourceSpan, document: SourceDocument, slot?: Slot): Collector {
return { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
}
/** 按译文顺序收集 node 落在 span 里的词;slot 见 collector。 */
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Token[] {
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: Slot): Token[] {
const out = collector(node, span, document, slot)
collect(node, span, out)
return out.tokens
@@ -719,13 +759,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
const group = block.group && table.groups?.[block.node.type]
if (block.group && group)
return group(block.group, readerContext(collector(block.node, block.header, document), block.header))
const slot: TableSlot = {}
const slot: Slot = {}
const tokens = tokensIn(block.node, block.header, document, slot)
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 { text: joinTokens(tokens), ...(slot.table && { table: slot.table }) }
return { text: joinTokens(tokens), ...slot }
}
function expression(node: Node, document: SourceDocument): Inline[] {