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

197 lines
8.1 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 { 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()
}
}