✨ feat(vscode): 代码阅读契约加命令规则(按命令名注册、读一条命令)、按位置的参数 / 输出 / 返回、解释器和选项块、分情况 / 命令 / 管道种类、常量色;模板引擎加节点读法;没有定义、高亮提供者的语言用语言插件认的同一文件里的名字跳转、高亮

This commit is contained in:
cruldra committed 2026-09-28 00:44:46 +08:00
1 parent cf09c24730
commit ff2a44f080
17 files changed
+555 -60

No files matched your search

+207 -26
View File
@@ -1,4 +1,4 @@
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 type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandReading, CommandRule, CommandWord, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -51,8 +51,31 @@ export interface ZhTable {
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/
nodeTemplates: Record<string, TemplatePart[]>
/**
* 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`)。
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。
*/
readers?: Record<string, NodeReader>
/** 解释器开关(SwitchFacts.id)的叫法:errexit → 出错就停;没有的写源码里的写法。 */
switches?: Record<string, string>
}
/** 读一个节点时能用的(见 ZhTable.readers)。 */
export interface ReaderContext {
document: SourceDocument
/** 照常翻一个节点(照样认读法、命令),只翻落在要翻的这一段里的部分。 */
say: (node: Node) => Inline[]
/** node 整个落在要翻的这一段里:复合语句的块只翻头部,读法按它决定写不写主体里的东西。 */
whole: (node: Node) => boolean
/** 源码里一个名字照抄、按 role 着色(不给 role 按语言插件定的词性),名字带源码下标。 */
name: (node: Node, role?: TokenRole) => Segment
/** 读一条命令(命令规则读的,没有规则时命令名照抄、各个词照常翻),连重定向;不是命令时没有。 */
command: (node: Node) => CommandReading | undefined
}
/** 一种节点的读法:一行译文,可以带一张排在这一段下面的表(分情况的「情况 / 做什么」);返回 undefined 就照常翻。 */
export type NodeReader = (node: Node, context: ReaderContext) => Inline[] | { text: Inline[], table?: PreviewTable } | undefined
interface Token {
/** 注释是空文字:不进译文,只在前后留一个空格。 */
text: string
@@ -68,14 +91,17 @@ interface Token {
group?: SegmentGroup
}
/** 跨几行写的调用排成一行、实参摊成表(见 emitCall):只排遇到的第一个,它的表记在 table 里。 */
interface CallSlot {
/**
* 排在这一段译文下面的那张表:跨几行写的调用的实参表(见 emitCall),或读法给的表(分情况的「情况 / 做什么」)。
* 一段只排一张,排了的记在 table 里。
*/
interface TableSlot {
table?: PreviewTable
}
/**
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
* calls 是这一段里跨几行写的调用要不要排成一行、实参摊成表;没有就照常翻。
* slot 是这一段下面能不能排一张表(跨几行写的调用、读法给的表);没有就照常翻。
*/
interface Collector {
document: SourceDocument
@@ -84,7 +110,7 @@ interface Collector {
tokens: Token[]
cursor: number
gap: boolean
calls?: CallSlot
slot?: TableSlot
}
const CONTEXT_SEPARATOR = ' > '
@@ -93,7 +119,7 @@ const CLOSING = new Set([')', ']', '}'])
const SEPARATORS = new Set([...CLOSING, ',', ';', ':'])
const HEADER_OPENERS = new Set(['{', ':'])
/** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */
const NAME_ROLES = new Set<TokenRole>(['variable', 'function', 'type'])
const NAME_ROLES = new Set<TokenRole>(['variable', 'constant', 'function', 'type'])
function firstLeaf(node: Node): Node {
const first = node.child(0)
@@ -194,6 +220,11 @@ const KIND_ZH: Record<BlockKind, string> = {
return: '返回',
statement: '语句',
comment: '注释',
interpreter: '解释器',
options: '选项',
case: '分情况',
command: '命令',
pipeline: '管道',
}
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
@@ -363,8 +394,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
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)
function expandedCall(node: Node, span: SourceSpan, slot: TableSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
if (slot.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
@@ -375,9 +406,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
* 一行译文的块、常量 / 变量的值、实参表里每个值各翻各的,各自排自己里面的那个调用。
*/
function expandedText(node: Node, span: SourceSpan, document: SourceDocument): { text: Inline[], table?: PreviewTable } {
const calls: CallSlot = {}
const text = joinTokens(tokensIn(node, span, document, calls))
return { text, ...(calls.table && { table: calls.table }) }
const slot: TableSlot = {}
const text = joinTokens(tokensIn(node, span, document, slot))
return { text, ...(slot.table && { table: slot.table }) }
}
/**
@@ -408,13 +439,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/**
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 calls 里。被调用的名字照常翻、带源码下标,
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 slot 里。被调用的名字照常翻、带源码下标,
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
* 调用后面还紧跟着同一个式子的别的部分(见 continued)时整行括起来:「(新建 X · 3 个参数).model_dump()」。
*/
function emitCall(out: Collector, calls: CallSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
calls.table = argumentTable(facts, span, out.document)
function emitCall(out: Collector, slot: TableSlot, 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)
advance(out, node.startIndex)
@@ -436,6 +467,61 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
out.cursor = node.endIndex
}
/** 读法用的:照常翻、看节点在不在这一段里、照抄名字、读命令。 */
function readerContext(out: Collector, span: SourceSpan): ReaderContext {
const { document } = out
return {
document,
say: node => joinTokens(tokensIn(node, span, document)),
whole: node => node.startIndex >= span.startIndex && node.endIndex <= span.endIndex,
name: (node, role) => segment(node.text, role ?? document.language.tokenRole(node), node.startIndex),
command: (node) => {
const facts = document.language.command?.(node)
return facts && commandReading(facts, document)
},
}
}
/** 读出来的几段接着前面的词输出;读法给了表、这一段还没排表时表排在这一段下面。 */
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)
out.slot.table = below
emitPieces(out, node, text)
}
function emitPieces(out: Collector, node: Node, pieces: Inline[]): void {
advance(out, node.startIndex)
for (const piece of pieces) {
if (!('role' in piece))
emit(out, { text: '', role: 'plain', translated: true, raw: '', group: piece })
else if (piece.text)
emit(out, { text: piece.text, role: piece.role, translated: true, raw: '', ...(piece.offset !== undefined && { offset: piece.offset }) })
}
out.cursor = node.endIndex
}
/**
* 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(带源码下标)、各个词照常翻。
* 后面跟着重定向的意思(见 redirectText)。
*/
function commandReading(facts: CommandFacts, document: SourceDocument): CommandReading {
const say = (node: Node): Inline[] => expression(node, document)
const ruleFor = (name: CommandWord): CommandRule | undefined => document.commandRule?.(name.literal ?? name.node.text)
const context: CommandContext = {
displayLanguage: 'zh',
translate: say,
command: (name, args) => ruleFor(name)?.read({ name, arguments: args, redirects: [] }, context),
}
const reading = ruleFor(facts.name)?.read(facts, context)
const name = facts.name.node
const text = reading?.text ?? [
segment(name.text, document.language.tokenRole(firstLeaf(name)), name.startIndex),
...facts.arguments.flatMap(arg => [segment(' ', 'plain', undefined), ...say(arg.node)]),
]
return { text: [...text, ...facts.redirects.flatMap(redirect => redirectText(redirect, say))], ...(reading?.value && { value: true }) }
}
function collect(node: Node, span: SourceSpan, out: Collector): void {
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
return
@@ -449,10 +535,20 @@ 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)
const { slot } = out
const call = slot && expandedCall(node, span, slot, out.document)
if (slot && call) {
emitCall(out, slot, node, call, span)
return
}
const reading = table.readers?.[node.type]?.(node, readerContext(out, span))
if (reading) {
emitReading(out, node, reading)
return
}
const command = language.command?.(node)
if (command) {
emitPieces(out, node, commandReading(command, out.document).text)
return
}
const parts = templateParts(node, span)
@@ -486,9 +582,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
/** 按译文顺序收集 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 }) }
/** 按译文顺序收集 node 落在 span 里的词;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Token[] {
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
collect(node, span, out)
return out.tokens
}
@@ -531,13 +627,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
if (block.kind === 'comment')
return { text: translateComment(block, document) }
const calls: CallSlot = {}
const tokens = tokensIn(block.node, block.header, document, calls)
const slot: TableSlot = {}
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), ...(calls.table && { table: calls.table }) }
return { text: joinTokens(tokens), ...(slot.table && { table: slot.table }) }
}
function expression(node: Node, document: SourceDocument): Inline[] {
@@ -559,6 +655,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
translated: node => within(node, nodeSpan(node)),
within,
expanded: node => expandedText(node, clipped(nodeSpan(node)), document),
body: node => expression(node, document),
returned: (node) => {
const [value] = language.command?.(node)?.arguments ?? []
return value ? expression(value.node, document) : []
},
entryCount: (literal, size) => {
const name = table.mapLiterals[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
@@ -580,6 +681,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return exceptionRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
case 'interpreter':
return interpreterRows(block.facts)
case 'options':
return optionsRows(block.facts, table.switches ?? {})
default:
return variableRows(block, phrases)
}
@@ -659,6 +764,10 @@ interface Phrases {
within: (node: Node, span: SourceSpan) => Inline[]
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
/** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */
body: (node: Node) => Inline[]
/** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */
returned: (node: Node) => Inline[]
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
entryCount: (literal: Node, size: number) => Segment[]
/** 源码原样照抄、按源码着色(装饰器)。 */
@@ -675,6 +784,31 @@ function hint(text: string): Segment {
return { text, role: 'hint' }
}
const STREAM_ZH: Record<'output' | 'error' | 'both', string> = { output: '输出', error: '错误输出', both: '全部输出' }
/**
* 重定向的意思,括号里淡色写在命令后面:`>&2` 打印到错误输出,`2>&1` 错误输出并进输出,`2>/dev/null` 丢掉错误输出,
* `> f` 写到 f,`>> f` 追加到 f,`< f` 从 f 读。
*/
function redirectText(redirect: Redirect, say: (node: Node) => Inline[]): Inline[] {
const note = (...parts: Inline[]): Inline[] => [hint('('), ...parts, hint(')')]
switch (redirect.kind) {
case 'merge':
if (redirect.stream === redirect.into)
return []
return note(hint(redirect.into === 'error' ? '打印到错误输出' : '错误输出并进输出'))
case 'read':
return note(hint('从 '), ...say(redirect.source), hint(' 读'))
default: {
const stream = STREAM_ZH[redirect.stream]
if (redirect.target.text === '/dev/null')
return note(hint(`丢掉${stream}`))
const where = redirect.kind === 'append' ? '追加到 ' : '写到 '
return note(hint(redirect.stream === 'output' ? where : `${stream}${where}`), ...say(redirect.target))
}
}
}
/** 布尔属性的一行:没有这个概念的语言(facts 里没填)没有这一行。 */
function flagRows(attribute: 'async' | 'abstract' | 'typeOnly', value: boolean | undefined): PropertyRow[] {
return value === undefined ? [] : [{ label: ATTRIBUTE_ZH[attribute], value: [{ text: YES_NO[`${value}`], role: 'storage' }] }]
@@ -728,11 +862,58 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
...accessRows(facts.access),
...flagRows('async', facts.async),
...decoratorRows(facts.decorators, say),
parameterRow('输入参数', facts.parameters, say),
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
...(facts.positional
? positionalRows(facts, facts.positional, say)
: [
parameterRow('输入参数', facts.parameters, say),
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
]),
]
}
/**
* 按位置取参数的函数(shell):输入参数写「N 个,按位置」,下面一张表「位置 | 函数里叫」(没起名的写 $N);
* 输出是它打印的那几句(调用处用 $(函数 …) 拿去用时注明);返回是写了的 return 值,没写 return、也不打印时是最后一句成不成功。
*/
function positionalRows(facts: FunctionFacts, positional: PositionalParameter[], say: Phrases): PropertyRow[] {
const inputs: PropertyRow = positional.length === 0
? { label: '输入参数', value: [hint('无')] }
: {
label: '输入参数',
value: [hint(`${positional.length} 个,按位置`)],
columns: ['位置', '函数里叫'],
items: positional.map(param => ({
cells: [[hint(`第 ${param.position} 个`)], param.name ? say.name(param.name, 'variable') : [segment(`$${param.position}`, 'variable', undefined)]],
...defining(param.name),
})),
}
const prints = facts.prints ?? []
const captured = facts.captured && facts.name ? [hint(`(调用处用 $(${facts.name.text} …) 拿到)`)] : []
const output: PropertyRow[] = prints.length > 0 ? [{ label: '输出', value: [...joined(prints.map(say.body), '\n'), ...captured] }] : []
const returns = facts.returns ?? []
const values = returns.map(say.returned).filter(value => value.length > 0)
const result: PropertyRow[] = values.length > 0
? [{ label: '返回', value: joined(values, '、') }]
: prints.length === 0 ? [{ label: '返回', value: [hint('最后一句成不成功')] }] : []
return [inputs, ...output, ...result]
}
/** 解释器:叫什么,怎么找到它(按 PATH 找,或写死的路径)。 */
function interpreterRows(facts: InterpreterFacts): PropertyRow[] {
return [
{ label: '解释器', value: [segment(facts.program, 'function', undefined)] },
{ label: '怎么找', value: [hint(facts.lookup === 'env' ? `按 PATH 去找(${facts.command})` : `写死的路径(${facts.command})`)] },
]
}
/** 解释器开关:每个开关一行,属性名是开关的意思(没有叫法的写源码里的写法),值是开没开,后面淡色写源码里的写法。 */
function optionsRows(facts: OptionsFacts, names: Record<string, string>): PropertyRow[] {
return facts.switches.map(item => ({
label: names[item.id] ?? item.flag,
value: [{ text: YES_NO[`${item.on}`], role: 'storage' }, { text: ' ', role: 'plain' }, hint(item.flag)],
}))
}
/** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow {
return {