import type { BlockAttributes, CodeBlock, KindBadge, LanguageAdapter, LibraryRule, Node, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TokenRole, Translator } from './contract' import type { TreeSitterRuntime } from './runtime' import { blockAttributes } from './attributes' import { commonIndent, splitLines } from './editText' import { absorbedRange, activeRules, claimBlock, importsIn } from './libraryRules' interface BlockViewBase { /** 在树里的位置,例如 '6.1.0'。 */ id: string /** BlockKind,或库规则认领的块的种类 id(RuleBlock.kind,带点,例如 sqlalchemy.table)。 */ kind: string /** 编辑范围的起止行,0 起:一般是 header;库规则认领的块到最后一个并进来的子块为止(见 RuleBlock.absorbs)。 */ startLine: number endLine: number /** 编辑范围的整行源码,去掉共同缩进。 */ code: string /** 源码着色(拼起来等于 code)。 */ codeTokens: Segment[] /** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */ defines?: SourceRange /** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */ attributes: BlockAttributes /** 库规则认领的块才有:种类标签的文字和颜色(RuleBlock.badge)。预览同类块一样默认收起它的子块。 */ badge?: KindBadge children: BlockView[] } /** * 发给 webview 的一块(可序列化):函数、类、导入、常量、变量块和库规则认领的块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */ export type BlockView = BlockViewBase & ({ translation: PreviewText } | { 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) } } /** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段。 */ function colorLines(root: Node, source: string, lines: Line[], startRow: number, endRow: number, indent: number, tokenRole: (leaf: Node) => TokenRole): 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 }) 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 - from]) for (let at = line.start + Math.min(indent, line.end - line.start); at < line.end; at++) put(source[at], owner[at - from]) } return segments } /** * 这块本身就是哪个名字的定义:函数名、类名,只声明一个变量(或一个解构模式)时的变量名。 * 导入、一条语句声明几个变量时没有,它们的名字各是属性表里的一行(PropertyRow.defines)。 */ function ownName(block: CodeBlock): Node | undefined { switch (block.kind) { case 'function': case 'class': return block.facts.name case 'const': case 'variable': return block.facts.declarators.length === 1 ? block.facts.declarators[0].name : undefined default: return undefined } } /** 用哪些库规则(某 languageId 注册的,按注册顺序;这份源码导入了规则关心的库才启用),规则出错时怎么记。 */ export interface RuleRun { rules: readonly LibraryRule[] report: (message: string) => void } const NO_RULES: RuleRun = { rules: [], report: () => {} } /** * 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。 * 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock),并进去的子块不再单独成块。 */ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined, rules: RuleRun = NO_RULES): BlockView[] { const lines = lineRanges(source) const texts = splitLines(source) const document: SourceDocument = { source, language: adapter } 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), } /** startRow..endRow 这几行整行源码去共同缩进,和它的着色。 */ const sourceRows = (root: Node, startRow: number, endRow: number): Pick => { 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), } } const build = (block: CodeBlock, id: string): BlockView => { const root = block.node.tree.rootNode const claim = active.length > 0 ? claimBlock(block, active, context, rules.report) : undefined if (claim) { const { kind, badge, rows, defines, attributes } = claim.block const { endRow, kept } = absorbedRange(block, claim.block.absorbs) return { id, kind, ...sourceRows(root, block.header.startRow, endRow), ...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }), attributes: { ...blockAttributes(block), ...attributes, kind }, badge, children: kept.map((child, i) => build(child, `${id}.${i}`)), properties: rows, } } const name = ownName(block) return { id, kind: block.kind, ...sourceRows(root, block.header.startRow, block.header.endRow), ...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }), attributes: blockAttributes(block), children: block.children.map((child, i) => build(child, `${id}.${i}`)), ...('facts' in block ? { properties: translator ? translator.describe(block, document) : [] } : { translation: translator ? translator.translate(block, document) : [] }), } } return blocks.map((block, i) => build(block, String(i))) } /** 解析一份源码并转成视图数据;语法树用完即释放。 */ export async function readBlockViews( runtime: Pick, adapter: LanguageAdapter, translator: Translator | undefined, languageId: string, source: string, rules: RuleRun = NO_RULES, ): Promise { const tree = await runtime.parse(adapter, languageId, source) try { return toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), source, adapter, translator, rules) } finally { tree.delete() } }