✨ feat(vscode): 代码阅读的解析与模板直译核心
This commit is contained in:
1 parent
af56985d9a
commit
4d34b8c715
24 files changed
+2294
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, Segment, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
|
||||
/** 发给 webview 的一块(可序列化)。 */
|
||||
export interface BlockView {
|
||||
/** 在树里的位置,例如 '6.1.0'。 */
|
||||
id: string
|
||||
kind: BlockKind
|
||||
/** header 的起止行,0 起。 */
|
||||
startLine: number
|
||||
endLine: number
|
||||
/** header 所在的整行源码,去掉共同缩进。 */
|
||||
code: string
|
||||
/** 源码着色(拼起来等于 code)。 */
|
||||
codeTokens: Segment[]
|
||||
translation: Segment[]
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** 代码块树 → 视图数据: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
|
||||
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),
|
||||
translation: translator ? translator.translate(block, source) : [],
|
||||
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user