197 lines
8.1 KiB
TypeScript
197 lines
8.1 KiB
TypeScript
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<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),
|
||
}
|
||
}
|
||
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<TreeSitterRuntime, 'parse'>,
|
||
adapter: LanguageAdapter,
|
||
translator: Translator | undefined,
|
||
languageId: string,
|
||
source: string,
|
||
rules: RuleRun = NO_RULES,
|
||
): Promise<BlockView[]> {
|
||
const tree = await runtime.parse(adapter, languageId, source)
|
||
try {
|
||
return toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), source, adapter, translator, rules)
|
||
}
|
||
finally {
|
||
tree.delete()
|
||
}
|
||
}
|