✨ feat(vscode): 代码阅读能读嵌在一种语言里的另一种语言的代码:语言插件报出是哪几段,宿主交给那种语言的插件只解析这几段、分步、翻成子块并叠着上色,外层翻译插件只问读出来的样子;契约加构建文件的几种块(解析指令、阶段、执行、复制、设置、标签、健康检查、启动)、分作用范围的变量、并成一块的几条语句,引擎排这些块的属性表

This commit is contained in:
cruldra committed 2026-09-28 11:53:47 +08:00
1 parent 834673391e
commit 3353cf870c
11 files changed
+603 -103

No files matched your search

+168 -7
View File
@@ -1,4 +1,4 @@
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 type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, 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 { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -56,8 +56,12 @@ export interface ZhTable {
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。
*/
readers?: Record<string, NodeReader>
/** 并成一块的几条同种语句(TextBlock.group)怎么读:按第一条的节点类型查,读成一行字加一张表;没有的照常翻第一条。 */
groups?: Record<string, GroupReader>
/** 解释器开关(SwitchFacts.id)的叫法:errexit → 出错就停;没有的写源码里的写法。 */
switches?: Record<string, string>
/** 解析指令(ParserDirective.name)的叫法和补充说明:syntax → 语法(BuildKit 按这个版本读这份文件);没有的写原名。 */
directives?: Record<string, { label: string, note?: string }>
}
/** 读一个节点时能用的(见 ZhTable.readers)。 */
@@ -76,6 +80,9 @@ export interface ReaderContext {
/** 一种节点的读法:一行译文,可以带一张排在这一段下面的表(分情况的「情况 / 做什么」);返回 undefined 就照常翻。 */
export type NodeReader = (node: Node, context: ReaderContext) => Inline[] | { text: Inline[], table?: PreviewTable } | undefined
/** 并成一块的几条同种语句的读法(见 ZhTable.groups):照源码顺序的各条。 */
export type GroupReader = (nodes: Node[], context: ReaderContext) => { text: Inline[], table?: PreviewTable }
interface Token {
/** 注释是空文字:不进译文,只在前后留一个空格。 */
text: string
@@ -225,6 +232,14 @@ const KIND_ZH: Record<BlockKind, string> = {
case: '分情况',
command: '命令',
pipeline: '管道',
directive: '解析指令',
stage: '阶段',
run: '执行',
copy: '复制',
setting: '设置',
label: '标签',
health: '健康检查',
start: '启动',
}
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
@@ -502,8 +517,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/**
* 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(带源码下标)、各个词照常翻。
* 后面跟着重定向的意思(见 redirectText)。
* 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(按函数着色、带源码下标)、
* 各个词照常翻。后面跟着重定向的意思(见 redirectText);规则给了表(选项写得多的命令)就带着。
*/
function commandReading(facts: CommandFacts, document: SourceDocument): CommandReading {
const say = (node: Node): Inline[] => expression(node, document)
@@ -516,10 +531,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
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),
segment(facts.name.literal ?? name.text, 'function', 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 }) }
return {
text: [...text, ...facts.redirects.flatMap(redirect => redirectText(redirect, say))],
...(reading?.value && { value: true }),
...(reading?.table && { table: reading.table }),
}
}
function collect(node: Node, span: SourceSpan, out: Collector): void {
@@ -548,7 +567,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
const command = language.command?.(node)
if (command) {
emitPieces(out, node, commandReading(command, out.document).text)
emitReading(out, node, commandReading(command, out.document))
return
}
const parts = templateParts(node, span)
@@ -627,6 +646,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
if (block.kind === 'comment')
return { text: translateComment(block, document) }
const group = block.group && table.groups?.[block.node.type]
if (block.group && group) {
const out: Collector = { document, root: block.node, tokens: [], cursor: block.header.startIndex, gap: false }
return group(block.group, readerContext(out, block.header))
}
const slot: TableSlot = {}
const tokens = tokensIn(block.node, block.header, document, slot)
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
@@ -669,6 +693,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node, language))] : verbatim(node, document, 'type'),
name: (node, role) => [segment(node.text, role, offsetOf(node, language))],
doc: node => doc(node, document),
command: (form) => {
if (form.form === 'exec')
return expandedText(form.node, clipped(nodeSpan(form.node)), document)
const reading = document.embedded?.(form.code.node)
return reading ? embeddedText(reading) : { text: verbatim(form.code.node, document) }
},
}
switch (block.kind) {
case 'function':
@@ -685,8 +715,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return interpreterRows(block.facts)
case 'options':
return optionsRows(block.facts, table.switches ?? {})
case 'directive':
return directiveRows(block.facts, table.directives ?? {})
case 'stage':
return stageRows(block.facts, phrases)
case 'health':
return healthRows(block.facts, phrases)
case 'start':
return startRows(block.facts, phrases)
default:
return variableRows(block, phrases)
return block.facts.scope ? scopedVariableRows(block.facts.declarators, block.facts.scope, phrases) : variableRows(block, phrases)
}
}
@@ -778,6 +816,20 @@ interface Phrases {
name: (node: Node, role: TokenRole) => Segment[]
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
doc: (node: Node) => Markdown | undefined
/** 一条要执行的命令:数组写法交给命令规则读,shell 写法是宿主读好的嵌着的代码(SourceDocument.embedded),可以带一张选项表。 */
command: (form: CommandForm) => { text: Inline[], table?: PreviewTable }
}
/**
* 嵌着的一段代码读成一行:整段只有一步时是这一步的译文(连它的表),几步时各步的译文连起来,
* 一步失败就停的(shell 的 &&)用「然后」连,别的用分号。
*/
export function embeddedText(reading: EmbeddedReading): { text: Inline[], table?: PreviewTable } {
const [first] = reading.steps
if (reading.whole && first)
return first
const separator: Inline[] = reading.chained ? [segment(' ', 'plain', undefined), segment('然后', 'keyword', undefined), segment(' ', 'plain', undefined)] : [segment(';', 'plain', undefined)]
return { text: reading.steps.flatMap((step, i) => i > 0 ? [...separator, ...step.text] : step.text) }
}
function hint(text: string): Segment {
@@ -914,6 +966,115 @@ function optionsRows(facts: OptionsFacts, names: Record<string, string>): Proper
}))
}
/** 解析指令:一条一行,行名是指令的叫法,值照写,后面淡色写补充说明(语法版本是给 BuildKit 看的)。 */
function directiveRows(facts: DirectiveFacts, names: Record<string, { label: string, note?: string }>): PropertyRow[] {
return facts.directives.map(({ name, value }) => {
const known = names[name]
return { label: known?.label ?? name, value: [segment(value, 'string', undefined), ...(known?.note ? [hint(known.note)] : [])] }
})
}
/** 选项的值:只是一个变量(`$BUILDPLATFORM`、`${X}`)时按变量着色、能跳到定义,别的照写成字符串。 */
function flagValue(flag: InstructionFlag): Inline[] {
const match = /^\$(\{)?(\w+)\}?$/.exec(flag.value)
if (!match)
return [segment(flag.value, 'string', flag.valueStart)]
const name = match[2]
return [segment(name, /^[A-Z][A-Z0-9_]*$/.test(name) ? 'constant' : 'variable', flag.valueStart + (match[1] ? 2 : 1))]
}
/**
* 阶段:名称(没起名的写按序号引用)/ 说明(紧贴在 FROM 上面的注释)/ 基础镜像(镜像名,标签、摘要写了才写;
* 接着前面某个阶段建的写「阶段 X」)/ 平台(写了 --platform 才有)。
*/
function stageRows(facts: StageFacts, say: Phrases): PropertyRow[] {
const doc = (facts.doc ?? []).map(node => say.doc(node)?.markdown).filter(Boolean).join('\n')
const platform = facts.flags.find(flag => flag.name === 'platform')
const image: Inline[] = facts.stage
? [segment('阶段', 'keyword', undefined), segment(' ', 'plain', undefined), ...say.name(facts.image, 'variable')]
: [
...say.translated(facts.image),
...(facts.tag ? [hint(',标签 '), ...say.translated(facts.tag)] : []),
...(facts.digest ? [hint(',摘要 '), ...say.translated(facts.digest)] : []),
]
return [
facts.name
? { label: '名称', value: say.name(facts.name, 'variable'), ...defining(facts.name) }
: { label: '名称', value: [hint(`没起名,按序号 ${facts.index} 引用`)] },
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
{ label: '基础镜像', value: image },
...(platform ? [{ label: '平台', value: flagValue(platform) }] : []),
]
}
const DURATION_UNITS: Record<string, string> = { h: '小时', m: '分', s: '秒', ms: '毫秒', us: '微秒', ns: '纳秒' }
/** 时长(`30s`、`1m30s`、`500ms`):数字按数字着色,单位写成中文;认不出的照写。 */
function durationText(flag: InstructionFlag): Inline[] {
const parts = [...flag.value.matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|[hms])/g)]
if (parts.length === 0 || parts.map(part => part[0]).join('') !== flag.value)
return [segment(flag.value, 'string', flag.valueStart)]
return parts.flatMap(([, amount, unit], i) => [...(i > 0 ? [hint(' ')] : []), segment(amount, 'number', undefined), hint(` ${DURATION_UNITS[unit]}`)])
}
/** 一行命令连着它的表(选项写得多的命令的「选项 | 意思」)。 */
function commandRow(label: string, command: { text: Inline[], table?: PreviewTable }): PropertyRow {
return { label, value: command.text, ...(command.table && { columns: command.table.columns, items: command.table.items }) }
}
/** 健康检查:间隔 / 超时 / 启动后宽限 / 宽限期里的间隔 / 失败几次算不健康(写了才有)/ 检查命令;NONE 只有一行「不做健康检查」。 */
function healthRows(facts: HealthFacts, say: Phrases): PropertyRow[] {
if (facts.none)
return [{ label: '检查', value: [hint('不做健康检查(基础镜像里设的也关掉)')] }]
const flag = (name: string): InstructionFlag | undefined => facts.flags.find(item => item.name === name)
const durations: [string, string][] = [['interval', '间隔'], ['timeout', '超时'], ['start-period', '启动后宽限'], ['start-interval', '宽限期里的间隔']]
const retries = flag('retries')
return [
...durations.flatMap(([name, label]) => {
const found = flag(name)
return found ? [{ label, value: durationText(found) }] : []
}),
...(retries ? [{ label: '失败几次算不健康', value: [segment(retries.value, 'number', retries.valueStart), hint(' 次')] }] : []),
...(facts.command ? [commandRow('检查命令', say.command(facts.command))] : []),
]
}
/** 启动:命令(ENTRYPOINT 叫入口命令)、写法(数组不经过 shell / 经过 shell)。 */
function startRows(facts: StartFacts, say: Phrases): PropertyRow[] {
return [
commandRow(facts.instruction === 'cmd' ? '命令' : '入口命令', say.command(facts.command)),
{ label: '写法', value: [hint(facts.command.form === 'exec' ? '数组,不经过 shell' : '经过 shell')] },
]
}
/** 节点所在的那条顶层语句(父节点是根)。 */
function statementOf(node: Node): Node {
let current = node
while (current.parent?.parent)
current = current.parent
return current
}
/**
* 分在哪儿有效的变量(Dockerfile 的构建参数、环境变量):一个变量是「构建参数 / 环境变量」和「默认值 / 值」两行(构建参数没写默认值的
* 写「没写,构建时传」);几个变量(一条写了几个,或几条连着写)并成一行,下面一张「名称 | 值」表,每行讲的定义是那个变量名。
*/
function scopedVariableRows(declarators: DeclaratorFacts[], scope: 'build' | 'environment', say: Phrases): PropertyRow[] {
const label = scope === 'build' ? '构建参数' : '环境变量'
const valueLabel = scope === 'build' ? '默认值' : '值'
const valueOf = (declarator: DeclaratorFacts): Inline[] => declarator.value ? say.translated(declarator.value) : [hint(scope === 'build' ? '没写,构建时传' : '空')]
const [only] = declarators
if (declarators.length === 1 && only)
return [{ label, value: say.translated(only.name), ...defining(only.name) }, { label: valueLabel, value: valueOf(only) }]
const statements = new Set(declarators.map(declarator => statementOf(declarator.name).id)).size
return [{
label,
value: [hint(statements > 1 ? `${statements} 条连着写` : `${declarators.length} 个`)],
columns: ['名称', valueLabel],
items: declarators.map(declarator => ({ cells: [say.translated(declarator.name), valueOf(declarator)], ...defining(declarator.name) })),
}]
}
/** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow {
return {