✨ 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

+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',