✨ feat(vscode): 代码阅读的解析与模板直译核心

This commit is contained in:
cruldra committed 2026-09-24 21:16:21 +08:00
1 parent af56985d9a
commit 4d34b8c715
24 files changed
+2294

No files matched your search

@@ -0,0 +1,88 @@
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { createPythonAdapter } from './python'
const adapter = createPythonAdapter(WASM_DIR)
function tree(source: string): Promise<Outline[]> {
return withBlocks(adapter, 'python', source, outline)
}
// fixtures/sample.py:行号 1 起
const SAMPLE: Outline[] = [
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: 4, kind: 'import' },
{ at: 6, kind: 'const' },
{ at: 7, kind: 'variable' },
{ at: 10, kind: 'function', kids: [
{ at: 11, kind: 'comment' },
{ at: 12, kind: 'comment' },
{ at: 13, kind: 'variable' },
{ at: 14, kind: 'if', kids: [{ at: 15, kind: 'return' }] },
{ at: 16, kind: 'else', kids: [{ at: 17, kind: 'variable' }] },
{ at: 18, kind: 'else', kids: [{ at: 19, kind: 'statement' }] },
{ at: 20, kind: 'loop', kids: [
{ at: 21, kind: 'if', kids: [{ at: 22, kind: 'statement' }] },
] },
{ at: 23, kind: 'try', kids: [
{ at: 24, kind: 'statement', kids: [{ at: 25, kind: 'statement' }] },
] },
{ at: 26, kind: 'catch', kids: [{ at: 27, kind: 'statement' }] },
{ at: 28, kind: 'finally', kids: [{ at: 29, kind: 'statement' }] },
{ at: 30, kind: 'return' },
] },
{ at: 33, kind: 'class', kids: [
{ at: 34, kind: 'field' },
{ at: 35, kind: 'field' },
{ at: 36, kind: 'field' },
{ at: 37, kind: 'variable' },
{ at: 39, kind: 'function', kids: [{ at: 40, kind: 'return' }] },
] },
]
describe('python 代码块树', () => {
it('样例覆盖 import / def / docstring / if-elif-else / for / try-except-finally / with / 推导式 / class 字段', async () => {
expect(await tree(readFixture('sample.py'))).toEqual(SAMPLE)
})
it('装饰器算进头部;冒号后、主体前的注释归子块;循环的 else 是兄弟块;match 的 case 是子块 if', async () => {
const source = [
'@cache',
'def load(key):',
' # 先查缓存',
' while key:',
' key = key[1:]',
' else:',
' pass',
' match key:',
' case \'a\':',
' return 1',
' case _:',
' return 2',
].join('\n')
expect(await tree(source)).toEqual([
{ at: [1, 2], kind: 'function', kids: [
{ at: 3, kind: 'comment' },
{ at: 4, kind: 'loop', kids: [{ at: 5, kind: 'variable' }] },
{ at: 6, kind: 'else', kids: [{ at: 7, kind: 'statement' }] },
{ at: 8, kind: 'if', kids: [
{ at: 9, kind: 'if', kids: [{ at: 10, kind: 'return' }] },
{ at: 11, kind: 'if', kids: [{ at: 12, kind: 'return' }] },
] },
] },
])
})
it('docstring 只认主体开头;不是开头的字符串表达式是语句', async () => {
const source = [
'def f():',
' x = 1',
' """not a docstring"""',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'function', kids: [{ at: 2, kind: 'variable' }, { at: 3, kind: 'statement' }] },
])
})
})
+217
View File
@@ -0,0 +1,217 @@
import type { BlockKind, CodeBlock, LanguageAdapter, Node, TokenRole } from '../contract'
import * as path from 'node:path'
import { compoundBlock, leafBlock, listBlocks, named, sameNode } from './shared'
/** 常量、类字段只认直接写在模块 / 类主体里的赋值。 */
type Scope = 'module' | 'class' | 'other'
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
function statements(items: Node[], scope: Scope, afterRow: number, docstring: boolean): CodeBlock[] {
return listBlocks(items, afterRow, (item, first) => statementBlocks(item, scope, docstring && first), () => true)
}
/** 冒号后面的主体:body / consequence 字段,except / finally 子句里是不带字段名的 block。 */
function bodyOf(node: Node): Node | null {
const body = node.childForFieldName('body') ?? node.childForFieldName('consequence')
if (body)
return body
const blocks = named(node).filter(child => child.type === 'block')
return blocks.at(-1) ?? null
}
function bodyBlock(kind: BlockKind, outer: Node, body: Node | null, scope: Scope, docstring = false): CodeBlock {
if (!body)
return leafBlock(kind, outer)
return compoundBlock(kind, outer, body.startIndex, named(body), (list, afterRow) => statements(list, scope, afterRow, docstring))
}
function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBlock[] {
switch (node.type) {
case 'import_statement':
case 'import_from_statement':
case 'future_import_statement':
return [leafBlock('import', node)]
case 'function_definition':
case 'class_definition':
return [definitionBlock(node, node)]
case 'decorated_definition': {
const definition = node.childForFieldName('definition')
return [definition ? definitionBlock(definition, node) : leafBlock('statement', node)]
}
case 'if_statement':
return ifBlocks(node)
case 'for_statement':
case 'while_statement':
return loopBlocks(node)
case 'try_statement':
return tryBlocks(node)
case 'with_statement':
return [bodyBlock('statement', node, bodyOf(node), 'other')]
case 'match_statement':
return [bodyBlock('if', node, bodyOf(node), 'other')]
case 'case_clause':
return [bodyBlock('if', node, bodyOf(node), 'other')]
case 'return_statement':
return [leafBlock('return', node)]
case 'expression_statement':
return [leafBlock(expressionKind(node, scope, docstring), node)]
default:
return [leafBlock('statement', node)]
}
}
/** function_definition / class_definition;outer 是带装饰器的整体时头部从装饰器开始。 */
function definitionBlock(definition: Node, outer: Node): CodeBlock {
const isClass = definition.type === 'class_definition'
return bodyBlock(isClass ? 'class' : 'function', outer, definition.childForFieldName('body'), isClass ? 'class' : 'other', true)
}
function isConstantTarget(target: Node | null): boolean {
if (!target)
return false
if (target.type === 'identifier')
return CONSTANT_NAME.test(target.text)
if (target.type === 'pattern_list' || target.type === 'tuple_pattern') {
const names = named(target)
return names.length > 0 && names.every(isConstantTarget)
}
return false
}
function expressionKind(node: Node, scope: Scope, docstring: boolean): BlockKind {
const [expression, ...rest] = named(node)
if (docstring && rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string'))
return 'comment'
if (expression?.type !== 'assignment')
return 'statement'
if (scope === 'class' && expression.childForFieldName('type'))
return 'field'
if (scope === 'module' && isConstantTarget(expression.childForFieldName('left')))
return 'const'
return 'variable'
}
/** elif / else 展平成紧跟 if 的兄弟块。 */
function ifBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('if', node, bodyOf(node), 'other')]
for (const alternative of node.childrenForFieldName('alternative')) {
if (alternative)
blocks.push(bodyBlock('else', alternative, bodyOf(alternative), 'other'))
}
return blocks
}
/** 循环的 else 是兄弟块 else。 */
function loopBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('loop', node, bodyOf(node), 'other')]
const alternative = node.childForFieldName('alternative')
if (alternative)
blocks.push(bodyBlock('else', alternative, bodyOf(alternative), 'other'))
return blocks
}
const TRY_CLAUSES: Record<string, BlockKind> = {
except_clause: 'catch',
except_group_clause: 'catch',
else_clause: 'else',
finally_clause: 'finally',
}
function tryBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('try', node, node.childForFieldName('body'), 'other')]
for (const clause of named(node)) {
const kind = TRY_CLAUSES[clause.type]
if (kind)
blocks.push(bodyBlock(kind, clause, bodyOf(clause), 'other'))
}
return blocks
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
const STORAGE_KEYWORDS = new Set(['def', 'class', 'async', 'lambda', 'global', 'nonlocal', 'del'])
const OPERATOR_KEYWORDS = new Set(['and', 'or', 'not', 'is'])
const COMPARISON = new Set(['comparison_operator', 'not in', 'is not'])
const LITERAL_KEYWORDS = new Set(['none', 'true', 'false'])
function insideString(leaf: Node): boolean {
for (let node = leaf.parent; node; node = node.parent) {
if (node.type === 'interpolation')
return false
if (node.type === 'string')
return true
}
return false
}
/** 注解里的名字:typed_parameter / 返回值 / 带注解赋值的 type 节点之内。 */
export function insideAnnotation(leaf: Node): boolean {
for (let node = leaf.parent; node; node = node.parent) {
if (node.type === 'type')
return true
if (node.type === 'block' || node.type === 'module')
return false
}
return false
}
function isCallee(node: Node): boolean {
const parent = node.parent
return !!parent && parent.type === 'call' && sameNode(parent.childForFieldName('function'), node)
}
function identifierRole(leaf: Node): TokenRole {
const parent = leaf.parent
if (!parent)
return 'variable'
if (insideAnnotation(leaf))
return 'type'
if (parent.type === 'function_definition' && sameNode(parent.childForFieldName('name'), leaf))
return 'function'
if (parent.type === 'class_definition' && sameNode(parent.childForFieldName('name'), leaf))
return 'type'
if (parent.type === 'decorator' || isCallee(leaf))
return 'function'
if (parent.type === 'attribute' && sameNode(parent.childForFieldName('attribute'), leaf) && isCallee(parent))
return 'function'
return 'variable'
}
export function pythonTokenRole(leaf: Node): TokenRole {
if (leaf.type === 'comment')
return 'comment'
if (insideString(leaf))
return 'string'
if (!leaf.isNamed) {
if (!/^[a-z]+$/.test(leaf.type))
return 'plain'
if (STORAGE_KEYWORDS.has(leaf.type))
return 'storage'
if (OPERATOR_KEYWORDS.has(leaf.type) || (leaf.type === 'in' && COMPARISON.has(leaf.parent?.type ?? '')))
return 'operator'
return 'keyword'
}
if (LITERAL_KEYWORDS.has(leaf.type))
return 'storage'
switch (leaf.type) {
case 'integer':
case 'float':
return 'number'
case 'identifier':
return identifierRole(leaf)
default:
return 'plain'
}
}
/** Python 语言插件。@param wasmDir 语法包 .wasm 所在目录 */
export function createPythonAdapter(wasmDir: string): LanguageAdapter {
return {
id: 'python',
languageIds: ['python'],
grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'),
toBlocks: root => statements(named(root), 'module', -1, true),
tokenRole: pythonTokenRole,
}
}
+122
View File
@@ -0,0 +1,122 @@
import type { BlockKind, CodeBlock, Node, SourceSpan } from '../contract'
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
export function named(node: Node | null | undefined): Node[] {
return node ? node.namedChildren.filter((child): child is Node => child !== null) : []
}
export function children(node: Node): Node[] {
return node.children.filter((child): child is Node => child !== null)
}
export function nodeSpan(node: Node): SourceSpan {
return {
startIndex: node.startIndex,
endIndex: node.endIndex,
startRow: node.startPosition.row,
endRow: node.endPosition.row,
}
}
export function leafBlock(kind: BlockKind, node: Node): CodeBlock {
return { kind, node, header: nodeSpan(node), children: [] }
}
/** 头部往前扩到 from(装饰器)。 */
export function extendHeader(block: CodeBlock, from: Node): CodeBlock {
return { ...block, header: { ...block.header, startIndex: from.startIndex, startRow: from.startPosition.row } }
}
function lastLeafBefore(node: Node, limit: number): Node | undefined {
for (let i = node.childCount - 1; i >= 0; i--) {
const child = node.child(i)
if (!child || child.startIndex >= limit || child.type === 'comment' || child.startIndex === child.endIndex)
continue
if (child.childCount === 0)
return child
const found = lastLeafBefore(child, limit)
if (found)
return found
}
return undefined
}
/** 复合语句的头部:从 outer 开头到主体开始之前最后一个非注释的词为止。 */
export function headerSpan(outer: Node, bodyStart: number): SourceSpan {
const last = lastLeafBefore(outer, bodyStart)
if (!last)
return nodeSpan(outer)
return {
startIndex: outer.startIndex,
endIndex: last.endIndex,
startRow: outer.startPosition.row,
endRow: last.endPosition.row,
}
}
/** node 里完全落在 [from, to) 之间的注释。 */
function commentsIn(node: Node, from: number, to: number): Node[] {
if (node.endIndex <= from || node.startIndex >= to)
return []
if (node.type === 'comment')
return node.startIndex >= from && node.endIndex <= to ? [node] : []
return children(node).flatMap(child => commentsIn(child, from, to))
}
export type ListBlocks = (items: Node[], afterRow: number) => CodeBlock[]
/**
* 复合语句块:头部到 bodyStart 之前,子块来自 items。
* 头部和主体之间的注释(例如 Python 冒号后、主体前的注释)也归子块。
*/
export function compoundBlock(kind: BlockKind, outer: Node, bodyStart: number, items: Node[], list: ListBlocks): CodeBlock {
const header = headerSpan(outer, bodyStart)
const between = commentsIn(outer, header.endIndex, bodyStart)
return { kind, node: outer, header, children: list([...between, ...items], header.endRow) }
}
/**
* 一串语句 → 块。注释单独成块,相邻行的行注释合成一块;
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
* afterRow 是这串语句之前的最后一行(头部结尾行),模块顶层传 -1;
* toBlocks 的 first 表示它是这串里第一条非注释语句(Python 认 docstring 用)。
*/
export function listBlocks(
items: Node[],
afterRow: number,
toBlocks: (item: Node, first: boolean) => CodeBlock[],
isLineComment: (comment: Node) => boolean,
): CodeBlock[] {
const out: CodeBlock[] = []
let lastRow = afterRow
let lineRun: CodeBlock | undefined
let first = true
for (const item of items) {
if (item.type === 'comment') {
if (item.startPosition.row === lastRow)
continue
const line = isLineComment(item)
if (line && lineRun && item.startPosition.row === lineRun.header.endRow + 1) {
lineRun.header = { ...lineRun.header, endIndex: item.endIndex, endRow: item.endPosition.row }
}
else {
const block = leafBlock('comment', item)
out.push(block)
lineRun = line ? block : undefined
}
}
else {
out.push(...toBlocks(item, first))
first = false
lineRun = undefined
}
lastRow = item.endPosition.row
}
return out
}
/** 同一个节点:web-tree-sitter 每次取子节点都会新建对象,只能按位置和类型比。 */
export function sameNode(a: Node | null | undefined, b: Node): boolean {
return !!a && a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type
}
@@ -0,0 +1,227 @@
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { createTypeScriptAdapter } from './typescript'
const adapter = createTypeScriptAdapter(WASM_DIR)
function tree(source: string, languageId = 'typescript'): Promise<Outline[]> {
return withBlocks(adapter, languageId, source, outline)
}
// 与原型 docs/prototype/code-reader/figma-plugin/code.js 的 CODE_TREE 一致
const ENV_LOCK: Outline[] = [
{ at: [1, 10], kind: 'comment' },
{ at: 12, kind: 'import' },
{ at: 13, kind: 'import' },
{ at: 15, kind: 'const' },
{ at: 16, kind: 'const' },
{ at: [18, 22], kind: 'comment' },
{ at: 23, kind: 'function', kids: [
{ at: 24, kind: 'variable' },
{ at: 25, kind: 'if', kids: [
{ at: 26, kind: 'return' },
] },
{ at: 27, kind: 'variable' },
{ at: 28, kind: 'variable' },
{ at: 29, kind: 'return' },
] },
{ at: 32, kind: 'type', kids: [
{ at: 33, kind: 'field' },
{ at: 34, kind: 'field' },
{ at: 35, kind: 'field' },
] },
{ at: 38, kind: 'function', kids: [
{ at: 39, kind: 'variable' },
{ at: 40, kind: 'variable' },
{ at: 41, kind: 'loop', kids: [
{ at: 42, kind: 'try', kids: [
{ at: 43, kind: 'statement' },
{ at: 44, kind: 'statement' },
] },
{ at: 46, kind: 'catch', kids: [
{ at: 47, kind: 'statement' },
] },
] },
{ at: 50, kind: 'return' },
] },
{ at: 53, kind: 'comment' },
{ at: 54, kind: 'function', kids: [
{ at: 55, kind: 'return' },
] },
{ at: 58, kind: 'comment' },
{ at: 59, kind: 'function', kids: [
{ at: 60, kind: 'return' },
] },
]
const ISSUE_LOCK: Outline[] = [
{ at: 1, kind: 'import' },
{ at: 3, kind: 'type', kids: [
{ at: 4, kind: 'field' },
{ at: 5, kind: 'field' },
{ at: 6, kind: 'field' },
] },
{ at: [9, 12], kind: 'comment' },
{ at: 13, kind: 'function', kids: [
{ at: 14, kind: 'variable' },
{ at: 15, kind: 'if', kids: [
{ at: 16, kind: 'return' },
] },
{ at: 17, kind: 'variable' },
{ at: 18, kind: 'if', kids: [
{ at: 19, kind: 'return' },
] },
{ at: [20, 24], kind: 'return' },
] },
]
describe('typeScript 代码块树', () => {
it('envLock.ts 与原型一致', async () => {
expect(await tree(readFixture('envLock.ts.txt'))).toEqual(ENV_LOCK)
})
it('issueLock.ts 与原型一致', async () => {
expect(await tree(readFixture('issueLock.ts.txt'))).toEqual(ISSUE_LOCK)
})
it('else / else if 展平成兄弟块,else if 的子块来自它自己的主体', async () => {
const source = [
'if (a) {',
' x()',
'} else if (b) {',
' y()',
'} else',
' z()',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'if', kids: [{ at: 2, kind: 'statement' }] },
{ at: 3, kind: 'else', kids: [{ at: 4, kind: 'statement' }] },
{ at: 5, kind: 'else', kids: [{ at: 6, kind: 'statement' }] },
])
})
it('try / catch / finally 是兄弟块;switch 的 case / default 是子块 if', async () => {
const source = [
'try {',
' a()',
'}',
'catch {',
' b()',
'}',
'finally {',
' c()',
'}',
'switch (x) {',
' case 1:',
' foo()',
' break',
' default:',
' bar()',
'}',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'try', kids: [{ at: 2, kind: 'statement' }] },
{ at: 4, kind: 'catch', kids: [{ at: 5, kind: 'statement' }] },
{ at: 7, kind: 'finally', kids: [{ at: 8, kind: 'statement' }] },
{ at: 10, kind: 'if', kids: [
{ at: 11, kind: 'if', kids: [{ at: 12, kind: 'statement' }, { at: 13, kind: 'statement' }] },
{ at: 14, kind: 'if', kids: [{ at: 15, kind: 'statement' }] },
] },
])
})
it('类:方法 → function、字段 → field、注释 → comment,装饰器算进成员头部', async () => {
const source = [
'export class Store extends Base {',
' // 缓存',
' private readonly cache = new Map()',
' @memo()',
' get size(): number {',
' return this.cache.size',
' }',
' handle = () => {',
' this.cache.clear()',
' }',
'}',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'class', kids: [
{ at: 2, kind: 'comment' },
{ at: 3, kind: 'field' },
{ at: [4, 5], kind: 'function', kids: [{ at: 6, kind: 'return' }] },
{ at: 8, kind: 'function', kids: [{ at: 9, kind: 'statement' }] },
] },
])
})
it('enum 成员 → field;type 别名 → type;多个循环 → loop', async () => {
const source = [
'enum Color {',
' Red = 1,',
' Blue,',
'}',
'type Pair = [string, number]',
'for (let i = 0; i < 3; i++) {',
' while (busy) wait()',
' do {',
' step()',
' } while (more)',
'}',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'type', kids: [{ at: 2, kind: 'field' }, { at: 3, kind: 'field' }] },
{ at: 5, kind: 'type' },
{ at: 6, kind: 'loop', kids: [
{ at: 7, kind: 'loop', kids: [{ at: 7, kind: 'statement' }] },
{ at: 8, kind: 'loop', kids: [{ at: 9, kind: 'statement' }] },
] },
])
})
it('函数值的变量 → function;回调参数带语句块主体的表达式语句 → statement 带子块', async () => {
const source = [
'const App = () => {',
' useEffect(() => {',
' load()',
' }, [])',
' let n = 0',
' return null',
'}',
'let total = 0',
'describe(\'x\', () => {',
' it(\'y\', async () => {',
' await run()',
' })',
'})',
].join('\n')
expect(await tree(source, 'typescriptreact')).toEqual([
{ at: 1, kind: 'function', kids: [
{ at: 2, kind: 'statement', kids: [{ at: 3, kind: 'statement' }] },
{ at: 5, kind: 'variable' },
{ at: 6, kind: 'return' },
] },
{ at: 8, kind: 'variable' },
{ at: 9, kind: 'statement', kids: [
{ at: 10, kind: 'statement', kids: [{ at: 11, kind: 'statement' }] },
] },
])
})
it('相邻行的行注释合成一块,空行隔开的分开;行尾注释不单独成块', async () => {
const source = [
'// a',
'// b',
'',
'// c',
'const x = 1 // 行尾',
'foo() // 行尾',
].join('\n')
expect(await tree(source, 'javascript')).toEqual([
{ at: [1, 2], kind: 'comment' },
{ at: 4, kind: 'comment' },
{ at: 5, kind: 'const' },
{ at: 6, kind: 'statement' },
])
})
})
@@ -0,0 +1,328 @@
import type { BlockKind, CodeBlock, LanguageAdapter, Node, TokenRole } from '../contract'
import * as path from 'node:path'
import { compoundBlock, extendHeader, leafBlock, listBlocks, named, sameNode } from './shared'
const GRAMMARS: Record<string, string> = {
typescript: 'tree-sitter-typescript.wasm',
typescriptreact: 'tree-sitter-tsx.wasm',
javascript: 'tree-sitter-javascript.wasm',
javascriptreact: 'tree-sitter-javascript.wasm',
}
const FUNCTION_VALUES = new Set(['arrow_function', 'function_expression', 'function', 'generator_function'])
const LOOPS = new Set(['for_statement', 'for_in_statement', 'while_statement', 'do_statement'])
const CLASSES = new Set(['class_declaration', 'abstract_class_declaration', 'class'])
function isLineComment(comment: Node): boolean {
return comment.text.startsWith('//')
}
/** 带语句块主体的函数值(箭头函数 / 函数表达式)的主体。 */
function functionBody(value: Node | null | undefined): Node | null {
if (!value || !FUNCTION_VALUES.has(value.type))
return null
const body = value.childForFieldName('body')
return body?.type === 'statement_block' ? body : null
}
/** 调用参数里第一个带语句块主体的函数的主体:useEffect(() => { … })、describe('x', () => { … })。 */
function callbackBody(expression: Node | null | undefined): Node | null {
const call = expression?.type === 'await_expression' ? expression.firstNamedChild : expression
if (call?.type !== 'call_expression' && call?.type !== 'new_expression')
return null
for (const arg of named(call.childForFieldName('arguments'))) {
const body = functionBody(arg)
if (body)
return body
}
return null
}
function statements(items: Node[], top: boolean, afterRow: number): CodeBlock[] {
return listBlocks(items, afterRow, item => statementBlocks(item, top), isLineComment)
}
/** 主体是语句块时子块来自它的语句,否则主体本身就是唯一的子语句(`if (x) return`)。 */
function bodyBlock(kind: BlockKind, outer: Node, body: Node | null, top = false): CodeBlock {
if (!body)
return leafBlock(kind, outer)
const items = body.type === 'statement_block' ? named(body) : [body]
return compoundBlock(kind, outer, body.startIndex, items, (list, afterRow) => statements(list, top, afterRow))
}
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
switch (node.type) {
case 'import_statement':
return [leafBlock('import', node)]
case 'export_statement':
return exportBlocks(node, top)
case 'if_statement':
return ifBlocks(node, node, 'if')
case 'try_statement':
return tryBlocks(node)
case 'switch_statement':
return [switchBlock(node)]
case 'switch_case':
case 'switch_default':
return [caseBlock(node)]
case 'return_statement':
return [leafBlock('return', node)]
case 'expression_statement':
return [bodyBlock('statement', node, callbackBody(node.firstNamedChild))]
case 'labeled_statement': {
const [first, ...rest] = statementBlocks(node.childForFieldName('body') ?? node, top)
return first ? [{ ...extendHeader(first, node), node }, ...rest] : []
}
case 'statement_block':
return statements(named(node), top, node.startPosition.row)
case 'empty_statement':
return []
default:
if (LOOPS.has(node.type))
return [bodyBlock('loop', node, node.childForFieldName('body'))]
return declarationBlocks(node, node, top)
}
}
/** export 包着声明时按声明定种类,块节点取整个 export_statement(头部包含 export)。 */
function exportBlocks(node: Node, top: boolean): CodeBlock[] {
const declaration = node.childForFieldName('declaration')
if (declaration)
return declarationBlocks(declaration, node, top)
const value = node.childForFieldName('value')
if (value && CLASSES.has(value.type))
return [classBlock(value, node)]
const body = functionBody(value)
return [body ? bodyBlock('function', node, body) : leafBlock('statement', node)]
}
function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBlock[] {
switch (declaration.type) {
case 'function_declaration':
case 'generator_function_declaration':
return [bodyBlock('function', outer, declaration.childForFieldName('body'))]
case 'function_signature':
return [leafBlock('function', outer)]
case 'lexical_declaration':
case 'variable_declaration':
return [variableBlock(declaration, outer, top)]
case 'interface_declaration':
case 'enum_declaration':
return [membersBlock(declaration, outer)]
case 'type_alias_declaration':
return [leafBlock('type', outer)]
default: {
if (CLASSES.has(declaration.type))
return [classBlock(declaration, outer)]
const body = declaration.childForFieldName('body')
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
}
}
}
/** 唯一声明值是带语句块主体的函数 → function;否则模块顶层的 const → const,其余 → variable。 */
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
const body = functionBody(value)
if (body)
return bodyBlock('function', outer, body)
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
return bodyBlock(top && isConst ? 'const' : 'variable', outer, callbackBody(value))
}
/** if 的 else / else if 展平成紧跟其后的兄弟块。 */
function ifBlocks(ifNode: Node, outer: Node, kind: 'if' | 'else'): CodeBlock[] {
const blocks = [bodyBlock(kind, outer, ifNode.childForFieldName('consequence'))]
const alternative = ifNode.childForFieldName('alternative')
if (alternative) {
const inner = named(alternative).find(child => child.type !== 'comment') ?? null
if (inner?.type === 'if_statement')
blocks.push(...ifBlocks(inner, alternative, 'else'))
else
blocks.push(bodyBlock('else', alternative, inner))
}
return blocks
}
function tryBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('try', node, node.childForFieldName('body'))]
const handler = node.childForFieldName('handler')
if (handler)
blocks.push(bodyBlock('catch', handler, handler.childForFieldName('body')))
const finalizer = node.childForFieldName('finalizer')
if (finalizer)
blocks.push(bodyBlock('finally', finalizer, finalizer.childForFieldName('body')))
return blocks
}
function switchBlock(node: Node): CodeBlock {
const body = node.childForFieldName('body')
if (!body)
return leafBlock('if', node)
return compoundBlock('if', node, body.startIndex, named(body), (list, afterRow) => statements(list, false, afterRow))
}
/** case / default:头部到冒号为止,子块来自它下面的语句。 */
function caseBlock(node: Node): CodeBlock {
const [first] = node.childrenForFieldName('body')
const bodyStart = first?.startIndex ?? node.endIndex
const items = named(node).filter(child => child.startIndex >= bodyStart)
return compoundBlock('if', node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow))
}
function classBlock(declaration: Node, outer: Node): CodeBlock {
const body = declaration.childForFieldName('body')
if (!body)
return leafBlock('class', outer)
return compoundBlock('class', outer, body.startIndex, named(body), (list, afterRow) => {
let decorators: Node | undefined
return listBlocks(list, afterRow, (member) => {
if (member.type === 'decorator') {
decorators ??= member
return []
}
const blocks = memberBlocks(member)
const from = decorators
decorators = undefined
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
}, isLineComment)
})
}
function memberBlocks(member: Node): CodeBlock[] {
switch (member.type) {
case 'method_definition':
return [bodyBlock('function', member, member.childForFieldName('body'))]
case 'method_signature':
case 'abstract_method_signature':
return [leafBlock('function', member)]
case 'class_static_block':
return [bodyBlock('statement', member, member.childForFieldName('body'))]
default: {
const body = functionBody(member.childForFieldName('value'))
return [body ? bodyBlock('function', member, body) : leafBlock('field', member)]
}
}
}
/** interface / enum:成员(属性、方法签名、枚举项)→ field。 */
function membersBlock(declaration: Node, outer: Node): CodeBlock {
const body = declaration.childForFieldName('body')
if (!body)
return leafBlock('type', outer)
return compoundBlock('type', outer, body.startIndex, named(body), (list, afterRow) =>
listBlocks(list, afterRow, member => [leafBlock('field', member)], isLineComment))
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
/** 流程关键字;其余字母关键字都算声明关键字(storage)。 */
const FLOW_KEYWORDS = new Set([
'import',
'from',
'as',
'export',
'default',
'if',
'else',
'for',
'of',
'in',
'while',
'do',
'switch',
'case',
'break',
'continue',
'return',
'throw',
'try',
'catch',
'finally',
'await',
'yield',
])
const LITERAL_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this', 'super'])
const STRING_NODES = new Set(['string', 'template_string', 'regex'])
const FUNCTION_NAMED = new Set(['function_declaration', 'generator_function_declaration', 'function_expression', 'function', 'generator_function', 'method_definition', 'method_signature', 'abstract_method_signature', 'function_signature'])
function insideString(leaf: Node): boolean {
for (let node = leaf.parent; node; node = node.parent) {
if (node.type === 'template_substitution')
return false
if (STRING_NODES.has(node.type))
return true
}
return false
}
function isCallee(node: Node): boolean {
const parent = node.parent
return !!parent && parent.type === 'call_expression' && sameNode(parent.childForFieldName('function'), node)
}
function nameRole(leaf: Node): TokenRole {
const parent = leaf.parent
if (!parent)
return 'variable'
if (FUNCTION_NAMED.has(parent.type) && sameNode(parent.childForFieldName('name'), leaf))
return 'function'
if (isCallee(leaf))
return 'function'
if (parent.type === 'member_expression' && sameNode(parent.childForFieldName('property'), leaf) && isCallee(parent))
return 'function'
if (parent.type === 'new_expression')
return 'type'
if (parent.type === 'variable_declarator' && sameNode(parent.childForFieldName('name'), leaf) && functionBody(parent.childForFieldName('value')))
return 'function'
return 'variable'
}
export function typescriptTokenRole(leaf: Node): TokenRole {
if (leaf.type === 'comment' || leaf.type === 'hash_bang_line')
return 'comment'
if (insideString(leaf))
return 'string'
if (!leaf.isNamed) {
if (leaf.parent?.type === 'predefined_type')
return 'type'
if (/^[a-z]+$/.test(leaf.type))
return FLOW_KEYWORDS.has(leaf.type) ? 'keyword' : 'storage'
return 'plain'
}
if (LITERAL_KEYWORDS.has(leaf.type))
return 'storage'
switch (leaf.type) {
case 'number':
return 'number'
case 'type_identifier':
return 'type'
case 'identifier':
case 'property_identifier':
case 'private_property_identifier':
return nameRole(leaf)
case 'shorthand_property_identifier':
case 'shorthand_property_identifier_pattern':
case 'statement_identifier':
return 'variable'
default:
return 'plain'
}
}
/** TypeScript / TSX / JavaScript / JSX 语言插件。@param wasmDir 语法包 .wasm 所在目录 */
export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
return {
id: 'typescript',
languageIds: Object.keys(GRAMMARS),
grammarPath: (languageId) => {
const file = GRAMMARS[languageId]
if (!file)
throw new Error(`TypeScript 语言插件不支持 ${languageId}`)
return path.join(wasmDir, file)
},
toBlocks: root => statements(named(root), true, -1),
tokenRole: typescriptTokenRole,
}
}