✨ feat(vscode): 代码阅读加枚举卡片,Python enum 库规则和 TypeScript enum 画成同一张表

This commit is contained in:
cruldra committed 2026-09-26 20:10:36 +08:00
1 parent 2251e67ef8
commit de355661f0
24 files changed
+858 -83

No files matched your search

+1 -1
View File
@@ -144,7 +144,7 @@ export function listBlocks(
else {
const blocks = toBlocks(item)
const [head] = blocks
if ((head?.kind === 'function' || head?.kind === 'class') && doc) {
if ((head?.kind === 'function' || head?.kind === 'class' || head?.kind === 'enum') && doc) {
blocks[0] = documented(head, doc)
doc = undefined
}
@@ -155,7 +155,7 @@ describe('typeScript 代码块树', () => {
])
})
it('enum 成员 → field;type 别名 → type;多个循环 → loop', async () => {
it('enum → 枚举块(整条声明是头部,成员不成子块);type 别名 → type;多个循环 → loop', async () => {
const source = [
'enum Color {',
' Red = 1,',
@@ -170,7 +170,7 @@ describe('typeScript 代码块树', () => {
'}',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'type', kids: [{ at: 2, kind: 'field' }, { at: 3, kind: 'field' }] },
{ at: [1, 4], kind: 'enum' },
{ at: 5, kind: 'type' },
{ at: 6, kind: 'loop', kids: [
{ at: 7, kind: 'loop', kids: [{ at: 7, kind: 'statement' }] },
@@ -561,3 +561,54 @@ describe('typeScript 类', () => {
expect(tsx[2]).toMatchObject({ abstract: false })
})
})
describe('typeScript 枚举', () => {
it('enums.ts:export / const / declare / 函数里的 enum 都是枚举块,紧贴的 JSDoc 算进头部', async () => {
expect(await tree(readFixture('enums.ts.txt'))).toEqual([
{ at: [1, 8], kind: 'enum' },
{ at: 10, kind: 'enum' },
{ at: 12, kind: 'enum' },
{ at: 14, kind: 'enum' },
{ at: 16, kind: 'enum' },
{ at: 18, kind: 'function', kids: [{ at: 19, kind: 'enum' }] },
])
})
it('成员:写的值、自增算出来的值(接不上数字就算不出来)、说明(前面单独成行的注释和 JSDoc、行尾注释);export 的公共、顶层其余私有、嵌套的没有', async () => {
const byLine = await facts(readFixture('enums.ts.txt'))
expect(byLine[1]).toEqual({
name: 'OrderStatus',
doc: '/** 订单状态。 */',
access: 'public',
decorators: [],
bases: [],
members: [
{ name: 'Pending', value: '\'pending\'', docs: ['/** 待支付 */'] },
{ name: 'Paid', value: '\'paid\'', docs: ['// 已支付'] },
{ name: 'Cancelled', value: '\'cancelled\'', docs: ['// 已取消'] },
],
})
expect(byLine[10]).toMatchObject({
access: 'private',
members: [
{ name: 'Up', computed: 0, docs: [] },
{ name: 'Down', value: '5', docs: [] },
{ name: 'Left', computed: 6, docs: [] },
{ name: 'Right', value: '-1', docs: [] },
{ name: 'Next', computed: 0, docs: [] },
{ name: 'Mask', value: '1 << 2', docs: [] },
{ name: 'After', docs: [] },
],
})
expect((byLine[10] as { members: object[] }).members[6]).toEqual({ name: 'After', docs: [] })
expect(byLine[12]).toMatchObject({ name: 'Flag', access: 'public', members: [{ name: 'A', value: '0x10' }, { name: 'B', computed: 17 }] })
expect(byLine[14]).toMatchObject({ name: 'Ambient', access: 'private', members: [{ name: 'X', computed: 0 }] })
expect(byLine[16]).toMatchObject({ name: 'Both', access: 'public', members: [{ name: 'Y', value: '1' }] })
expect(byLine[19]).toEqual({ name: 'Inner', decorators: [], bases: [], members: [{ name: 'Z', computed: 0, docs: [] }] })
})
it('jSDoc 挂在同一行上一个成员后面时讲的是下一个成员;最后一个成员后面的注释不算', async () => {
const byLine = await facts('enum E { A, /** 第二 */ B, C // 第三\n // 收尾\n}')
expect(byLine[1]).toMatchObject({ members: [{ name: 'A', docs: [] }, { name: 'B', docs: ['/** 第二 */'] }, { name: 'C', docs: ['// 第三'] }] })
})
})
+64 -3
View File
@@ -1,4 +1,4 @@
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren } from './shared'
@@ -51,6 +51,49 @@ function declarationAccess(outer: Node, top: boolean): Access | undefined {
return top ? 'private' : undefined
}
/** 数字字面量(可带正负号)的值;别的写法算不出来。 */
function numberValue(node: Node): number | undefined {
const sign = node.type === 'unary_expression' ? node.child(0)?.type : undefined
const literal = sign ? node.childForFieldName('argument') : node
if (literal?.type !== 'number' || (sign && sign !== '-' && sign !== '+'))
return undefined
const value = Number(literal.text.replace(/_/g, ''))
return Number.isFinite(value) ? (sign === '-' ? -value : value) : undefined
}
/**
* enum 的成员,照源码顺序:没写值的成员接着上一个数字往上加(第一个是 0),上一个的值不是数字字面量、也不是算出来的数就算不出来。
* 说明:从上一个成员之后起单独成行的注释、写在它这一行行尾的注释(JSDoc 总是讲下一个成员)。
*/
function enumMembers(body: Node | null): EnumMemberFacts[] {
const members: EnumMemberFacts[] = []
let pending: Node[] = []
let lastRow = -1
let next: number | undefined = 0
for (const child of named(body)) {
if (child.type === 'comment') {
const last = members.at(-1)
if (last && pending.length === 0 && child.startPosition.row === lastRow && !COMMENTS.isDoc(child))
last.docs.push(child)
else
pending.push(child)
continue
}
const assigned = child.type === 'enum_assignment'
const name = assigned ? child.childForFieldName('name') ?? child.firstNamedChild : child
const value = assigned ? child.childForFieldName('value') ?? undefined : undefined
if (!name)
continue
const computed: number | undefined = value ? undefined : next
const base: number | undefined = value ? numberValue(value) : computed
next = base === undefined ? undefined : base + 1
members.push({ name, ...(value && { value }), ...(computed !== undefined && { computed }), docs: pending })
pending = []
lastRow = child.endPosition.row
}
return members
}
/** 类成员:写了 public / private / protected 按写的,#name 是私有,默认公共。 */
function memberAccess(member: Node): Access {
const modifier = named(member).find(child => child.type === 'accessibility_modifier')
@@ -274,8 +317,14 @@ function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
case 'variable_declaration':
return [variableBlock(declaration, outer, top)]
case 'interface_declaration':
case 'enum_declaration':
return [membersBlock(declaration, outer)]
case 'enum_declaration':
return [enumBlock(declaration, outer, top)]
case 'ambient_declaration': {
// declare enum X {}:别的 declare 写法照旧是一条语句
const inner = named(declaration).find(child => child.type === 'enum_declaration')
return [inner ? enumBlock(inner, outer, top) : leafBlock('statement', outer)]
}
case 'type_alias_declaration':
return [leafBlock('type', outer)]
default: {
@@ -399,7 +448,19 @@ function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
}
}
/** interface / enum:成员(属性、方法签名、枚举项)→ field。 */
/** enum(含 const enum、declare enum):整条声明是头部,成员都在属性表里讲,没有子块。 */
function enumBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
const name = declaration.childForFieldName('name')
if (!name)
return leafBlock('type', outer)
return {
kind: 'enum',
...leafShape(outer),
facts: { name, access: declarationAccess(outer, top), decorators: [], bases: [], members: enumMembers(declaration.childForFieldName('body')) },
}
}
/** interface:成员(属性、方法签名)→ field。 */
function membersBlock(declaration: Node, outer: Node): CodeBlock {
const body = declaration.childForFieldName('body')
if (!body)