✨ 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

+191 -75
View File
@@ -1,4 +1,5 @@
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TextBlock, TokenRole, Translator } from './contract'
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
import type { TreeSitterRuntime } from './runtime'
import { blockAttributes } from './attributes'
import { commonIndent, splitLines } from './editText'
@@ -63,19 +64,27 @@ function leavesIn(node: Node, from: number, to: number, out: Node[]): void {
}
}
/** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段。 */
function colorLines(root: Node, source: string, lines: Line[], startRow: number, endRow: number, indent: number, tokenRole: (leaf: Node) => TokenRole): Segment[] {
/** 着色的一层:一棵语法树,和给它的叶子定词性的语言插件。嵌着的代码的树叠在外层的树上面,盖住的地方按它着色。 */
interface ColorLayer {
root: Node
tokenRole: (leaf: Node) => TokenRole
}
/** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段;后面的层盖过前面的。 */
function colorLines(layers: readonly ColorLayer[], source: string, lines: Line[], startRow: number, endRow: number, indent: number): Segment[] {
const from = lines[startRow].start
const to = lines[endRow].end
const owner = new Int32Array(to - from).fill(-1)
const roles: TokenRole[] = []
const leaves: Node[] = []
leavesIn(root.descendantForIndex(from, to) ?? root, from, to, leaves)
leaves.forEach((leaf, i) => {
roles.push(tokenRole(leaf))
for (let at = Math.max(leaf.startIndex, from); at < Math.min(leaf.endIndex, to); at++)
owner[at - from] = i
})
for (const { root, tokenRole } of layers) {
const leaves: Node[] = []
leavesIn(root.descendantForIndex(from, to) ?? root, from, to, leaves)
for (const leaf of leaves) {
roles.push(tokenRole(leaf))
for (let at = Math.max(leaf.startIndex, from); at < Math.min(leaf.endIndex, to); at++)
owner[at - from] = roles.length - 1
}
}
const segments: Segment[] = []
let current = -2
@@ -112,11 +121,43 @@ function ownName(block: CodeBlock): Node | undefined {
case 'const':
case 'variable':
return block.facts.declarators.length === 1 ? block.facts.declarators[0].name : undefined
case 'stage':
return block.facts.name
default:
return undefined
}
}
/** 解析好的一段嵌着的代码(见 EmbeddedCode):它那种语言的语法树、源码 / 语言插件、翻译插件,分好的几步。 */
export interface EmbeddedPart {
code: EmbeddedCode
root: Node
document: SourceDocument
translator: Translator | undefined
steps: StepBlocks
}
/** 一份源码里嵌着的各段代码,按 EmbeddedCode.node 的起始下标找。 */
export type EmbeddedParts = ReadonlyMap<number, EmbeddedPart>
/** 嵌着的代码读成的样子(SourceDocument.embedded):各步交给那种语言的翻译插件翻成一行,注释不算一步。 */
function embeddedReading({ steps, document, translator }: EmbeddedPart): EmbeddedReading {
const read = (block: CodeBlock): { text: Inline[], table?: PreviewTable } => {
if (!translator)
return { text: [{ text: block.node.text, role: 'plain' }] }
if ('facts' in block)
return { text: translator.expression(block.node, document) }
const { text, table } = translator.translate(block, document)
return { text: 'markdown' in text ? [{ text: text.markdown, role: 'comment' }] : text, ...(table && { table }) }
}
const [only] = steps.blocks
return {
steps: steps.blocks.filter(block => block.kind !== 'comment').map(read),
chained: steps.chained,
whole: steps.blocks.length === 1 && !!only,
}
}
/**
* 用哪些库规则(某 languageId 注册的,按注册顺序;这份源码导入了规则关心的库才启用),规则出错时怎么记,
* 规则查名字的定义时交出眼下知道的(RuleContext.lookup,见 ruleLookups.ts);没给时总是没查到。
@@ -134,81 +175,113 @@ const NO_RULES: RuleRun = { rules: [], report: () => {} }
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
* 并进去的子块不再单独成块。
*/
export function toBlockViews(blocks: CodeBlock[], document: SourceDocument, translator: Translator | undefined, rules: RuleRun = NO_RULES): BlockView[] {
const { source, language: adapter } = document
export function toBlockViews(
blocks: CodeBlock[],
document: SourceDocument,
translator: Translator | undefined,
rules: RuleRun = NO_RULES,
embedded: EmbeddedParts = new Map(),
): BlockView[] {
const { source } = document
const lines = lineRanges(source)
const texts = splitLines(source)
const imports = importsIn(blocks)
const active = activeRules(rules.rules, imports)
const context: RuleContext = {
document,
imports,
...(translator && { displayLanguage: translator.displayLanguage }),
translate: node => translator ? translator.expression(node, document) : [{ text: node.text, role: 'plain' }],
doc: node => translator?.doc(node, document),
lookup: rules.lookup ?? (() => undefined),
}
/** startRow..endRow 这几行整行源码去共同缩进,和它的着色。 */
const sourceRows = (root: Node, startRow: number, endRow: number): Pick<BlockView, 'startLine' | 'endLine' | 'code' | 'codeTokens'> => {
const outer = blocks[0]?.node.tree.rootNode
const colors: ColorLayer[] = [
...(outer ? [{ root: outer, tokenRole: document.language.tokenRole }] : []),
...[...embedded.values()].map(part => ({ root: part.root, tokenRole: part.document.language.tokenRole })),
]
/** startRow..endRow 这几行整行源码去共同缩进,和它的着色(嵌着的代码按它那种语言着色)。 */
const sourceRows = (startRow: number, endRow: number): Pick<BlockView, 'startLine' | 'endLine' | 'code' | 'codeTokens'> => {
const rows = texts.slice(startRow, endRow + 1)
const indent = commonIndent(rows).length
return {
startLine: startRow,
endLine: endRow,
code: rows.map(row => row.slice(Math.min(indent, row.length))).join('\n'),
codeTokens: colorLines(root, source, lines, startRow, endRow, indent, adapter.tokenRole),
codeTokens: colorLines(colors, source, lines, startRow, endRow, indent),
}
}
/** 一行译文,连着摊开实参的表、块的说明(有才有)。 */
const textView = (block: TextBlock): { translation: PreviewText, table?: PreviewTable, doc?: Markdown } => {
if (!translator)
return { translation: [] }
const { text, table } = translator.translate(block, document)
const doc = block.doc && translator.doc(block.doc, document)
return { translation: text, ...(table && { table }), ...(doc && { doc }) }
}
/** 按源码先后逐块给位置记号:先序遍历,块的记号在它的子块之前要 */
const anchors = new BlockAnchors()
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):头部是编辑范围,子块照常交给各规则看。scope 见 BlockAnchors.place */
function plain(block: CodeBlock, id: string, scope: string): BlockView {
const name = ownName(block)
const rows = sourceRows(block.node.tree.rootNode, block.header.startRow, block.header.endRow)
const { anchor, inner } = anchors.place(scope, block.kind, name?.text, rows.code)
return {
id,
anchor,
kind: block.kind,
...rows,
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
attributes: blockAttributes(block),
children: block.children.map((child, i) => build(child, `${id}.${i}`, inner)),
...('facts' in block ? { properties: translator ? translator.describe(block, document) : [] } : textView(block)),
}
}
function build(block: CodeBlock, id: string, scope: string): BlockView {
const claim = active.length > 0 ? claimBlock(block, active, context, rules.report) : undefined
if (!claim)
return plain(block, id, scope)
const { span, kept } = claimedShape(block, claim.block.absorbs)
if ('as' in claim.block) {
const { attributes } = block
return plain({ ...claim.block.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, id, scope)
}
const { kind, badge, rows, defines, attributes } = claim.block
const lines = sourceRows(block.node.tree.rootNode, span.startRow, span.endRow)
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), lines.code)
return {
id,
anchor,
kind,
...lines,
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
badge,
children: kept.map((child, i) => build(child, `${id}.${i}`, inner)),
properties: rows,
/** 一种语言的块怎么转:用这种语言的源码、翻译插件和库规则(嵌着的代码没有库规则)。 */
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun): (block: CodeBlock, id: string, scope: string) => BlockView {
const imports = importsIn(layerBlocks)
const active = activeRules(layerRules.rules, imports)
const context: RuleContext = {
document: layerDocument,
imports,
...(layerTranslator && { displayLanguage: layerTranslator.displayLanguage }),
translate: node => layerTranslator ? layerTranslator.expression(node, layerDocument) : [{ text: node.text, role: 'plain' }],
doc: node => layerTranslator?.doc(node, layerDocument),
lookup: layerRules.lookup ?? (() => undefined),
}
/** 一行译文,连着摊开实参的表、块的说明(有才有)。 */
const textView = (block: TextBlock): { translation: PreviewText, table?: PreviewTable, doc?: Markdown } => {
if (!layerTranslator)
return { translation: [] }
const { text, table } = layerTranslator.translate(block, layerDocument)
const doc = block.doc && layerTranslator.doc(block.doc, layerDocument)
return { translation: text, ...(table && { table }), ...(doc && { doc }) }
}
/** 块执行的嵌着的代码分成的几步:只有一步时是它的子块,几步时是各步,交给那种语言转(见 TextBlock.embedded)。 */
const embeddedChildren = (block: CodeBlock, id: string, scope: string, from: number): BlockView[] => {
const part = 'embedded' in block && block.embedded ? embedded.get(block.embedded.node.startIndex) : undefined
if (!part)
return []
const steps = part.steps.blocks
const [only] = steps
const kids = steps.length === 1 && only ? only.children : steps
const build = layer(steps, part.document, part.translator, NO_RULES)
return kids.map((child, i) => build(child, `${id}.${from + i}`, scope))
}
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):头部是编辑范围,子块照常交给各规则看。scope 见 BlockAnchors.place */
function plain(block: CodeBlock, id: string, scope: string): BlockView {
const name = ownName(block)
const rows = sourceRows(block.header.startRow, block.header.endRow)
const { anchor, inner } = anchors.place(scope, block.kind, name?.text, rows.code)
return {
id,
anchor,
kind: block.kind,
...rows,
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
attributes: blockAttributes(block),
children: [
...block.children.map((child, i) => build(child, `${id}.${i}`, inner)),
...embeddedChildren(block, id, inner, block.children.length),
],
...('facts' in block ? { properties: layerTranslator ? layerTranslator.describe(block, layerDocument) : [] } : textView(block)),
}
}
function build(block: CodeBlock, id: string, scope: string): BlockView {
const claim = active.length > 0 ? claimBlock(block, active, context, layerRules.report) : undefined
if (!claim)
return plain(block, id, scope)
const { span, kept } = claimedShape(block, claim.block.absorbs)
if ('as' in claim.block) {
const { attributes } = block
return plain({ ...claim.block.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, id, scope)
}
const { kind, badge, rows, defines, attributes } = claim.block
const lines = sourceRows(span.startRow, span.endRow)
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), lines.code)
return {
id,
anchor,
kind,
...lines,
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
badge,
children: kept.map((child, i) => build(child, `${id}.${i}`, inner)),
properties: rows,
}
}
return build
}
const build = layer(blocks, document, translator, rules)
return blocks.map((block, i) => build(block, String(i), ''))
}
@@ -230,6 +303,15 @@ export interface DocumentExtras {
rules?: RuleRun
calleeKind?: SourceDocument['calleeKind']
commandRule?: SourceDocument['commandRule']
/** 嵌着的代码的那种语言用哪个语言插件、翻译插件、命令规则(见 EmbeddedCode);没有的不读,照抄。 */
embedded?: (languageId: string) => EmbeddedLanguage | undefined
}
/** 读嵌着的代码用的:那种语言的语言插件、选中的翻译插件、命令名的命令规则。 */
export interface EmbeddedLanguage {
adapter: LanguageAdapter
translator: Translator | undefined
commandRule?: SourceDocument['commandRule']
}
/** 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。 */
@@ -242,15 +324,49 @@ export async function readBlockViews(
extras: DocumentExtras = {},
): Promise<DocumentViews> {
const tree = await runtime.parse(adapter, languageId, source)
const trees: Tree[] = [tree]
try {
const parts = new Map<number, EmbeddedPart>()
for (const code of adapter.embedded?.(tree.rootNode, source, languageId) ?? []) {
const language = extras.embedded?.(code.languageId)
if (!language)
continue
const inner = await runtime.parse(language.adapter, code.languageId, source, code.ranges)
trees.push(inner)
const root = inner.rootNode
const steps = language.adapter.steps?.(root, source, code.languageId) ?? { blocks: language.adapter.toBlocks(root, source, code.languageId), chained: false }
const document: SourceDocument = { source, language: language.adapter, markers: [], ...(language.commandRule && { commandRule: language.commandRule }) }
parts.set(code.node.startIndex, { code, root, document, translator: language.translator, steps })
}
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
const { calleeKind, commandRule } = extras
const document: SourceDocument = { source, language: adapter, markers, ...(calleeKind && { calleeKind }), ...(commandRule && { commandRule }) }
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), document, translator, extras.rules)
return { blocks, displayNames: names, symbols: adapter.symbols?.(tree.rootNode, source, languageId) ?? [] }
const readings = new Map<number, EmbeddedReading>()
const embedded = (node: Node): EmbeddedReading | undefined => {
const part = parts.get(node.startIndex)
if (!part)
return undefined
const reading = readings.get(node.startIndex) ?? embeddedReading(part)
readings.set(node.startIndex, reading)
return reading
}
const document: SourceDocument = {
source,
language: adapter,
markers,
...(calleeKind && { calleeKind }),
...(commandRule && { commandRule }),
...(parts.size > 0 && { embedded }),
}
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), document, translator, extras.rules, parts)
const symbols = [
...adapter.symbols?.(tree.rootNode, source, languageId) ?? [],
...[...parts.values()].flatMap(part => part.document.language.symbols?.(part.root, source, part.code.languageId) ?? []),
]
return { blocks, displayNames: names, symbols }
}
finally {
tree.delete()
for (const parsed of trees)
parsed.delete()
}
}
+143 -7
View File
@@ -4,13 +4,17 @@ import type { Disposable } from 'vscode'
export type { Node }
/**
* 块的种类。后面几种给脚本语言(shell):interpreter 第一行 `#!` 写的解释器,options 打开 / 关掉解释器的开关(`set -euo pipefail`),
* case 按一个值分情况(整条语句,和它的每个情况),command 执行一条命令,pipeline 几条命令用管道串起来。
* 块的种类。interpreter 到 pipeline 给脚本语言(shell):interpreter 第一行 `#!` 写的解释器,options 打开 / 关掉解释器的开关
* (`set -euo pipefail`),case 按一个值分情况(整条语句,和它的每个情况),command 执行一条命令,pipeline 几条命令用管道串起来。
* directive 往后给构建文件(Dockerfile):directive 文件开头给构建工具看的解析指令(`# syntax=…`),stage 一个构建阶段(FROM 和它后面的指令),
* run 构建时执行的命令,copy 往镜像里复制文件,setting 一条设置(工作目录、运行身份、端口……),label 镜像的标签,
* health 健康检查,start 容器启动时执行的命令。
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
/** 源码里一段的下标:源码字符串下标(web-tree-sitter 的 startIndex / endIndex),end 为开区间。 */
export interface SourceRange { startIndex: number, endIndex: number }
@@ -92,6 +96,96 @@ export interface OptionsFacts {
switches: SwitchFacts[]
}
/** 文件开头给构建工具看的一条解析指令(Dockerfile 的 `# syntax=docker/dockerfile:1.7`):名字和值照写。 */
export interface ParserDirective {
name: string
value: string
}
export interface DirectiveFacts {
/** 照源码顺序,一行一条。 */
directives: ParserDirective[]
}
/**
* 写在指令后面的一个选项(Dockerfile 的 `--from=deps`、`--chown=u:g`、`--interval=30s`):名字、值,值在源码里从哪开始
* (值里的阶段名、变量能跳到定义)。node 是整个选项。
*/
export interface InstructionFlag {
node: Node
name: string
value: string
valueStart: number
}
/** 一个构建阶段(Dockerfile 的 FROM),它后面到下一个 FROM 之前的指令是它的子块。 */
export interface StageFacts {
/** 阶段名(`AS deps` 的 deps);没起名的没有。 */
name?: Node
/** 第几个阶段,从 0 数:没起名的阶段按它引用(`COPY --from=0`)。 */
index: number
/** 紧贴在 FROM 上面的几行注释(中间没空行):阶段的说明,算在块的头部里。 */
doc?: Node[]
/** 基础镜像的名字(`node:22-alpine` 的 node);stage 为真时是前面某个阶段的名字。 */
image: Node
/** 前面的阶段名(`FROM builder`):从那个阶段接着往下建。 */
stage: boolean
/** 标签(`:22-alpine`,带冒号)、摘要(`@sha256:…`,带 @),写了才有。 */
tag?: Node
digest?: Node
/** FROM 后面的选项(`--platform=linux/amd64`)。 */
flags: InstructionFlag[]
}
/** 一条要执行的命令的两种写法:数组(`["node", "server.js"]`,不经过 shell,node 是整个数组),或交给 shell 执行的一段代码。 */
export type CommandForm
= | { form: 'exec', node: Node }
| { form: 'shell', code: EmbeddedCode }
/** 健康检查(Dockerfile 的 HEALTHCHECK):none 是关掉健康检查(`HEALTHCHECK NONE`);否则有选项(间隔、超时……)和检查命令。 */
export interface HealthFacts {
none: boolean
flags: InstructionFlag[]
command?: CommandForm
}
/** 容器启动时执行的命令:CMD(默认命令,`docker run` 后面写了命令就换掉它)或 ENTRYPOINT(入口,CMD 成了它的参数)。 */
export interface StartFacts {
instruction: 'cmd' | 'entrypoint'
command: CommandForm
}
/**
* 写在这门语言里的另一种语言的代码(Dockerfile 的 RUN、shell 写法的 CMD、HEALTHCHECK 的检查命令是 shell)。
* ranges 是源码里属于它的几段,去掉了外层语言的记号(续行的 `\`),拼起来是一份那种语言的代码;宿主按 languageId 找语言插件,
* 只解析这几段(下标照旧是这份源码的),块交给那种语言的翻译插件。node 是外层语法树里装着它的节点,翻译插件拿它问 SourceDocument.embedded。
*/
export interface EmbeddedCode {
node: Node
languageId: string
ranges: SourceRange[]
}
/**
* 一段嵌着的代码被当成一串步骤执行时(Dockerfile 的 RUN)怎么分块:一步一块;chained 表示前一步失败就不往下走(shell 用 && 连着的一串)。
* 见 LanguageAdapter.steps。
*/
export interface StepBlocks {
blocks: CodeBlock[]
chained: boolean
}
/**
* 宿主读嵌着的代码读出来的(SourceDocument.embedded):各步读成的一行(那种语言的翻译插件翻的,可以带一张表;注释不算一步),
* 是不是一步失败就停。whole 表示整段就是一步(没有别的块):这一步就是整段,它的子块归嵌着它的块;
* 否则各块是嵌着它的块的子块(见 TextBlock.embedded)。连词、「N 条命令」这类叫法归本语言的翻译插件。
*/
export interface EmbeddedReading {
steps: { text: Inline[], table?: PreviewTable }[]
chained: boolean
whole: boolean
}
/**
* 父类的一项:TypeScript `extends Base<T>` 里的 Base(一个表达式)和跟在它后面的泛型实参 `<T>`(语法里是两个节点);
* Python 类参数列表里的一项,`metaclass=ABCMeta` 这类关键字参数、`*bases` 也算。
@@ -278,10 +372,14 @@ export interface CommandFacts {
redirects: Redirect[]
}
/** 命令规则读出来的:一行译文;value 表示读出来的是一个东西(`dirname "$0"` 是脚本所在目录),不是一个动作。 */
/**
* 命令规则读出来的:一行译文;value 表示读出来的是一个东西(`dirname "$0"` 是脚本所在目录),不是一个动作;
* table 是排在这一行下面的表(选项写得多的命令:「选项 | 意思」)。
*/
export interface CommandReading {
text: Inline[]
value?: boolean
table?: PreviewTable
}
/** 命令规则读命令时能用的。 */
@@ -302,7 +400,7 @@ export interface CommandRule {
id: string // 'shell.rm'
label: string // 'rm'
languageIds: string[]
/** 管哪些命令名;几条规则都管同一个名字时先注册的算。 */
/** 管哪些命令名;几条规则都管同一个名字时先注册的算。'*' 管没有自己的规则的命令(`pg_repack --version` → 打印 pg_repack 的版本)。 */
commands: string[]
read: (command: CommandFacts, context: CommandContext) => CommandReading | undefined
}
@@ -333,8 +431,13 @@ export interface VariableFacts {
* 中间可以隔空行和赋值行尾的注释)。它算在块的头部里,不再单独成一块。
*/
doc?: Node
/** 一条语句声明几个变量(`let a = 1, b = 2`)就有几项。 */
/** 一条语句声明几个变量(`let a = 1, b = 2`)就有几项;几条连着写的声明并成一块时(Dockerfile 连着的 ENV)是它们的全部。 */
declarators: DeclaratorFacts[]
/**
* 变量在哪儿有效,语言分这个才填:build 是构建参数(Dockerfile 的 ARG,只在构建时有,值是默认值、构建时可以另传),
* environment 是环境变量(Dockerfile 的 ENV,构建时和容器运行时都有)。
*/
scope?: 'build' | 'environment'
}
/**
@@ -381,7 +484,15 @@ export interface VariableBlock extends BlockShape { kind: 'const' | 'variable',
export interface InterpreterBlock extends BlockShape { kind: 'interpreter', facts: InterpreterFacts }
/** 开关块是一整条 `set …`,每个开关一行。 */
export interface OptionsBlock extends BlockShape { kind: 'options', facts: OptionsFacts }
export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
/** 解析指令块是文件开头连着的几行 `# 名字=值`,一条一行。 */
export interface DirectiveBlock extends BlockShape { kind: 'directive', facts: DirectiveFacts }
/** 阶段块的头部是 FROM 那一行(连同上面的说明),它后面的指令是子块。 */
export interface StageBlock extends BlockShape { kind: 'stage', facts: StageFacts }
export interface HealthBlock extends BlockShape { kind: 'health', facts: HealthFacts }
export interface StartBlock extends BlockShape { kind: 'start', facts: StartFacts }
export type FactBlock
= | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
| DirectiveBlock | StageBlock | HealthBlock | StartBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
@@ -399,6 +510,17 @@ export interface TextBlock extends BlockShape {
* 算在块的头部里;预览在一行译文下面按 Markdown 显示(Translator.doc)。
*/
doc?: Node
/**
* 几条连着写的同种语句并成的一块(Dockerfile 连着的 COPY、LABEL):照源码顺序的各条,node 是第一条,头部盖住全部。
* 翻译插件排成一行字加一张表。
*/
group?: Node[]
/**
* 这块执行的是嵌着的另一种语言的代码(Dockerfile 的 RUN):宿主交给那种语言的语言插件分成几步(LanguageAdapter.steps),
* 只有一步时这一步的子块是这块的子块,几步时各步是这块的子块,都交给那种语言的翻译插件;这块自己的译文照旧归本语言的翻译插件
* (它从 SourceDocument.embedded 拿到读出来的样子)。
*/
embedded?: EmbeddedCode
}
export type CodeBlock = TextBlock | FactBlock
@@ -512,6 +634,10 @@ export interface LanguageAdapter {
command?: (node: Node) => CommandFacts | undefined
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[]
/** 这份源码里嵌着的别的语言的代码(见 EmbeddedCode),块里的(TextBlock.embedded)、属性表里的(HealthFacts.command)都算;没有的语言不填。 */
embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[]
/** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块(见 StepBlocks);不填就按 toBlocks 分、不算一步失败就停。 */
steps?: (root: Node, source: string, languageId: string) => StepBlocks
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
displayNames?: DisplayNameReader
}
@@ -556,6 +682,11 @@ export interface SourceDocument {
calleeKind?: (call: CallFacts) => BlockKind | undefined
/** 这个命令名的命令规则(这种语言注册的,先注册的算);没有、或宿主不给时没有,命令照抄。规则出错由宿主记下、当它没读。 */
commandRule?: (name: string) => CommandRule | undefined
/**
* 嵌着的代码(EmbeddedCode.node 是 node 的那段)读出来的样子:宿主用那种语言的语言插件、翻译插件读好了交出来。
* 那种语言没有注册、宿主不给时没有,翻译插件照抄。
*/
embedded?: (node: Node) => EmbeddedReading | undefined
}
/** 筛选属性和值的显示名,筛选栏、块的种类(图标的悬停提示)用它;没给名字的显示 id。 */
@@ -589,7 +720,8 @@ export interface Translator {
export interface ThemeColor { dark: string, light: string }
/**
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 22 个给内置种类(命令、管道共用一个),
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 26 个给内置种类(几种共用一个:
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置共用 sliders-horizontal,字段、标签共用 tag),
* 后面的给库规则的种类;库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),
* 没有按任意名字找图标的路。
*/
@@ -616,6 +748,10 @@ export const KIND_ICON_NAMES = [
'sliders-horizontal',
'signpost',
'square-terminal',
'layers',
'copy',
'activity',
'play',
'database',
'package',
'server-cog',
@@ -56,6 +56,14 @@ export async function documentMessage(
rules: { rules, report, lookup },
calleeKind,
commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report),
embedded: (embeddedId) => {
const embeddedAdapter = registry.languageFor(embeddedId)
return embeddedAdapter && {
adapter: embeddedAdapter,
translator: registry.selectedTranslator(embeddedId),
commandRule: commandRuleLookup(registry.commandRulesFor(embeddedId), report),
}
},
})
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames, symbols }
+11 -1
View File
@@ -206,7 +206,17 @@ function commentNodes(block: CodeBlock): Node[] {
function ownDoc(block: CodeBlock): Node | undefined {
if (!('facts' in block))
return block.doc
return block.kind === 'import' || block.kind === 'interpreter' || block.kind === 'options' ? undefined : block.facts.doc
switch (block.kind) {
case 'function':
case 'class':
case 'enum':
case 'exception':
case 'const':
case 'variable':
return block.facts.doc
default:
return undefined
}
}
/** 类属性的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
+21 -5
View File
@@ -1,5 +1,5 @@
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { LanguageAdapter } from './contract'
import type { Point, Range, Tree } from '@vscode/tree-sitter-wasm'
import type { LanguageAdapter, SourceRange } from './contract'
import * as path from 'node:path'
import { Language, Parser } from '@vscode/tree-sitter-wasm'
@@ -8,6 +8,19 @@ export function bundledWasmDir(): string {
return path.join(__dirname, 'tree-sitter')
}
/** 源码下标的行列(行从 0 起,列按 UTF-16,同 web-tree-sitter 解析 JS 字符串时的算法)。 */
function pointAt(source: string, index: number): Point {
const lineStart = source.lastIndexOf('\n', index - 1) + 1
let row = 0
for (let at = source.indexOf('\n'); at !== -1 && at < index; at = source.indexOf('\n', at + 1))
row++
return { row, column: index - lineStart }
}
function includedRange(source: string, { startIndex, endIndex }: SourceRange): Range {
return { startIndex, endIndex, startPosition: pointAt(source, startIndex), endPosition: pointAt(source, endIndex) }
}
// web-tree-sitter 的运行时是进程级单例:Parser.init 只能有效调用一次,后来的 wasm 目录不再生效
let runtimeReady: Promise<void> | undefined
@@ -27,10 +40,13 @@ export class TreeSitterRuntime {
/** @param wasmDir 放 tree-sitter.wasm 的目录 */
constructor(private readonly wasmDir: string) {}
/** 返回的语法树占着 wasm 内存,用完必须 `tree.delete()`。 */
async parse(adapter: LanguageAdapter, languageId: string, source: string): Promise<Tree> {
/**
* 返回的语法树占着 wasm 内存,用完必须 `tree.delete()`。给了 ranges 就只解析源码里的这几段(嵌着的代码,见 EmbeddedCode),
* 节点的下标、行列照旧是整份源码的。
*/
async parse(adapter: LanguageAdapter, languageId: string, source: string, ranges?: readonly SourceRange[]): Promise<Tree> {
const parser = await this.parserFor(adapter.grammarPath(languageId))
const tree = parser.parse(source)
const tree = parser.parse(source, null, ranges ? { includedRanges: ranges.map(range => includedRange(source, range)) } : undefined)
if (!tree)
throw new Error(`tree-sitter 没能解析 ${languageId} 源码`)
return tree
+24
View File
@@ -1,4 +1,5 @@
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { BlockView } from './blockView'
import type { BlockKind, CodeBlock, Inline, LanguageAdapter, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment } from './contract'
import type { FileEvent, IndexHost, WorkspaceFile } from './workspaceIndex'
import { readFileSync } from 'node:fs'
@@ -9,6 +10,9 @@ import { TreeSitterRuntime } from './runtime'
/** 单元测试直接用依赖包里的 wasm,不走打包产物。 */
export const WASM_DIR = path.resolve(__dirname, '../../node_modules/@vscode/tree-sitter-wasm/wasm')
/** Dockerfile 的语法包在 tree-sitter-wasm 里(打包时 esbuild 把它和上面的一起复制进 dist/tree-sitter)。 */
export const DOCKERFILE_WASM_DIR = path.resolve(__dirname, '../../node_modules/tree-sitter-wasm/out/dockerfile')
export const runtime = new TreeSitterRuntime(WASM_DIR)
export function readFixture(name: string): string {
@@ -81,6 +85,26 @@ export function tableLines(table: PreviewTable | undefined): string[] {
return itemLines(table?.items, '')
}
/**
* 视图数据写成文字(整份源码读出来的样子):一行译文的块是「种类 行号: 译文」,属性表块是「种类 行号」下面每行「属性: 值」,
* 子块比上一层多缩进两格;Markdown 只取第一行。表(属性行展开的、译文下面的、格子里套的)每行比上一层多缩进,
* 格子用 | 隔开,格子里的换行写成 ⏎。
*/
export function viewLines(blocks: readonly BlockView[], indent = ''): string[] {
const value = (text: PreviewText): string => 'markdown' in text ? text.markdown.split('\n')[0] : segmentText(text)
const items = (list: PropertyItem[] | undefined, pad: string): string[] => (list ?? []).flatMap(item => [
pad + item.cells.map(segmentText).join(' | ').replace(/\n/g, ' ⏎ '),
...items(item.items, `${pad} `),
...items(item.table?.items, `${pad} `),
])
return blocks.flatMap((block) => {
const head = 'properties' in block
? [`${indent}${block.kind} ${block.startLine + 1}`, ...block.properties.flatMap(row => [`${indent} ${row.label}: ${value(row.value)}`, ...items(row.items, `${indent} `)])]
: [`${indent}${block.kind} ${block.startLine + 1}: ${value(block.translation)}`, ...items(block.table?.items, `${indent} `)]
return [...head, ...viewLines(block.children, `${indent} `)]
})
}
/** 语法树节点换成它的源码,没填的可选字段去掉。 */
export function nodesToText(value: unknown): unknown {
if (value instanceof Node)
+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 {
@@ -26,6 +26,7 @@ body {
--cr-kind-comment: #6a9955;
--cr-kind-options: #e5c07b;
--cr-kind-command: #d7ba7d;
--cr-kind-stage: #56b6c2;
margin: 0;
font-family: var(--vscode-font-family, system-ui, sans-serif);
@@ -60,23 +61,25 @@ body.vscode-high-contrast-light {
--cr-kind-comment: #008000;
--cr-kind-options: #9a6a00;
--cr-kind-command: #7c5f12;
--cr-kind-stage: #127f8c;
}
/* 种类颜色:同一类语义共用一个颜色,图标区分具体是哪种(kindIcons.tsx)。卡片取自己的种类,子层取所属块的种类 */
[data-kind="import"] { --kind: var(--cr-kind-import); }
:is([data-kind="import"], [data-kind="copy"]) { --kind: var(--cr-kind-import); }
[data-kind="function"] { --kind: var(--cr-kind-function); }
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
[data-kind="enum"] { --kind: var(--cr-kind-enum); }
[data-kind="exception"] { --kind: var(--cr-kind-exception); }
:is([data-kind="field"], [data-kind="const"], [data-kind="variable"]) { --kind: var(--cr-kind-data); }
:is([data-kind="field"], [data-kind="const"], [data-kind="variable"], [data-kind="label"]) { --kind: var(--cr-kind-data); }
:is([data-kind="if"], [data-kind="else"], [data-kind="case"]) { --kind: var(--cr-kind-branch); }
[data-kind="stage"] { --kind: var(--cr-kind-stage); }
[data-kind="loop"] { --kind: var(--cr-kind-loop); }
:is([data-kind="try"], [data-kind="catch"], [data-kind="finally"]) { --kind: var(--cr-kind-error); }
[data-kind="return"] { --kind: var(--cr-kind-return); }
:is([data-kind="statement"], [data-kind="interpreter"]) { --kind: var(--cr-kind-statement); }
:is([data-kind="try"], [data-kind="catch"], [data-kind="finally"], [data-kind="health"]) { --kind: var(--cr-kind-error); }
:is([data-kind="return"], [data-kind="start"]) { --kind: var(--cr-kind-return); }
:is([data-kind="statement"], [data-kind="interpreter"], [data-kind="directive"]) { --kind: var(--cr-kind-statement); }
[data-kind="comment"] { --kind: var(--cr-kind-comment); }
[data-kind="options"] { --kind: var(--cr-kind-options); }
:is([data-kind="command"], [data-kind="pipeline"]) { --kind: var(--cr-kind-command); }
:is([data-kind="options"], [data-kind="setting"]) { --kind: var(--cr-kind-options); }
:is([data-kind="command"], [data-kind="pipeline"], [data-kind="run"]) { --kind: var(--cr-kind-command); }
/* 库规则认领的块:颜色由规则给,暗色、亮色两个写在元素的 style 上(--kind-dark / --kind-light),这里按主题挑 */
[data-badge] { --kind: var(--kind-dark); }
body:is(.vscode-light, .vscode-high-contrast-light) [data-badge] { --kind: var(--kind-light); }
@@ -36,6 +36,14 @@ describe('块的种类图标', () => {
case: 'signpost',
command: 'square-terminal',
pipeline: 'square-terminal',
directive: 'terminal',
stage: 'layers',
run: 'square-terminal',
copy: 'copy',
setting: 'sliders-horizontal',
label: 'tag',
health: 'activity',
start: 'play',
})
})
@@ -2,20 +2,24 @@ import type { LucideIcon } from 'lucide-react'
import type { ReactElement } from 'react'
import type { BlockKind, BlockView, KindIconName } from './types'
import {
Activity,
Box,
Braces,
CalendarClock,
Code,
Copy,
CornerDownLeft,
CornerDownRight,
Database,
Globe,
Import,
Layers,
ListOrdered,
Lock,
MessageSquareText,
Network,
Package,
Play,
Repeat,
ServerCog,
Shield,
@@ -56,6 +60,10 @@ export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
'sliders-horizontal': SlidersHorizontal,
'signpost': Signpost,
'square-terminal': SquareTerminal,
'layers': Layers,
'copy': Copy,
'activity': Activity,
'play': Play,
'database': Database,
'package': Package,
'server-cog': ServerCog,
@@ -92,6 +100,14 @@ export const KIND_ICONS: Record<BlockKind, KindIconName> = {
case: 'signpost',
command: 'square-terminal',
pipeline: 'square-terminal',
directive: 'terminal',
stage: 'layers',
run: 'square-terminal',
copy: 'copy',
setting: 'sliders-horizontal',
label: 'tag',
health: 'activity',
start: 'play',
}
/**
+3 -1
View File
@@ -6,6 +6,7 @@ export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
/** 着色用的词性,源码着色和译文着色共用。 */
export type TokenRole
@@ -88,7 +89,8 @@ export interface ThemeColor { dark: string, light: string }
export type KindIconName
= | 'import' | 'square-function' | 'box' | 'braces' | 'list-ordered' | 'triangle-alert' | 'tag' | 'lock' | 'variable'
| 'split' | 'corner-down-right' | 'repeat' | 'shield' | 'shield-alert' | 'shield-check' | 'corner-down-left' | 'code'
| 'message-square-text' | 'terminal' | 'sliders-horizontal' | 'signpost' | 'square-terminal' | 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network'
| 'message-square-text' | 'terminal' | 'sliders-horizontal' | 'signpost' | 'square-terminal' | 'layers' | 'copy' | 'activity' | 'play'
| 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network'
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和图标(从 KindIconName 里挑);块左边只画图标 */
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }