✨ feat(vscode): 代码阅读能读嵌在一种语言里的另一种语言的代码:语言插件报出是哪几段,宿主交给那种语言的插件只解析这几段、分步、翻成子块并叠着上色,外层翻译插件只问读出来的样子;契约加构建文件的几种块(解析指令、阶段、执行、复制、设置、标签、健康检查、启动)、分作用范围的变量、并成一块的几条语句,引擎排这些块的属性表
This commit is contained in:
1 parent
834673391e
commit
3353cf870c
11 files changed
+603
-103
No files matched your search
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user