✨ feat(vscode): 代码阅读预览顶部加块筛选栏

语言插件按和语言无关的 id 报块属性(通用的由宿主从 facts 推),翻译插件起名,webview 只按 id 筛;
筛选整个工作区共用、记在 workspaceState;JavaScript 不再显示「抽象:否」「仅类型:否」
This commit is contained in:
cruldra committed 2026-09-25 21:31:19 +08:00
1 parent 4eaf2ae11d
commit 1c7e31c51a
29 files changed
+1364 -319

No files matched your search

@@ -532,4 +532,15 @@ describe('typeScript 类', () => {
expect(js[1]).toMatchObject({ name: 'A', bases: [{ value: 'mixin(B)' }], implements: [] })
expect(js[2]).toMatchObject({ access: 'public', bases: [{ value: 'C' }] })
})
it('javaScript / JSX 没有抽象类、仅类型导入:facts 里不填 abstract、typeOnly(不是填 false)', async () => {
for (const languageId of ['javascript', 'javascriptreact']) {
const js = await facts('import { a } from \'a\'\nclass K {}', languageId)
expect(js[1]).toEqual({ sources: ['\'a\''], items: [{ kind: 'named', name: 'a' }] })
expect(js[2]).toEqual({ name: 'K', access: 'private', decorators: [], typeParameters: [], bases: [], implements: [] })
}
const tsx = await facts('import { a } from \'a\'\nclass K {}', 'typescriptreact')
expect(tsx[1]).toMatchObject({ typeOnly: false })
expect(tsx[2]).toMatchObject({ abstract: false })
})
})
+224 -211
View File
@@ -10,6 +10,9 @@ const GRAMMARS: Record<string, string> = {
javascriptreact: 'tree-sitter-javascript.wasm',
}
/** 分得出抽象类、仅类型导入的 languageId;JavaScript / JSX 没有这两个概念。 */
const TYPED_LANGUAGES = new Set(['typescript', 'typescriptreact'])
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'])
@@ -41,118 +44,6 @@ function callbackBody(expression: Node | null | undefined): Node | null {
return null
}
function statements(items: Node[], top: boolean, afterRow: number): CodeBlock[] {
return listBlocks(items, afterRow, item => statementBlocks(item, top), COMMENTS)
}
/** 主体是语句块时子块来自它的语句,否则主体本身就是唯一的子语句(`if (x) return`)。 */
function bodyShape(outer: Node, body: Node | null): BlockShape {
if (!body)
return leafShape(outer)
const items = body.type === 'statement_block' ? named(body) : [body]
return compoundShape(outer, body.startIndex, items, (list, afterRow) => statements(list, false, afterRow))
}
function bodyBlock(kind: TextKind, outer: Node, body: Node | null): CodeBlock {
return { kind, ...bodyShape(outer, body) }
}
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
switch (node.type) {
case 'import_statement':
return [{ kind: 'import', ...leafShape(node), facts: importFacts(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(node, value, value.childForFieldName('name'), 'public')]
if (value && functionBody(value))
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
return [leafBlock('statement', node)]
}
function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBlock[] {
switch (declaration.type) {
case 'function_declaration':
case 'generator_function_declaration':
case 'function_signature':
return [functionBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
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(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
const body = declaration.childForFieldName('body')
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
}
}
}
/** 唯一声明值是带语句块主体的函数 → function,是类表达式 → class;否则模块顶层的 const → const,其余 → variable。 */
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
const access = declarationAccess(outer, top)
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
if (value && CLASSES.has(value.type))
return classBlock(outer, value, declarators[0].childForFieldName('name'), access)
if (value && functionBody(value))
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
return {
kind: top && isConst ? 'const' : 'variable',
...bodyShape(outer, callbackBody(value)),
facts: { access, declarators: declarators.map(declaratorFacts) },
}
}
/** 函数块:fn 是声明、方法或函数值,带语句块主体时子块来自主体;name 是它在源码里的名字(变量名、方法名)。 */
function functionBlock(outer: Node, fn: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
return {
kind: 'function',
...bodyShape(outer, fn.childForFieldName('body')),
facts: functionFacts(fn, name, access, decorators),
}
}
/** 模块里的声明:export 了是公共,顶层没 export 是私有,嵌套在语句块里的没有访问级别。 */
function declarationAccess(outer: Node, top: boolean): Access | undefined {
if (outer.type === 'export_statement')
@@ -242,8 +133,8 @@ function objectEntries(value: Node | null): EntryFacts[] | undefined {
})
}
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;`import type` 只导入类型。 */
function importFacts(node: Node): ImportFacts {
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;TypeScript(typed)的 `import type` 只导入类型。 */
function importFacts(node: Node, typed: boolean): ImportFacts {
const require = named(node).find(child => child.type === 'import_require_clause')
const source = (require ?? node).childForFieldName('source')
const items: ImportItem[] = []
@@ -265,87 +156,22 @@ function importFacts(node: Node): ImportFacts {
}
}
}
return { sources: source ? [source] : [], typeOnly: children(node).some(child => child.type === 'type'), items }
}
/** 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 { kind: 'if', ...compoundShape(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 { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
}
/**
* 类块:declaration 是类声明或类表达式,name 是它在源码里的名字(类名;赋给变量、类字段的类表达式取变量名、字段名),
* decorators 是类主体里排在它前面的装饰器(类字段的值是类表达式时)。
*/
function classBlock(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
const body = declaration.childForFieldName('body')
const facts = classFacts(outer, declaration, name, access, decorators)
if (!body)
return { kind: 'class', ...leafShape(outer), facts }
return { kind: 'class', ...compoundShape(outer, body.startIndex, named(body), classMembers), facts }
}
/** 类成员:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
function classMembers(list: Node[], afterRow: number): CodeBlock[] {
let decorators: Node[] = []
return listBlocks(list, afterRow, (member) => {
if (member.type === 'decorator') {
decorators.push(member)
return []
}
const blocks = memberBlocks(member, decorators)
const [from] = decorators
decorators = []
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
}, COMMENTS)
const sources = source ? [source] : []
return typed ? { sources, typeOnly: children(node).some(child => child.type === 'type'), items } : { sources, items }
}
/**
* 类的装饰器写在 export 前面时挂在 export 语句上,写在 class 前面时挂在类声明上;
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式。
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式;
* 只有 TypeScript(typed)分抽象类。
*/
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[]): ClassFacts {
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[], typed: boolean): ClassFacts {
const holders = sameNode(outer, declaration) ? [declaration] : [outer, declaration]
const heritage = syntaxChildren(named(declaration).find(child => child.type === 'class_heritage'))
return {
name: name ?? undefined,
access,
abstract: declaration.type === 'abstract_class_declaration',
...(typed && { abstract: declaration.type === 'abstract_class_declaration' }),
decorators: [...decorators, ...holders.flatMap(holder => named(holder).filter(child => child.type === 'decorator'))],
typeParameters: syntaxChildren(declaration.childForFieldName('type_parameters')),
bases: heritage.flatMap((part) => {
@@ -370,35 +196,222 @@ function extendsBases(clause: Node): BaseFacts[] {
return bases
}
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
switch (member.type) {
case 'method_definition':
case 'method_signature':
case 'abstract_method_signature':
return [functionBlock(member, member, member.childForFieldName('name'), memberAccess(member), decorators)]
case 'class_static_block':
return [bodyBlock('statement', member, member.childForFieldName('body'))]
default: {
const value = member.childForFieldName('value')
if (value && CLASSES.has(value.type))
return [classBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
if (value && functionBody(value))
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
return [leafBlock('field', member)]
/**
* 读代码块树的一套函数。typed:TypeScript(typescript、typescriptreact),分得出抽象类、仅类型导入;
* JavaScript 没有这两个概念,facts 里不填(见 classFacts、importFacts)。
*/
function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
function statements(items: Node[], top: boolean, afterRow: number): CodeBlock[] {
return listBlocks(items, afterRow, item => statementBlocks(item, top), COMMENTS)
}
/** 主体是语句块时子块来自它的语句,否则主体本身就是唯一的子语句(`if (x) return`)。 */
function bodyShape(outer: Node, body: Node | null): BlockShape {
if (!body)
return leafShape(outer)
const items = body.type === 'statement_block' ? named(body) : [body]
return compoundShape(outer, body.startIndex, items, (list, afterRow) => statements(list, false, afterRow))
}
function bodyBlock(kind: TextKind, outer: Node, body: Node | null): CodeBlock {
return { kind, ...bodyShape(outer, body) }
}
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
switch (node.type) {
case 'import_statement':
return [{ kind: 'import', ...leafShape(node), facts: importFacts(node, typed) }]
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)
}
}
}
/** interface / enum:成员(属性、方法签名、枚举项)→ field。 */
function membersBlock(declaration: Node, outer: Node): CodeBlock {
const body = declaration.childForFieldName('body')
if (!body)
return leafBlock('type', outer)
return {
kind: 'type',
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) =>
listBlocks(list, afterRow, member => [leafBlock('field', member)], COMMENTS)),
/** 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(node, value, value.childForFieldName('name'), 'public')]
if (value && functionBody(value))
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
return [leafBlock('statement', node)]
}
function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBlock[] {
switch (declaration.type) {
case 'function_declaration':
case 'generator_function_declaration':
case 'function_signature':
return [functionBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
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(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
const body = declaration.childForFieldName('body')
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
}
}
}
/** 唯一声明值是带语句块主体的函数 → function,是类表达式 → class;否则模块顶层的 const → const,其余 → variable。 */
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
const access = declarationAccess(outer, top)
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
if (value && CLASSES.has(value.type))
return classBlock(outer, value, declarators[0].childForFieldName('name'), access)
if (value && functionBody(value))
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
return {
kind: top && isConst ? 'const' : 'variable',
...bodyShape(outer, callbackBody(value)),
facts: { access, declarators: declarators.map(declaratorFacts) },
}
}
/** 函数块:fn 是声明、方法或函数值,带语句块主体时子块来自主体;name 是它在源码里的名字(变量名、方法名)。 */
function functionBlock(outer: Node, fn: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
return {
kind: 'function',
...bodyShape(outer, fn.childForFieldName('body')),
facts: functionFacts(fn, name, access, decorators),
}
}
/** 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 { kind: 'if', ...compoundShape(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 { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
}
/**
* 类块:declaration 是类声明或类表达式,name 是它在源码里的名字(类名;赋给变量、类字段的类表达式取变量名、字段名),
* decorators 是类主体里排在它前面的装饰器(类字段的值是类表达式时)。
*/
function classBlock(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
const body = declaration.childForFieldName('body')
const facts = classFacts(outer, declaration, name, access, decorators, typed)
if (!body)
return { kind: 'class', ...leafShape(outer), facts }
return { kind: 'class', ...compoundShape(outer, body.startIndex, named(body), classMembers), facts }
}
/** 类成员:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
function classMembers(list: Node[], afterRow: number): CodeBlock[] {
let decorators: Node[] = []
return listBlocks(list, afterRow, (member) => {
if (member.type === 'decorator') {
decorators.push(member)
return []
}
const blocks = memberBlocks(member, decorators)
const [from] = decorators
decorators = []
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
}, COMMENTS)
}
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
switch (member.type) {
case 'method_definition':
case 'method_signature':
case 'abstract_method_signature':
return [functionBlock(member, member, member.childForFieldName('name'), memberAccess(member), decorators)]
case 'class_static_block':
return [bodyBlock('statement', member, member.childForFieldName('body'))]
default: {
const value = member.childForFieldName('value')
if (value && CLASSES.has(value.type))
return [classBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
if (value && functionBody(value))
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
return [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 {
kind: 'type',
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) =>
listBlocks(list, afterRow, member => [leafBlock('field', member)], COMMENTS)),
}
}
return root => statements(named(root), true, -1)
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
@@ -509,7 +522,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
throw new Error(`TypeScript 语言插件不支持 ${languageId}`)
return path.join(wasmDir, file)
},
toBlocks: root => statements(named(root), true, -1),
toBlocks: (root, _source, languageId) => blockReader(TYPED_LANGUAGES.has(languageId))(root),
tokenRole: typescriptTokenRole,
}
}