144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
|
import type { TreeSitterRuntime } from './runtime'
|
|
import { commonIndent, splitLines } from './editText'
|
|
|
|
interface BlockViewBase {
|
|
/** 在树里的位置,例如 '6.1.0'。 */
|
|
id: string
|
|
kind: BlockKind
|
|
/** header 的起止行,0 起。 */
|
|
startLine: number
|
|
endLine: number
|
|
/** header 所在的整行源码,去掉共同缩进。 */
|
|
code: string
|
|
/** 源码着色(拼起来等于 code)。 */
|
|
codeTokens: Segment[]
|
|
/** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */
|
|
defines?: SourceRange
|
|
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
|
|
}
|
|
}
|
|
|
|
/** 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。 */
|
|
export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined): BlockView[] {
|
|
const lines = lineRanges(source)
|
|
const texts = splitLines(source)
|
|
const build = (block: CodeBlock, id: string): BlockView => {
|
|
const { startRow, endRow } = block.header
|
|
const rows = texts.slice(startRow, endRow + 1)
|
|
const indent = commonIndent(rows).length
|
|
const name = ownName(block)
|
|
return {
|
|
id,
|
|
kind: block.kind,
|
|
startLine: startRow,
|
|
endLine: endRow,
|
|
code: rows.map(row => row.slice(Math.min(indent, row.length))).join('\n'),
|
|
codeTokens: colorLines(block.node.tree.rootNode, source, lines, startRow, endRow, indent, adapter.tokenRole),
|
|
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
|
|
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
|
|
...('facts' in block
|
|
? { properties: translator ? translator.describe(block, source) : [] }
|
|
: { translation: translator ? translator.translate(block, source) : [] }),
|
|
}
|
|
}
|
|
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,
|
|
): Promise<BlockView[]> {
|
|
const tree = await runtime.parse(adapter, languageId, source)
|
|
try {
|
|
return toBlockViews(adapter.toBlocks(tree.rootNode, source), source, adapter, translator)
|
|
}
|
|
finally {
|
|
tree.delete()
|
|
}
|
|
}
|