618 lines
31 KiB
TypeScript
618 lines
31 KiB
TypeScript
import type { Tree } from '@vscode/tree-sitter-wasm'
|
||
import type { BlockAttributes, BlockConcept, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleCard, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
|
||
import type { TreeSitterRuntime } from './runtime'
|
||
import type { ImportedName } from './symbolLinks'
|
||
import { blockAttributes } from './attributes'
|
||
import { KIND_ICONS } from './contract'
|
||
import { commonIndent, splitLines } from './editText'
|
||
import { explanationSpan, isCommentBlock } from './languages/shared'
|
||
import { activeRules, claimBlock, importsIn } from './libraryRules'
|
||
import { BlockAnchors } from './qa/anchor'
|
||
import { importedNames } from './symbolLinks'
|
||
|
||
interface BlockViewBase {
|
||
/** 在树里的位置,例如 '6.1.0'。 */
|
||
id: string
|
||
/** 块的位置记号(见 BlockAnchors):问答挂在它上面,文件别处改了照样认得回来。 */
|
||
anchor: string
|
||
/** BlockKind,或库规则卡片的种类 id(RuleCard.kind,带点,例如 sqlalchemy.table)。 */
|
||
kind: string
|
||
/**
|
||
* 编辑范围的起止行,0 起:块在源码里的完整范围,头部连同各子孙块、结尾(见 blockSpans);库规则认领的块是原块整条语句。
|
||
* 块编辑时它的子孙都在编辑框里,webview 不画它们;点「编辑」时和它重叠的别的块退出编辑,同一行不会同时在两处能改。
|
||
*/
|
||
startLine: number
|
||
endLine: number
|
||
/** 块自己那部分(头部)的最后一行:复合语句到主体开始之前,库规则认领的块是整条语句。问 AI 时贴外层块的开头用。 */
|
||
headerEndLine: number
|
||
/** 编辑范围的整行源码,去掉共同缩进。 */
|
||
code: string
|
||
/** 源码着色(拼起来等于 code)。 */
|
||
codeTokens: Segment[]
|
||
/** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */
|
||
defines?: SourceRange
|
||
/** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */
|
||
attributes: BlockAttributes
|
||
/**
|
||
* 这块认出来的概念,从左到右:语言的种类(再加语言插件另认的),库规则的卡片再加规则的种类和规则另认的。
|
||
* 同一个 id 只留第一个。项目入口那一块的第一个换成入口(见 entries/entryMarks.ts)。webview 按这个画图标,不自己用种类查。
|
||
*/
|
||
concepts: BlockConcept[]
|
||
/** 「重写注释」写到哪(见 CommentTarget);不能写注释的块(嵌着的代码里的、语言没给出新写的办法又还没有注释的)没有。 */
|
||
commentTarget?: CommentTarget
|
||
/** 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色(RuleCard.badge)。图标在 concepts 里。预览同类块一样默认收起它的子块。 */
|
||
badge?: KindBadge
|
||
/** 库规则挂在父卡片子行上的子块才有(见 RuleSlot):展开时画在父卡片属性表第 row 行下面,不排在父卡片的子块里。 */
|
||
slot?: BlockSlot
|
||
children: BlockView[]
|
||
}
|
||
|
||
/** 子块挂在父卡片的哪儿:属性表第 row 行的第 item 个子行,点那个子行的第一格才在这一行下面展开(见 RuleSlot)。 */
|
||
export interface BlockSlot {
|
||
row: number
|
||
item: number
|
||
}
|
||
|
||
/**
|
||
* 「重写注释」写到哪,行号 0 起。
|
||
* replace 换掉这几行:块的说明(docstring、JSDoc、`///`),没有说明时是块前面的普通注释,文件开头的说明卡片是它自己。
|
||
* insert 是都没有,新写一段插在 line 这一行上面。shell 是带定界符的空壳(正文占位 `_`),缩进同这一行。
|
||
* breakAt 是主体和声明写在同一行:从这一行的这一列拆开,说明插进主体里。
|
||
*/
|
||
export type CommentTarget
|
||
= | { kind: 'replace', startLine: number, endLine: number }
|
||
| { kind: 'insert', line: number, shell: string, breakAt?: number }
|
||
|
||
/**
|
||
* 发给 webview 的一块(可序列化):函数、类、枚举、异常、导入、常量、变量块和库规则的卡片预览时是属性表,文件开头的说明是 Markdown,其余是一行译文。
|
||
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(TextTranslation.table),块的说明(TextBlock.doc,翻译插件给的 Markdown 原文),
|
||
* 几行属性(rows:「注释」一行)。
|
||
*/
|
||
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } | { properties: PropertyRow[] })
|
||
|
||
interface Line { start: number, end: number }
|
||
|
||
/** 每行在源码里的起止下标(end 不含换行符)。 */
|
||
function lineRanges(source: string): Line[] {
|
||
const lines: Line[] = []
|
||
let start = 0
|
||
for (const text of splitLines(source)) {
|
||
lines.push({ start, end: start + text.length })
|
||
start += text.length + (source[start + text.length] === '\r' ? 2 : 1)
|
||
}
|
||
return lines
|
||
}
|
||
|
||
function leavesIn(node: Node, from: number, to: number, out: Node[]): void {
|
||
if (node.endIndex <= from || node.startIndex >= to || node.startIndex === node.endIndex)
|
||
return
|
||
if (node.childCount === 0) {
|
||
out.push(node)
|
||
return
|
||
}
|
||
for (const child of node.children) {
|
||
if (child)
|
||
leavesIn(child, from, to, out)
|
||
}
|
||
}
|
||
|
||
/** 着色的一层:一棵语法树,和给它的叶子定词性的语言插件。嵌着的代码的树叠在外层的树上面,盖住的地方按它着色。 */
|
||
interface ColorLayer {
|
||
root: Node
|
||
tokenRole: (leaf: Node) => TokenRole
|
||
}
|
||
|
||
/** 源码每个字符归哪个叶子(-1 是不归哪个叶子的空白),和各叶子的词性:一份源码算一次,各块按行取。 */
|
||
interface Coloring {
|
||
owner: Int32Array
|
||
roles: TokenRole[]
|
||
}
|
||
|
||
/** 各着色层依次铺上去,后面的层(嵌着的代码)盖住前面的。 */
|
||
function coloring(layers: readonly ColorLayer[], source: string): Coloring {
|
||
const owner = new Int32Array(source.length).fill(-1)
|
||
const roles: TokenRole[] = []
|
||
for (const { root, tokenRole } of layers) {
|
||
const leaves: Node[] = []
|
||
leavesIn(root, 0, source.length, leaves)
|
||
for (const leaf of leaves) {
|
||
roles.push(tokenRole(leaf))
|
||
owner.fill(roles.length - 1, leaf.startIndex, Math.min(leaf.endIndex, source.length))
|
||
}
|
||
}
|
||
return { owner, roles }
|
||
}
|
||
|
||
/** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段。 */
|
||
function colorLines({ owner, roles }: Coloring, source: string, lines: Line[], startRow: number, endRow: number, indent: number): Segment[] {
|
||
const segments: Segment[] = []
|
||
let current = -2
|
||
const put = (char: string, who: number): void => {
|
||
const prev = segments.at(-1)
|
||
const role = who < 0 ? 'plain' : roles[who]
|
||
if (prev && (who === current || (role === 'plain' && prev.role === 'plain')))
|
||
prev.text += char
|
||
else
|
||
segments.push({ text: char, role })
|
||
current = who
|
||
}
|
||
for (let row = startRow; row <= endRow; row++) {
|
||
const line = lines[row]
|
||
if (row > startRow)
|
||
put('\n', owner[line.start - 1])
|
||
for (let at = line.start + Math.min(indent, line.end - line.start); at < line.end; at++)
|
||
put(source[at], owner[at])
|
||
}
|
||
return segments
|
||
}
|
||
|
||
/**
|
||
* 这块本身就是哪个名字的定义:函数名、类名,只声明一个变量(或一个解构模式)时的变量名。
|
||
* 导入、一条语句声明几个变量时没有,它们的名字各是属性表里的一行(PropertyRow.defines)。
|
||
*/
|
||
function ownName(block: CodeBlock): Node | undefined {
|
||
switch (block.kind) {
|
||
case 'function':
|
||
case 'class':
|
||
case 'enum':
|
||
case 'exception':
|
||
case 'module':
|
||
return block.facts.name
|
||
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);没给时总是没查到。
|
||
*/
|
||
export interface RuleRun {
|
||
rules: readonly LibraryRule[]
|
||
report: (message: string) => void
|
||
lookup?: RuleContext['lookup']
|
||
}
|
||
|
||
const NO_RULES: RuleRun = { rules: [], report: () => {} }
|
||
|
||
/** 源码里一段的结尾:到哪个字符为止(不含),那个字符在第几行。 */
|
||
type SpanEnd = Pick<SourceSpan, 'endIndex' | 'endRow'>
|
||
|
||
function nodeEnd(node: Node): SpanEnd {
|
||
return { endIndex: node.endIndex, endRow: node.endPosition.row }
|
||
}
|
||
|
||
function later(a: SpanEnd, b: SpanEnd): SpanEnd {
|
||
return b.endIndex > a.endIndex ? b : a
|
||
}
|
||
|
||
/** 讲这块的注释(BlockShape.comments)里最靠后的结尾,和 end 比取靠后的:这一层最后一块代码后面的注释挂在它上面。 */
|
||
function commentsEnd(block: CodeBlock, end: SpanEnd): SpanEnd {
|
||
return (block.comments ?? []).reduce((acc, comment) => later(acc, comment.header), end)
|
||
}
|
||
|
||
/** 块的完整范围从哪起:头部和讲它的注释里最靠前的那个开头(注释写在块上面)。 */
|
||
function blockStart(block: CodeBlock): Pick<SourceSpan, 'startIndex' | 'startRow'> {
|
||
const first = block.comments?.[0]?.header
|
||
return first && first.startIndex < block.header.startIndex ? first : block.header
|
||
}
|
||
|
||
/** 各子孙块的头部、节点、讲它的注释里最靠后的结尾,和 end 比取靠后的:子孙一般写在块的节点里,Dockerfile 阶段的指令写在 FROM 后面。 */
|
||
function descendantsEnd(blocks: readonly CodeBlock[], end: SpanEnd): SpanEnd {
|
||
return blocks.reduce((acc, child) => descendantsEnd(child.children, commentsEnd(child, later(later(acc, child.header), nodeEnd(child.node)))), end)
|
||
}
|
||
|
||
/** index 之前最后一个非空白字符的结尾,不早于头部开头。 */
|
||
function lastCharBefore(source: string, header: SourceSpan, index: number): SpanEnd {
|
||
let end = index
|
||
while (end > header.startIndex && /\s/.test(source[end - 1]))
|
||
end--
|
||
return { endIndex: end, endRow: header.startRow + splitLines(source.slice(header.startIndex, end)).length - 1 }
|
||
}
|
||
|
||
/** 一块在源码里的完整范围(whole,编辑范围),和不算讲它的注释时这条语句的范围(statement)。 */
|
||
interface BlockSpan {
|
||
whole: SourceSpan
|
||
statement: SourceSpan
|
||
}
|
||
|
||
/**
|
||
* 一串兄弟块各自在源码里的范围:语句从头部开头,到头部、所在语句、各子孙块里最靠后的那个字符;完整范围再连同讲它的注释。
|
||
* 语言插件把 else、catch 这类子句展平成紧跟着的兄弟块,它们的节点都落在打头那块的节点(整条 if / try)里:
|
||
* 这条语句按各块的开头切开,每块到下一块开头(连同讲它的注释)之前最后一个非空白字符为止,最后一块到语句结尾(Shell 的 fi 归它)。
|
||
*/
|
||
function blockSpans(list: readonly CodeBlock[], source: string): BlockSpan[] {
|
||
let statement: Node | undefined
|
||
return list.map((block, i) => {
|
||
if (!statement || block.node.startIndex < statement.startIndex || block.node.endIndex > statement.endIndex)
|
||
statement = block.node
|
||
const next = list[i + 1]
|
||
const nextStart = next && Math.min(blockStart(next).startIndex, next.node.startIndex)
|
||
const until = nextStart !== undefined && nextStart < statement.endIndex ? lastCharBefore(source, block.header, nextStart) : nodeEnd(statement)
|
||
const end = descendantsEnd(block.children, later(block.header, until))
|
||
const wholeEnd = commentsEnd(block, end)
|
||
const start = blockStart(block)
|
||
return {
|
||
whole: { startIndex: start.startIndex, startRow: start.startRow, endIndex: wholeEnd.endIndex, endRow: wholeEnd.endRow },
|
||
statement: { startIndex: block.header.startIndex, startRow: block.header.startRow, endIndex: end.endIndex, endRow: end.endRow },
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 「注释」一行插进属性表:排在第一行(名称这类讲它是谁的一行)后面,有说明(值是 Markdown 的一行)时排在说明后面。
|
||
*/
|
||
function withCommentRow(rows: readonly PropertyRow[], row: PropertyRow): PropertyRow[] {
|
||
const doc = rows.findIndex(item => 'markdown' in item.value)
|
||
const at = doc >= 0 ? doc + 1 : Math.min(1, rows.length)
|
||
return [...rows.slice(0, at), row, ...rows.slice(at)]
|
||
}
|
||
|
||
/**
|
||
* 代码块树 → 视图数据:块的完整范围整行源码去共同缩进、按语法树着色,调用翻译插件。
|
||
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
|
||
* 并进去的子块不再单独成块。讲块的注释(BlockShape.comments)排成卡片上的「注释」一行(见 withCommentRow)。
|
||
*/
|
||
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 outer = blocks[0]?.node.tree.rootNode
|
||
const colors = coloring([
|
||
...(outer ? [{ root: outer, tokenRole: document.language.tokenRole }] : []),
|
||
...[...embedded.values()].map(part => ({ root: part.root, tokenRole: part.document.language.tokenRole })),
|
||
], source)
|
||
/** 一段占的整行源码去共同缩进,和它的着色(嵌着的代码按它那种语言着色)。 */
|
||
const sourceRows = ({ startRow, endRow }: SourceSpan): 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(colors, source, lines, startRow, endRow, indent),
|
||
}
|
||
}
|
||
/** 一段占的整行源码:没名字的块按它认(见 BlockAnchors.place)。 */
|
||
const rowsText = ({ startRow, endRow }: SourceSpan): string => texts.slice(startRow, endRow + 1).join('\n')
|
||
/** 按源码先后逐块给位置记号:先序遍历,块的记号在它的子块之前要 */
|
||
const anchors = new BlockAnchors()
|
||
|
||
/**
|
||
* 一种语言的块怎么转:用这种语言的源码、翻译插件和库规则(嵌着的代码没有库规则)。
|
||
* 转的是一串兄弟块:各块的完整范围(编辑范围)按这一串算(见 blockSpans),id 是 prefix 接上它在这一串里的位置(从 from 数起)。
|
||
* writable 为假时(嵌着的代码)块不能写注释:新写的行注释会写进外层语言的那几行里。
|
||
*/
|
||
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun, writable: boolean): (list: readonly CodeBlock[], prefix: string, scope: string, from?: number) => 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)?.body
|
||
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
|
||
return layer(steps, part.document, part.translator, NO_RULES, false)(steps.length === 1 && only ? only.children : steps, `${id}.`, scope, from)
|
||
}
|
||
/**
|
||
* 这块的概念,从左到右,同一个 id 只留第一个:语言的种类(图标、名字用内置种类和翻译插件起的种类名),
|
||
* 语言插件另认的,库规则的卡片再加规则的种类和规则另认的。card 不填就是没有规则卡片(认成现成块的也算)。
|
||
*/
|
||
function blockConcepts(block: CodeBlock, card?: RuleCard): BlockConcept[] {
|
||
const seen = new Set<string>()
|
||
const concepts: BlockConcept[] = []
|
||
const add = (concept: BlockConcept): void => {
|
||
if (seen.has(concept.id))
|
||
return
|
||
seen.add(concept.id)
|
||
concepts.push(concept)
|
||
}
|
||
add({
|
||
id: block.kind,
|
||
label: layerTranslator?.attributeNames?.values.kind?.[block.kind] ?? block.kind,
|
||
icon: KIND_ICONS[block.kind],
|
||
})
|
||
for (const concept of layerDocument.language.concepts?.(block) ?? [])
|
||
add(concept)
|
||
if (card) {
|
||
add({ id: card.kind, label: card.badge.label, icon: card.badge.icon, color: card.badge.color })
|
||
for (const concept of card.concepts ?? [])
|
||
add(concept)
|
||
}
|
||
return concepts
|
||
}
|
||
/** 「注释」一行:讲这块的几段注释(BlockShape.comments)各读成一段 Markdown,空行隔开;名字是翻译插件给种类 comment 起的。没有注释、没有翻译插件时没有。 */
|
||
function commentRow(block: CodeBlock): PropertyRow | undefined {
|
||
if (!layerTranslator || !block.comments)
|
||
return undefined
|
||
const parts = block.comments.flatMap((comment) => {
|
||
const { text } = layerTranslator.translate(comment, layerDocument)
|
||
return 'markdown' in text && text.markdown !== '' ? [text.markdown] : []
|
||
})
|
||
const label = layerTranslator.attributeNames?.values.kind?.comment ?? 'comment'
|
||
return parts.length > 0 ? { label, value: { markdown: parts.join('\n\n') } } : undefined
|
||
}
|
||
/** 属性表卡片的属性表连同「注释」一行(见 withCommentRow)。 */
|
||
function cardRows(block: CodeBlock, rows: PropertyRow[]): { properties: PropertyRow[] } {
|
||
const row = commentRow(block)
|
||
return { properties: row ? withCommentRow(rows, row) : rows }
|
||
}
|
||
/** 「重写注释」写到哪(见 CommentTarget):块前面的普通注释换掉,没有就新写在块上面。 */
|
||
/** 「重写注释」写到哪:有说明就换说明,否则换块前面的普通注释,都没有就按这门语言的说明新写。 */
|
||
function commentTarget(block: CodeBlock): Pick<BlockView, 'commentTarget'> {
|
||
if (!writable)
|
||
return {}
|
||
if (isCommentBlock(block))
|
||
return { commentTarget: { kind: 'replace', startLine: block.header.startRow, endLine: block.header.endRow } }
|
||
const leading = (block.comments ?? []).filter(comment => comment.header.startIndex < block.header.startIndex)
|
||
const doc = explanationSpan(block)
|
||
const covered = doc !== undefined && leading.some(comment => comment.header.startIndex <= doc.startIndex && comment.header.endIndex >= doc.endIndex)
|
||
if (doc && !covered)
|
||
return { commentTarget: { kind: 'replace', startLine: doc.startRow, endLine: doc.endRow } }
|
||
const [first] = leading
|
||
const last = leading.at(-1)
|
||
if (first && last)
|
||
return { commentTarget: { kind: 'replace', startLine: first.header.startRow, endLine: last.header.endRow } }
|
||
const fresh = layerDocument.language.freshDoc?.(block)
|
||
if (fresh)
|
||
return { commentTarget: { kind: 'insert', line: fresh.line, shell: fresh.shell, ...(fresh.breakAt !== undefined && { breakAt: fresh.breakAt }) } }
|
||
const marker = layerDocument.language.lineComment
|
||
return marker ? { commentTarget: { kind: 'insert', line: block.header.startRow, shell: `${marker} _` } } : {}
|
||
}
|
||
/** 一行译文的卡片:译文、实参表、说明,有注释时下面再排「注释」一行。 */
|
||
function textCard(block: TextBlock): ReturnType<typeof textView> & { rows?: PropertyRow[] } {
|
||
const row = commentRow(block)
|
||
return { ...textView(block), ...(row && { rows: [row] }) }
|
||
}
|
||
|
||
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):编辑范围是 span,头部是块自己那部分,子块照常交给各规则看。scope 见 BlockAnchors.place */
|
||
function plain(block: CodeBlock, span: SourceSpan, id: string, scope: string): BlockView {
|
||
const name = ownName(block)
|
||
const { anchor, inner } = anchors.place(scope, block.kind, name?.text, rowsText(block.header))
|
||
return {
|
||
id,
|
||
anchor,
|
||
kind: block.kind,
|
||
...sourceRows(span),
|
||
headerEndLine: block.header.endRow,
|
||
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
|
||
attributes: blockAttributes(block),
|
||
concepts: blockConcepts(block),
|
||
...commentTarget(block),
|
||
children: [
|
||
...list(block.children, `${id}.`, inner),
|
||
...embeddedChildren(block, id, inner, block.children.length),
|
||
],
|
||
...('facts' in block ? cardRows(block, layerTranslator ? layerTranslator.describe(block, layerDocument) : []) : textCard(block)),
|
||
}
|
||
}
|
||
/**
|
||
* 块先问启用的库规则,没认领就照它自己的样子。认领了的:规则讲的是整条语句,头部就是整个范围;
|
||
* 规则没并进来的子块(方法、别的成员)不管写在哪,都照源码顺序留作它的子块。规则略去的块(讲在了别的卡片里)不出块。
|
||
*/
|
||
function build(block: CodeBlock, { whole: span, statement }: BlockSpan, id: string, scope: string): BlockView | undefined {
|
||
const claim = active.length > 0 ? claimBlock(block, active, context, layerRules.report) : undefined
|
||
if (!claim)
|
||
return plain(block, span, id, scope)
|
||
const replacement = claim.block
|
||
if ('omit' in replacement)
|
||
return undefined
|
||
const kept = block.children.filter(child => !replacement.absorbs.includes(child))
|
||
if ('as' in replacement) {
|
||
const { attributes, comments } = block
|
||
return plain({ ...replacement.as, node: block.node, header: statement, children: kept, ...(attributes && { attributes }), ...(comments && { comments }) }, span, id, scope)
|
||
}
|
||
const { kind, badge, rows, defines, attributes } = replacement
|
||
const slots = new Map((replacement.slots ?? []).map(({ block: child, row, item }): [CodeBlock, BlockSlot] => [child, { row, item }]))
|
||
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), rowsText(statement))
|
||
return {
|
||
id,
|
||
anchor,
|
||
kind,
|
||
...sourceRows(span),
|
||
headerEndLine: statement.endRow,
|
||
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
|
||
attributes: { ...blockAttributes(block), ...attributes, kind },
|
||
concepts: blockConcepts(block, replacement),
|
||
...commentTarget(block),
|
||
badge,
|
||
children: list(kept, `${id}.`, inner, 0, slots),
|
||
...cardRows(block, rows),
|
||
}
|
||
}
|
||
/** 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。slots 是父卡片挂在它属性表子行上的那几块(见 BlockSlot)。 */
|
||
function list(siblings: readonly CodeBlock[], prefix: string, scope: string, from = 0, slots?: ReadonlyMap<CodeBlock, BlockSlot>): BlockView[] {
|
||
const spans = blockSpans(siblings, source)
|
||
const views: BlockView[] = []
|
||
siblings.forEach((block, i) => {
|
||
const view = build(block, spans[i], `${prefix}${from + views.length}`, scope)
|
||
const slot = slots?.get(block)
|
||
if (view)
|
||
views.push(slot ? { ...view, slot } : view)
|
||
})
|
||
return views
|
||
}
|
||
return list
|
||
}
|
||
|
||
return layer(blocks, document, translator, rules, true)(blocks, '', '')
|
||
}
|
||
|
||
/**
|
||
* 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)、
|
||
* 语言插件自己认得出的名字(LanguageAdapter.symbols,语言扩展查不到定义、各处出现时用)、
|
||
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)。
|
||
*/
|
||
export interface DocumentViews {
|
||
blocks: BlockView[]
|
||
displayNames: DisplayName[]
|
||
symbols: LocalSymbol[]
|
||
imports: ImportedName[]
|
||
}
|
||
|
||
/**
|
||
* 排视图数据时宿主另外交给规则、翻译插件的:用哪些库规则(RuleRun),调用的名字眼下查到定义成哪种块(SourceDocument.calleeKind),
|
||
* 命令名的命令规则(SourceDocument.commandRule)。
|
||
*/
|
||
export interface DocumentExtras {
|
||
fileName?: SourceDocument['fileName']
|
||
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']
|
||
}
|
||
|
||
/** 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。 */
|
||
export async function readBlockViews(
|
||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||
adapter: LanguageAdapter,
|
||
translator: Translator | undefined,
|
||
languageId: string,
|
||
source: string,
|
||
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 { fileName, calleeKind, commandRule } = extras
|
||
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,
|
||
...(fileName && { fileName }),
|
||
...(calleeKind && { calleeKind }),
|
||
...(commandRule && { commandRule }),
|
||
...(parts.size > 0 && { embedded }),
|
||
}
|
||
const codeBlocks = adapter.toBlocks(tree.rootNode, source, languageId)
|
||
const blocks = toBlockViews(codeBlocks, 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, imports: importedNames(importsIn(codeBlocks)) }
|
||
}
|
||
finally {
|
||
for (const parsed of trees)
|
||
parsed.delete()
|
||
}
|
||
}
|
||
|
||
/** 块树里自己的名字(见 ownName)盖住 offset 的最里层那块的种类;offset 不在哪块的名字上时没有。 */
|
||
export function blockKindAt(blocks: readonly CodeBlock[], offset: number): BlockKind | undefined {
|
||
for (const block of blocks) {
|
||
const inner = blockKindAt(block.children, offset)
|
||
if (inner)
|
||
return inner
|
||
const name = ownName(block)
|
||
if (name && name.startIndex <= offset && offset < name.endIndex)
|
||
return block.kind
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/** 解析一份源码,offset 处的名字定义的是哪种块(见 blockKindAt);语法树用完即释放。 */
|
||
export async function readDefinitionKind(
|
||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||
adapter: LanguageAdapter,
|
||
languageId: string,
|
||
source: string,
|
||
offset: number,
|
||
): Promise<BlockKind | undefined> {
|
||
const tree = await runtime.parse(adapter, languageId, source)
|
||
try {
|
||
return blockKindAt(adapter.toBlocks(tree.rootNode, source, languageId), offset)
|
||
}
|
||
finally {
|
||
tree.delete()
|
||
}
|
||
}
|