Files
superwork/vscode/src/codeReader/blockView.ts
T

618 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
}