✨ 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()
}
}