✨ 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

+3 -2
View File
@@ -26,11 +26,12 @@ function factAttributes(block: CodeBlock): Partial<Record<CommonAttribute, strin
return { access: block.facts.access, async: flag(block.facts.async) }
case 'class':
return { access: block.facts.access, abstract: flag(block.facts.abstract) }
case 'import':
return { typeOnly: flag(block.facts.typeOnly) }
case 'enum':
case 'const':
case 'variable':
return { access: block.facts.access }
case 'import':
return { typeOnly: flag(block.facts.typeOnly) }
default:
return {}
}
+30 -22
View File
@@ -7,7 +7,7 @@ import { activeRules, claimBlock, claimedShape, importsIn } from './libraryRules
interface BlockViewBase {
/** 在树里的位置,例如 '6.1.0'。 */
id: string
/** BlockKind,或库规则认领的块的种类 id(RuleBlock.kind,带点,例如 sqlalchemy.table)。 */
/** BlockKind,或库规则卡片的种类 id(RuleCard.kind,带点,例如 sqlalchemy.table)。 */
kind: string
/**
* 编辑范围的起止行,0 起:一般是 header;库规则认领的块是原块整条语句(见 claimedShape),盖住它留下的子块的行。
@@ -23,13 +23,13 @@ interface BlockViewBase {
defines?: SourceRange
/** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */
attributes: BlockAttributes
/** 库规则认领的块才有:种类标签的文字和颜色(RuleBlock.badge)。预览同类块一样默认收起它的子块。 */
/** 库规则的卡片才有:种类标签的文字和颜色(RuleCard.badge)。预览同类块一样默认收起它的子块。 */
badge?: KindBadge
children: BlockView[]
}
/**
* 发给 webview 的一块(可序列化):函数、类、导入、常量、变量块和库规则认领的块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
* 发给 webview 的一块(可序列化):函数、类、枚举、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
* 一行译文的块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文,预览排在译文下面。
*/
export type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
@@ -103,6 +103,7 @@ function ownName(block: CodeBlock): Node | undefined {
switch (block.kind) {
case 'function':
case 'class':
case 'enum':
return block.facts.name
case 'const':
case 'variable':
@@ -122,7 +123,8 @@ const NO_RULES: RuleRun = { rules: [], report: () => {} }
/**
* 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock),并进去的子块不再单独成块。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
* 并进去的子块不再单独成块。
*/
export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined, rules: RuleRun = NO_RULES): BlockView[] {
const lines = lineRanges(source)
@@ -153,34 +155,40 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
const doc = block.doc && translator?.doc(block.doc, document)
return { translation: translator ? translator.translate(block, document) : [], ...(doc && { doc }) }
}
const build = (block: CodeBlock, id: string): BlockView => {
const root = block.node.tree.rootNode
const claim = active.length > 0 ? claimBlock(block, active, context, rules.report) : undefined
if (claim) {
const { kind, badge, rows, defines, attributes } = claim.block
const { span, kept } = claimedShape(block, claim.block.absorbs)
return {
id,
kind,
...sourceRows(root, span.startRow, span.endRow),
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
badge,
children: kept.map((child, i) => build(child, `${id}.${i}`)),
properties: rows,
}
}
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):头部是编辑范围,子块照常交给各规则看。 */
function plain(block: CodeBlock, id: string): BlockView {
const name = ownName(block)
return {
id,
kind: block.kind,
...sourceRows(root, block.header.startRow, block.header.endRow),
...sourceRows(block.node.tree.rootNode, block.header.startRow, block.header.endRow),
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
attributes: blockAttributes(block),
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
...('facts' in block ? { properties: translator ? translator.describe(block, document) : [] } : textView(block)),
}
}
function build(block: CodeBlock, id: string): BlockView {
const claim = active.length > 0 ? claimBlock(block, active, context, rules.report) : undefined
if (!claim)
return plain(block, id)
const { span, kept } = claimedShape(block, claim.block.absorbs)
if ('as' in claim.block) {
const { attributes } = block
return plain({ ...claim.block.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, id)
}
const { kind, badge, rows, defines, attributes } = claim.block
return {
id,
kind,
...sourceRows(block.node.tree.rootNode, span.startRow, span.endRow),
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
badge,
children: kept.map((child, i) => build(child, `${id}.${i}`)),
properties: rows,
}
}
return blocks.map((block, i) => build(block, String(i)))
}
+60 -9
View File
@@ -4,7 +4,7 @@ import type { Disposable } from 'vscode'
export type { Node }
export type BlockKind
= | 'import' | 'function' | 'class' | 'type' | 'field' | 'const' | 'variable'
= | 'import' | 'function' | 'class' | 'enum' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
/** 源码里一段的下标:源码字符串下标(web-tree-sitter 的 startIndex / endIndex),end 为开区间。 */
@@ -70,6 +70,37 @@ export interface ClassFacts {
implements: Node[]
}
/** 枚举的一个成员;节点都指向源码。 */
export interface EnumMemberFacts {
name: Node
/** 源码里写的值;没写(TypeScript 的自增成员)、或值交给库自动取(Python 的 `auto()`)时没有。 */
value?: Node
/** 没写值、语言算得出来时的值(TypeScript 数值枚举自增:0、1、2……);算不出来时没有。 */
computed?: number
/**
* 成员的说明文字节点(注释、JSDoc、属性 docstring),照源码顺序:紧挨在它上面(从上一个成员之后起)单独成行的、
* 写在它这一行行尾的、Python 紧跟在它后面的属性 docstring。翻译插件逐个取原文连起来。
*/
docs: Node[]
}
/**
* 枚举:TypeScript 的 `enum` 由语言插件报;Python 没有枚举语法,自带 enum 库的写法(`class X(enum.Enum)`)由库规则认成这种块。
* 两边同一种块、同一张属性表。
*/
export interface EnumFacts {
name: Node
/** 枚举自己的说明,同 ClassFacts.doc。 */
doc?: Node
/** 同 ClassFacts.access。 */
access?: Access
decorators: Node[]
/** 照源码写的父类(Python 的 `str, enum.Enum`);TypeScript 没有。 */
bases: BaseFacts[]
/** 照源码顺序。 */
members: EnumMemberFacts[]
}
/** 导入的一项。 */
export type ImportItem
= | { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
@@ -145,7 +176,7 @@ export interface VariableFacts {
* - kind:块的种类(BlockKind);
* - access:访问级别,'public' | 'private' | 'protected',语言可以加自己的值(Kotlin 的 'internal',见 BlockShape.attributes);
* - async、abstract、typeOnly:异步、抽象、仅类型,值是 'true' | 'false'。
* 宿主从块的 kind 和 facts 推出它们(FunctionFacts.access / async、ClassFacts.access / abstract、ImportFacts.typeOnly、VariableFacts.access),
* 宿主从块的 kind 和 facts 推出它们(FunctionFacts.access / async、ClassFacts.access / abstract、EnumFacts.access、ImportFacts.typeOnly、VariableFacts.access),
* 语言插件不用另报;facts 里没填的(语言没有这个概念、块没有这一项),块就没有这个属性,也不受这个属性的筛选影响。
*/
export const COMMON_ATTRIBUTES = ['kind', 'access', 'async', 'abstract', 'typeOnly'] as const
@@ -174,12 +205,17 @@ export interface BlockShape {
/** 预览成属性表的块:语言插件从语法树读出结构化信息(facts),翻译插件把它排成属性表。 */
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
export type FactBlock = FunctionBlock | ClassBlock | ImportBlock | VariableBlock
export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ImportBlock | VariableBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | VariableBlock
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | VariableBlock
/** 属性表块的种类和 facts,不带块的形状(节点、头部、子块):库规则把一块认成现成的一种属性表块时给它(RuleFacts)。 */
export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick<B, 'kind' | 'facts'> : never : never
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
@@ -301,7 +337,7 @@ export interface Translator {
displayLanguage: string
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
translate: (block: TextBlock, document: SourceDocument) => PreviewText
/** 函数、类、导入、常量、变量块 → 属性表。 */
/** 函数、类、枚举、导入、常量、变量块 → 属性表。 */
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
/** 一个表达式或类型节点整个翻成一行译文,译法同属性表里的值:库规则翻默认值、类型时借它。 */
expression: (node: Node, document: SourceDocument) => Inline[]
@@ -318,9 +354,13 @@ export interface ThemeColor { dark: string, light: string }
export interface KindBadge { label: string, color: ThemeColor }
/**
* 库规则认领一块后给出的替身:预览里它代替原来那块,是一张属性表(种类标签、每一行都由规则给),下面挂着原块留下的子块。
* 库规则认领一块后给出的替身,预览里它代替原来那块,下面挂着原块留下的子块:
* 规则自己的卡片(RuleCard),或认成契约里现成的一种属性表块(RuleFacts)。
*/
export interface RuleBlock {
export type RuleBlock = RuleCard | RuleFacts
/** 规则自己的卡片:一张属性表,种类标签、每一行都由规则给(用词归规则)。 */
export interface RuleCard {
/** 种类 id,写成「规则 id.名字」(sqlalchemy.table):筛选的 kind 属性值就是它,筛选记住的也是它,要稳定。 */
kind: string
/** 种类标签的文字和颜色:筛选栏里这个种类也叫这个名字。 */
@@ -338,6 +378,16 @@ export interface RuleBlock {
absorbs: CodeBlock[]
}
/**
* 把这块认成契约里现成的一种属性表块(Python 自带 enum 库的类 → 枚举块):宿主当语言插件报的这种块处理,
* 种类、种类名、颜色、属性表、筛选属性都和语言自带的这种块一样,由翻译插件排;规则只给 facts(库的知识),不给用词。
* 块的头部是原块整条语句(facts 里的节点都在里面),absorbs 同 RuleCard.absorbs。
*/
export interface RuleFacts {
as: BlockFacts
absorbs: CodeBlock[]
}
/** 库规则看一块时宿主给的:这份源码、它的导入,以及借当前翻译插件翻表达式、取说明的办法。 */
export interface RuleContext {
document: SourceDocument
@@ -396,8 +446,9 @@ export interface RuleWorkspace {
}
/**
* 库规则:认出某个库的一种写法(SQLAlchemy 的模型类),把那块整个换成一张专门的卡片;文件导入了它关心的库才启用。
* 规则只经契约拿信息:块(kind、node、header、children、facts)和 RuleContext;用词(属性名、种类名、类型的译名)属于规则自己。
* 库规则:认出某个库的一种写法(SQLAlchemy 的模型类),把那块整个换成一张专门的卡片,或认成现成的一种属性表块
* (enum 库的类 → 枚举块);文件导入了它关心的库才启用。
* 规则只经契约拿信息:块(kind、node、header、children、facts)和 RuleContext;卡片的用词(属性名、种类名、类型的译名)属于规则自己。
*/
export interface LibraryRule {
id: string // 'sqlalchemy'
@@ -0,0 +1,38 @@
# luffy-agent-platform 的 backend/src/luffy_agent/referral/models.py、vault/models.py 节选:enum 库规则的样例
from __future__ import annotations
import enum
from sqlalchemy import Enum as SQLEnum
class ReferralStatus(str, enum.Enum):
NORMAL = "normal"
FROZEN = "frozen"
class RelationStatus(str, enum.Enum):
ACTIVE = "active"
INVALID = "invalid"
class CommissionStatus(str, enum.Enum):
PENDING = "pending" # 待结算
AVAILABLE = "available" # 可提现
WITHDRAWING = "withdrawing" # 提现中
WITHDRAWN = "withdrawn" # 已提现
INVALID = "invalid" # 已失效
class SecretScope(enum.StrEnum):
"""条目作用域: user 全局 / app 级 (同名覆盖 user)."""
USER = "user"
APP = "app"
class SecretKind(enum.StrEnum):
"""条目类型: secret 只写不读; variable 可读回明文."""
SECRET = "secret"
VARIABLE = "variable"
@@ -0,0 +1,20 @@
/** 订单状态。 */
export enum OrderStatus {
/** 待支付 */
Pending = 'pending',
Paid = 'paid', // 已支付
// 已取消
Cancelled = 'cancelled',
}
enum Direction { Up, Down = 5, Left, Right = -1, Next, Mask = 1 << 2, After }
export const enum Flag { A = 0x10, B }
declare enum Ambient { X }
export declare const enum Both { Y = 1 }
function local(): void {
enum Inner { Z }
}
+2
View File
@@ -13,6 +13,7 @@ import { registerNoteCommands } from './notes/commands'
import { FileNotes } from './notes/fileNotes'
import { CodeReaderRegistry } from './registry'
import { celeryRule } from './rules/celery'
import { enumRule } from './rules/enum'
import { sqlalchemyRule } from './rules/sqlalchemy'
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
import { pythonZhTranslator } from './translators/pythonZh'
@@ -91,6 +92,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
api.registerTranslator(pythonZhTranslator),
api.registerLibraryRule(sqlalchemyRule),
api.registerLibraryRule(celeryRule),
api.registerLibraryRule(enumRule),
)
return api
}
+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)
+22 -1
View File
@@ -6,7 +6,7 @@ import { toBlockViews } from './blockView'
import { replaceLines } from './editText'
import { createPythonAdapter } from './languages/python'
import { activeRules, fromLibrary, importsIn, resolveReference } from './libraryRules'
import { WASM_DIR, withBlocks } from './testSupport'
import { propertyLines, WASM_DIR, withBlocks } from './testSupport'
import { pythonZhTranslator } from './translators/pythonZh'
const py = createPythonAdapter(WASM_DIR)
@@ -177,6 +177,27 @@ describe('库规则:认领', () => {
])
})
it('认成契约里现成的属性表块:种类、属性表、筛选属性照语言插件报的这种块,编辑范围是整个类;并进来的不是子块时照常显示', async () => {
const asEnum = (absorbs: (block: CodeBlock) => CodeBlock[]): LibraryRule => ({
...cardRule('lib'),
claim: block => block.kind === 'class' && block.facts.name
? { as: { kind: 'enum', facts: { name: block.facts.name, access: block.facts.access, decorators: [], bases: [], members: [] } }, absorbs: absorbs(block) }
: undefined,
})
const [, recognized] = await views(MODEL, [asEnum(block => block.children.slice(0, 1))])
expect(recognized).toMatchObject({ kind: 'enum', startLine: 2, endLine: 8, attributes: { kind: 'enum', access: 'public' } })
expect(recognized.badge).toBeUndefined()
expect(propertyLines('properties' in recognized ? recognized.properties : [])).toEqual(['名称: User', '访问级别: 公共', '成员: 无'])
expect(recognized.children.map(child => child.kind)).toEqual(['comment', 'variable', 'function'])
const reports: string[] = []
const stranger: CodeBlock[] = []
await withBlocks(py, 'python', 'x = 1', blocks => stranger.push(...blocks))
const [, shown] = await views(MODEL, [asEnum(() => stranger)], reports)
expect(shown.kind).toBe('class')
expect(reports).toEqual(['库规则 lib 给第 3 行的 class 块的替身不合约定,这块照常显示:要并进来的块不是这块的子块'])
})
it('筛选:规则的种类和别的种类一样计数,名字取规则给的标签', async () => {
const list = await views(MODEL, [cardRule('orm')])
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values).toEqual([
+4 -2
View File
@@ -40,10 +40,12 @@ export interface Claim {
/** 替身不合 RuleBlock 的约定时说哪儿不对。 */
function problemOf(rule: LibraryRule, block: CodeBlock, claimed: RuleBlock): string | undefined {
if (!claimed.kind.startsWith(`${rule.id}.`))
return `种类 id「${claimed.kind}」没以「${rule.id}.」开头`
if (claimed.absorbs.some(child => !block.children.includes(child)))
return '要并进来的块不是这块的子块'
if ('as' in claimed)
return undefined
if (!claimed.kind.startsWith(`${rule.id}.`))
return `种类 id「${claimed.kind}」没以「${rule.id}.」开头`
for (const { reference, offset } of referencedSegments(claimed.rows)) {
if (reference?.rule !== rule.id)
return `引用「${reference?.key}」标的规则是「${reference?.rule}」,不是自己`
+3 -3
View File
@@ -1,4 +1,4 @@
import type { IndexedFile, IndexEntry, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { IndexedFile, IndexEntry, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { Call } from './pythonTree'
import { aliasesOf, callOf, lastName, stringContent, stringValue, syntaxChildren } from './pythonTree'
import { hint, joined, name, plain, range, word } from './segments'
@@ -482,7 +482,7 @@ function appOfConf(node: Node | null | undefined, attribute: string): Node | und
}
/** 定时任务卡片:应用 / 说明 / (conf.update 里别的配置各一行)/ 任务表。 */
function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: Node | undefined, env: Env): RuleBlock {
function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: Node | undefined, env: Env): RuleCard {
const { words, context } = env
const { labels, heads } = words
const entries = entriesOf(schedule)
@@ -506,7 +506,7 @@ function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: N
// ─── Celery 应用 ──────────────────────────────────────────────────────────────
/** Celery 应用卡片:名称 / 说明 / 应用名 / 消息队列 / 结果存储,别的参数照写的顺序各一行,行名是参数名。 */
function appBlock(target: Node, call: Call, doc: Node | undefined, env: Env): RuleBlock {
function appBlock(target: Node, call: Call, doc: Node | undefined, env: Env): RuleCard {
const { words, context } = env
const { labels } = words
const parameters = new Map<string, Node>()
@@ -12,7 +12,7 @@ describe('库规则只经 contract 拿信息', () => {
const files = readdirSync(__dirname).filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts'))
it('目录里有规则', () => {
expect(files).toEqual(expect.arrayContaining(['sqlalchemy.ts', 'celery.ts']))
expect(files).toEqual(expect.arrayContaining(['sqlalchemy.ts', 'celery.ts', 'enum.ts']))
})
it.each(files)('%s 不 import 语言插件、翻译插件和宿主', (file) => {
+260
View File
@@ -0,0 +1,260 @@
import type { BlockView } from '../blockView'
import type { Inline, PropertyRow, Segment } from '../contract'
import { describe, expect, it } from 'vitest'
import { fileAttributes } from '../attributes'
import { toBlockViews } from '../blockView'
import { replaceLines } from '../editText'
import { createPythonAdapter } from '../languages/python'
import { createTypeScriptAdapter } from '../languages/typescript'
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { typescriptZhTranslator } from '../translators/typescriptZh'
import { celeryRule } from './celery'
import { enumRule } from './enum'
import { sqlalchemyRule } from './sqlalchemy'
const py = createPythonAdapter(WASM_DIR)
const ts = createTypeScriptAdapter(WASM_DIR)
const SNAPSHOT = readFixture('enumModels.py')
/** 和扩展里注册的顺序一样。 */
const RULES = [sqlalchemyRule, celeryRule, enumRule]
async function views(source: string, reports: string[] = []): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, source, py, pythonZhTranslator, { rules: RULES, report: message => reports.push(message) }))
}
function tsViews(source: string): Promise<BlockView[]> {
return withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, source, ts, typescriptZhTranslator, { rules: RULES, report: () => {} }))
}
function properties(view: BlockView | undefined): PropertyRow[] {
if (!view || !('properties' in view))
throw new Error(`块 ${view?.id} 不是属性表`)
return view.properties
}
function named(list: BlockView[], name: string): BlockView {
const found = list.find(view => 'properties' in view && propertyLines(view.properties)[0] === `名称: ${name}`)
if (!found)
throw new Error(`没有 ${name}`)
return found
}
/** 属性表里所有带下标的词(连表格里的格子)。 */
function linkedSegments(rows: PropertyRow[]): Segment[] {
const pieces: Inline[] = rows.flatMap(row => [
...('markdown' in row.value ? [] : row.value),
...(row.items ?? []).flatMap(item => item.cells.flat()),
])
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
}
const RULE_CASES = [
'from enum import Enum as E, IntFlag, auto, member, nonmember',
'import enum',
'',
'',
'class Color(E):',
' """颜色。"""',
'',
' _ignore_ = ["TEMP"]',
' __slots__ = ()',
' _order_ = "RED GREEN"',
' # 红色',
' # 最常用',
' RED = auto()',
' GREEN = 2 # 绿',
' """绿色。"""',
' TEMP = 9',
' __secret = 1',
' helper = lambda self: 1',
' label = property(lambda self: self.name)',
' table = nonmember({})',
'',
' def describe(self) -> str:',
' return self.name',
'',
' BLUE = member(3)',
' _private = 5',
' ALIAS: int = 4',
' _value_: int',
'',
' class Meta:',
' pass',
'',
' # 收尾',
'',
'',
'@enum.unique',
'class Perm(IntFlag):',
' R = 4',
' W = 2',
'',
].join('\n')
describe('enum 库规则(Python)', () => {
it('luffy 的枚举:和原型一样的枚举卡片,说明取行尾注释,类的 docstring 当说明', async () => {
const list = await views(SNAPSHOT)
expect(list.map(view => view.kind)).toEqual(['comment', 'import', 'import', 'import', 'enum', 'enum', 'enum', 'enum', 'enum'])
const commission = named(list, 'CommissionStatus')
expect(commission.badge).toBeUndefined()
expect(propertyLines(properties(commission))).toEqual([
'名称: CommissionStatus',
'访问级别: 公共',
'继承: str, enum.Enum',
'成员: 5 个',
' PENDING | "pending" | 待结算',
' AVAILABLE | "available" | 可提现',
' WITHDRAWING | "withdrawing" | 提现中',
' WITHDRAWN | "withdrawn" | 已提现',
' INVALID | "invalid" | 已失效',
])
expect(properties(commission).at(-1)?.columns).toEqual(['成员', '值', '说明'])
expect(propertyLines(properties(named(list, 'SecretScope')))).toEqual([
'名称: SecretScope',
'说明: 条目作用域: user 全局 / app 级 (同名覆盖 user).',
'访问级别: 公共',
'继承: enum.StrEnum',
'成员: 2 个',
' USER | "user"',
' APP | "app"',
])
expect(properties(named(list, 'SecretScope')).at(-1)?.columns).toEqual(['成员', '值'])
})
it('筛选:种类是 enum、叫「枚举」,访问级别同原来的类;卡片默认的编辑范围是整个类', async () => {
const list = await views(SNAPSHOT)
const commission = named(list, 'CommissionStatus')
expect(commission.attributes).toEqual({ kind: 'enum', access: 'public' })
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values)
.toContainEqual({ id: 'enum', name: '枚举', count: 5 })
const lines = SNAPSHOT.split('\n')
expect(lines[commission.startLine]).toBe('class CommissionStatus(str, enum.Enum):')
expect(lines[commission.endLine]).toBe(' INVALID = "invalid" # 已失效')
})
it('成员照 Python 的规则认:dunder、sunder、私有名、_ignore_ 里的、lambda、property、nonmember、只写注解的不是成员;auto() 写「自动」,member(x) 的值是 x', async () => {
const list = await views(RULE_CASES)
const color = named(list, 'Color')
expect(propertyLines(properties(color))).toEqual([
'名称: Color',
'说明: 颜色。',
'访问级别: 公共',
'继承: E',
'成员: 5 个',
' RED | 自动 | 红色 最常用',
' GREEN | 2 | 绿 绿色。',
' BLUE | 3 | ',
' _private | 5 | ',
' ALIAS | 4 | ',
])
const auto = properties(color).at(-1)?.items?.[0].cells[1]
expect(auto).toEqual([{ text: '自动', role: 'hint' }])
expect(propertyLines(properties(named(list, 'Perm')))).toEqual([
'名称: Perm',
'访问级别: 公共',
'装饰器: @enum.unique',
'继承: IntFlag',
'成员: 2 个',
' R | 4',
' W | 2',
])
})
it('方法、嵌套类、不是成员的属性不管写在哪都照常是子块;成员前面的注释并进卡片,最后面的注释留下;卡片「编辑」写回整个类', async () => {
const list = await views(RULE_CASES)
const color = named(list, 'Color')
const lines = RULE_CASES.split('\n')
expect(color.children.map(child => lines[child.startLine].trim())).toEqual([
'_ignore_ = ["TEMP"]',
'__slots__ = ()',
'_order_ = "RED GREEN"',
'TEMP = 9',
'__secret = 1',
'helper = lambda self: 1',
'label = property(lambda self: self.name)',
'table = nonmember({})',
'def describe(self) -> str:',
'_value_: int',
'class Meta:',
'# 收尾',
])
expect([lines[color.startLine], lines[color.endLine]]).toEqual(['class Color(E):', ' # 收尾'])
expect(replaceLines(RULE_CASES, color.startLine, color.endLine, color.code)).toBe(RULE_CASES)
const edited = replaceLines(RULE_CASES, color.startLine, color.endLine, color.code.replace('GREEN = 2', 'GREEN = 20'))
expect(edited).toBe(RULE_CASES.replace('GREEN = 2', 'GREEN = 20'))
})
it('跳到定义、高亮出现处:卡片是类名的定义,成员行是成员名的定义,带下标的词都指着源码里的这个名字', async () => {
const list = await views(SNAPSHOT)
const commission = named(list, 'CommissionStatus')
expect(SNAPSHOT.slice(commission.defines?.startIndex, commission.defines?.endIndex)).toBe('CommissionStatus')
const members = properties(commission).at(-1)?.items ?? []
expect(members.map(item => SNAPSHOT.slice(item.defines?.startIndex, item.defines?.endIndex))).toEqual(['PENDING', 'AVAILABLE', 'WITHDRAWING', 'WITHDRAWN', 'INVALID'])
const linked = linkedSegments(properties(commission))
expect(linked.map(segment => segment.text)).toEqual(expect.arrayContaining(['CommissionStatus', 'str', 'enum', 'Enum', 'PENDING', '"pending"']))
for (const segment of linked)
expect(SNAPSHOT.slice(segment.offset, (segment.offset ?? 0) + segment.text.length), segment.text).toBe(segment.text)
})
it('不是 enum 的枚举类型不认:没导入 enum、父类是 SQLAlchemy 的 Enum、父类不是枚举类型', async () => {
const plain = await views('class Color(Enum):\n RED = 1\n')
expect(plain.map(view => view.kind)).toEqual(['class'])
const other = await views('import enum\nfrom sqlalchemy import Enum\n\n\nclass A(Enum):\n X = 1\n\n\nclass B(enum.auto):\n Y = 1\n\n\nclass C(str):\n Z = 1\n')
expect(other.map(view => view.kind)).toEqual(['import', 'import', 'class', 'class', 'class'])
const aliased = await views('import enum as e\n\n\nclass A(e.IntEnum):\n X = 1\n')
expect(aliased.map(view => view.kind)).toEqual(['import', 'enum'])
})
})
describe('枚举:TypeScript 和 Python 同一张卡片', () => {
it('同样的成员:种类、种类名、属性表的行、列名、格子都一样(TypeScript 没有「继承」)', async () => {
const tsSource = 'export enum Status {\n PENDING = "pending", // 待结算\n /** 可提现 */\n AVAILABLE = "available",\n}\n'
const [tsEnum] = await tsViews(tsSource)
const [pending] = properties(tsEnum).at(-1)?.items ?? []
expect(pending.cells[0]).toEqual([{ text: 'PENDING', role: 'variable', offset: tsSource.indexOf('PENDING') }])
expect(pending.defines).toEqual({ startIndex: tsSource.indexOf('PENDING'), endIndex: tsSource.indexOf('PENDING') + 'PENDING'.length })
const [, pyEnum] = await views('import enum\n\n\nclass Status(enum.Enum):\n PENDING = "pending" # 待结算\n # 可提现\n AVAILABLE = "available"\n')
const shape = (view: BlockView): unknown => {
const rows = properties(view).filter(row => row.label !== '继承')
return {
kind: view.kind,
badge: view.badge,
attributes: view.attributes,
lines: propertyLines(rows),
columns: rows.map(row => row.columns),
roles: rows.at(-1)?.items?.map(item => item.cells.map(cell => flatSegments(cell).map(segment => segment.role))),
}
}
expect(shape(tsEnum)).toEqual(shape(pyEnum))
expect(propertyLines(properties(tsEnum))).toEqual([
'名称: Status',
'访问级别: 公共',
'成员: 2 个',
' PENDING | "pending" | 待结算',
' AVAILABLE | "available" | 可提现',
])
})
it('typeScript 没写值的成员写算出来的数,算不出来写「自动」,都是说明词', async () => {
const [direction] = await tsViews('enum Direction { Up, Down = 5, Left, Mask = 1 << 2, After }')
expect(propertyLines(properties(direction))).toEqual([
'名称: Direction',
'访问级别: 私有',
'成员: 5 个',
' Up | 0',
' Down | 5',
' Left | 6',
' Mask | 1 << 2',
' After | 自动',
])
expect(properties(direction).at(-1)?.items?.map(item => item.cells[1])).toEqual([
[{ text: '0', role: 'hint' }],
[{ text: '5', role: 'number' }],
[{ text: '6', role: 'hint' }],
expect.any(Array),
[{ text: '自动', role: 'hint' }],
])
})
})
+152
View File
@@ -0,0 +1,152 @@
import type { ClassBlock, CodeBlock, EnumMemberFacts, ImportFacts, LibraryRule, Node, RuleFacts } from '../contract'
import { leadingComments, withLeadingComments } from './children'
import { stringValue, syntaxChildren } from './pythonTree'
/**
* Python 自带 enum 库的规则:父类里有 enum 的枚举类型(Enum、IntEnum、StrEnum、Flag、IntFlag、ReprEnum;写成 `enum.X`、
* 从 enum 导入的名字或它的别名都算,可以和别的父类混着写:`class Status(str, enum.Enum)`)的类,认成契约里的枚举块(EnumFacts):
* 和 TypeScript 的 enum 是同一种块、同一张属性表,用词归翻译插件,规则只给 enum 库的知识——哪些类是枚举、哪些赋值是成员。
* 成员照 Python 的规则认(见 memberOf);成员连同讲它的注释并进来,方法、嵌套类、别的属性照常是子块。
* 父类是同一文件里别的枚举类(`class Sub(Base)`)的认不出来:规则只看这一块。
*/
const LIBRARY = 'enum'
const ENUM_TYPES = new Set(['Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum'])
/** 值是这几种内置描述符的类属性不是成员。 */
const DESCRIPTORS = new Set(['property', 'classmethod', 'staticmethod'])
/** 这份源码里怎么叫 enum 库的东西:库自己(`import enum`、`import enum as e`),和从它导入的名字(本地名 → 原名)。 */
interface Names {
modules: Set<string>
imported: Map<string, string>
}
function namesOf(imports: readonly ImportFacts[]): Names {
const names: Names = { modules: new Set(), imported: new Map() }
for (const facts of imports) {
const fromEnum = facts.sources.some(source => source.text === LIBRARY)
for (const item of facts.items) {
if (item.kind === 'module' && item.module.text === LIBRARY)
names.modules.add(item.alias?.text ?? LIBRARY)
else if (item.kind === 'named' && fromEnum)
names.imported.set(item.alias?.text ?? item.name.text, item.name.text)
}
}
return names
}
/** 节点指的是 enum 库里的哪个名字:`enum.Enum`、从 enum 导入的 `Enum`(别名换回原名);别的是 undefined。 */
function enumName(node: Node | null | undefined, names: Names): string | undefined {
if (node?.type === 'identifier')
return names.imported.get(node.text)
const object = node?.type === 'attribute' ? node.childForFieldName('object') : null
return object?.type === 'identifier' && names.modules.has(object.text) ? node?.childForFieldName('attribute')?.text : undefined
}
/** 调用的是 enum 库里的哪个函数(`auto()`、`enum.nonmember(...)`)。 */
function enumCall(node: Node | null | undefined, names: Names): string | undefined {
return node?.type === 'call' ? enumName(node.childForFieldName('function'), names) : undefined
}
/** 类主体里给一个名字赋值的子块(`A = 1`、`A: int = 1`、只写注解的 `A: int`):名字和值。 */
function assignmentOf(block: CodeBlock): { name: Node, value?: Node } | undefined {
if (block.kind !== 'variable' && block.kind !== 'field')
return undefined
const assignment = block.node.firstNamedChild
const name = assignment?.type === 'assignment' ? assignment.childForFieldName('left') : null
if (name?.type !== 'identifier')
return undefined
return { name, value: assignment?.childForFieldName('right') ?? undefined }
}
/** `_ignore_` 里列的名字:空格隔开的字符串,或字符串的列表、元组。 */
function ignoredNames(children: readonly CodeBlock[]): Set<string> {
const value = children.map(assignmentOf).find(assignment => assignment?.name.text === '_ignore_')?.value
const text = stringValue(value)
if (text !== undefined)
return new Set(text.split(/\s+/).filter(Boolean))
const items = value?.type === 'list' || value?.type === 'tuple' ? syntaxChildren(value).map(stringValue) : []
return new Set(items.filter((item): item is string => item !== undefined))
}
/** 名字能当成员:不是 `__x__`(dunder)、`_x_`(enum 保留的 sunder)、`__x`(私有,会被改名)。 */
function memberName(name: string): boolean {
const dunder = name.length > 4 && name.startsWith('__') && name.endsWith('__')
const sunder = name.length > 2 && name.startsWith('_') && name.endsWith('_') && name[1] !== '_' && name.at(-2) !== '_'
const privateName = name.startsWith('__') && !name.endsWith('__')
return !dunder && !sunder && !privateName
}
/**
* 一个子块是不是成员,是的话它的值:名字能当成员、不在 `_ignore_` 里,赋了值,值不是函数(lambda)、内置描述符
* (property、classmethod、staticmethod)、`nonmember(...)`、连着赋值(`A = B = 1`)。
* `auto()` 的值交给库自动取(没有 value),`member(x)` 的值是 x。
*/
function memberOf(block: CodeBlock, ignored: ReadonlySet<string>, names: Names): { name: Node, value?: Node } | undefined {
const assignment = assignmentOf(block)
const value = assignment?.value
if (!assignment || !value || !memberName(assignment.name.text) || ignored.has(assignment.name.text))
return undefined
const callee = value.type === 'call' ? value.childForFieldName('function') : null
if (value.type === 'lambda' || value.type === 'assignment' || (callee?.type === 'identifier' && DESCRIPTORS.has(callee.text)))
return undefined
switch (enumCall(value, names)) {
case 'nonmember':
return undefined
case 'auto':
return { name: assignment.name }
case 'member':
return { name: assignment.name, value: syntaxChildren(value.childForFieldName('arguments'))[0] }
default:
return { name: assignment.name, value }
}
}
/** 注释块里的各条注释(几条行注释连成的一块是好几个节点);当注释用的字符串是那个字符串。 */
function commentNodes(block: CodeBlock): Node[] {
if (block.node.type !== 'comment')
return syntaxChildren(block.node).slice(0, 1)
const nodes: Node[] = []
for (let node: Node | null = block.node; node?.type === 'comment' && node.endIndex <= block.header.endIndex; node = node.nextSibling)
nodes.push(node)
return nodes
}
/** 成员的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、紧跟在它后面的属性 docstring。 */
function memberDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] {
const trailing = block.node.nextSibling
const doc = block.kind === 'variable' ? block.facts.doc : block.kind === 'field' ? block.doc : undefined
return [
...leadingComments(children, block).flatMap(commentNodes),
...(trailing?.type === 'comment' && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []),
...(doc ? [doc] : []),
]
}
function enumBlock(block: ClassBlock, names: Names): RuleFacts | undefined {
const { facts, children } = block
if (!facts.name || !facts.bases.some(base => ENUM_TYPES.has(enumName(base.value, names) ?? '')))
return undefined
const ignored = ignoredNames(children)
const members: EnumMemberFacts[] = []
const blocks: CodeBlock[] = []
for (const child of children) {
const member = memberOf(child, ignored, names)
if (!member)
continue
members.push({ ...member, docs: memberDocs(children, child) })
blocks.push(child)
}
return {
as: { kind: 'enum', facts: { name: facts.name, doc: facts.doc, access: facts.access, decorators: facts.decorators, bases: facts.bases, members } },
absorbs: withLeadingComments(children, blocks),
}
}
export const enumRule: LibraryRule = {
id: 'enum',
label: 'Python 枚举(enum)',
languageIds: ['python'],
libraries: [LIBRARY],
claim: (block, context) => block.kind === 'class' ? enumBlock(block, namesOf(context.imports)) : undefined,
}
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, ThemeColor, TokenRole } from '../contract'
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, TokenRole } from '../contract'
import type { Call } from './pythonTree'
import { withLeadingComments } from './children'
import { aliasesOf, callOf, canonicalName, lastName, stringValue, syntaxChildren } from './pythonTree'
@@ -414,7 +414,7 @@ function tableRow(label: string, heads: string[], items: PropertyItem[], env: En
return items.length > 0 ? [{ label, value: [hint(env.words.count(items.length))], columns: heads, items }] : []
}
function tableBlock(block: ClassBlock, model: Model, env: Env): RuleBlock {
function tableBlock(block: ClassBlock, model: Model, env: Env): RuleCard {
const { words, context } = env
const { facts } = block
const { labels } = words
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BaseFacts, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -136,6 +136,7 @@ const KIND_ZH: Record<BlockKind, string> = {
import: '导入',
function: '函数',
class: '类',
enum: '枚举',
type: '类型',
field: '字段',
const: '常量',
@@ -432,6 +433,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return functionRows(block.facts, phrases)
case 'class':
return classRows(block.facts, phrases)
case 'enum':
return enumRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
default:
@@ -611,11 +614,46 @@ function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
...flagRows('abstract', facts.abstract),
...decoratorRows(facts.decorators, say),
...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)),
...listRow('父类', facts.bases.map(base => base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value))),
...listRow('父类', facts.bases.map(base => baseText(base, say))),
...listRow('实现的接口', facts.implements.map(say.translated)),
]
}
/** 父类的一项:TypeScript 的泛型实参紧跟在后面。 */
function baseText(base: BaseFacts, say: Phrases): Inline[] {
return base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value)
}
/**
* 枚举(TypeScript 的 enum、Python enum 库的类,同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),有才显示;
* 成员收成一张表:成员 | 值 | 说明。没写值的成员写语言算出的值(自增的 0、1、2……),算不出来写「自动」,都按说明词着色;
* 说明是成员的注释、JSDoc、属性 docstring 的原文用空格连起来,一个成员都没有说明时不给这一列。
*/
function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] {
const docs = facts.members.map(member => member.docs.map(node => say.doc(node)?.markdown).filter(Boolean).join(' '))
const described = docs.some(Boolean)
return [
{ label: '名称', value: say.name(facts.name, 'type') },
...docRows(facts.doc, say),
...accessRows(facts.access),
...decoratorRows(facts.decorators, say),
...(facts.bases.length > 0 ? [{ label: '继承', value: joined(facts.bases.map(base => baseText(base, say)), ', ') }] : []),
{
label: '成员',
value: count(facts.members.length),
columns: described ? ['成员', '值', '说明'] : ['成员', '值'],
items: facts.members.map((member, i) => ({
cells: [
say.name(member.name, 'variable'),
member.value ? say.translated(member.value) : [hint(member.computed === undefined ? '自动' : String(member.computed))],
...(described ? [docs[i] ? [segment(docs[i], 'comment', undefined)] : []] : []),
],
...defining(member.name),
})),
},
]
}
/** 函数、类、常量、变量的说明:有文字才有这一行。 */
function docRows(doc: Node | undefined, say: Phrases): PropertyRow[] {
const markdown = doc && say.doc(doc)