✨ feat(vscode): 代码阅读类块改成属性表并默认折叠,类的说明进「说明」行
This commit is contained in:
1 parent
3329aca2e1
commit
43323659e1
22 files changed
+580
-121
No files matched your search
@@ -29,11 +29,13 @@ function byId(list: BlockView[], id: string): BlockView {
|
||||
const SAMPLES: [LanguageAdapter, string, Translator, string][] = [
|
||||
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'issueLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'classes.ts.txt'],
|
||||
[py, 'python', pythonZhTranslator, 'sample.py'],
|
||||
[py, 'python', pythonZhTranslator, 'routes.py'],
|
||||
[py, 'python', pythonZhTranslator, 'classes.py'],
|
||||
]
|
||||
|
||||
const PROPERTY_KINDS = new Set(['function', 'import', 'const', 'variable'])
|
||||
const PROPERTY_KINDS = new Set(['function', 'class', 'import', 'const', 'variable'])
|
||||
|
||||
function translationText(view: BlockView): string {
|
||||
if (!('translation' in view))
|
||||
@@ -57,7 +59,7 @@ describe('blockView', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each(SAMPLES)('%#: 函数、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
|
||||
it.each(SAMPLES)('%#: 函数、类、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
|
||||
for (const view of flatten(await views(adapter, languageId, translator, readFixture(fixture)))) {
|
||||
expect('properties' in view, view.id).toBe(PROPERTY_KINDS.has(view.kind))
|
||||
expect('translation' in view, view.id).toBe(!PROPERTY_KINDS.has(view.kind))
|
||||
@@ -100,6 +102,26 @@ describe('blockView', () => {
|
||||
expect(getUser?.children[0]).toMatchObject({ kind: 'comment', startLine: 38, endLine: 38 })
|
||||
})
|
||||
|
||||
it('类的说明同样算在类块的头部里:JSDoc 连着中间的装饰器,docstring 连着它前面的注释', async () => {
|
||||
const repo = byId(await views(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt')), '1')
|
||||
expect(repo).toMatchObject({ kind: 'class', startLine: 2, endLine: 8 })
|
||||
expect(repo.code.split('\n').slice(-3)).toEqual([
|
||||
' */',
|
||||
'@Injectable()',
|
||||
'export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {',
|
||||
])
|
||||
expect('properties' in repo && repo.properties[1]).toEqual({ label: '说明', value: { markdown: '仓库基类。\n\n子类实现 `load`。' } })
|
||||
|
||||
const notFound = byId(await views(py, 'python', pythonZhTranslator, readFixture('classes.py')), '3')
|
||||
expect(notFound).toMatchObject({ kind: 'class', startLine: 17, endLine: 19 })
|
||||
expect(notFound.code.split('\n')).toEqual([
|
||||
'class NotFound(AppException): # 行尾',
|
||||
' # 说明前面的注释',
|
||||
' """找不到资源。"""',
|
||||
])
|
||||
expect(notFound.children[0]).toMatchObject({ kind: 'variable', startLine: 20, endLine: 20 })
|
||||
})
|
||||
|
||||
it('注释块的译文、函数的「说明」是 Markdown 原文,webview 按它渲染', async () => {
|
||||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
expect(byId(env, '0')).toMatchObject({ kind: 'comment', translation: { markdown: expect.stringMatching(/^\.env file lock \/ unlock helpers\.\n\nRecursively/) } })
|
||||
@@ -157,13 +179,13 @@ describe('blockView', () => {
|
||||
expect(signature.filter(token => token.text === 'str')).toEqual([{ text: 'str', role: 'type' }, { text: 'str', role: 'type' }])
|
||||
})
|
||||
|
||||
it('块自己的名字(defines):函数名、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块没有', async () => {
|
||||
it('块自己的名字(defines):函数名、类名(赋给变量的类表达式取变量名)、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块、匿名类没有', async () => {
|
||||
const own = (source: string, list: BlockView[]): Record<string, string | undefined> =>
|
||||
Object.fromEntries(list.map(view => [view.id, view.defines && source.slice(view.defines.startIndex, view.defines.endIndex)]))
|
||||
const pySource = ['import os', 'def f(x): pass', 'a, b = 1, 2', 'for i in x: pass'].join('\n')
|
||||
expect(own(pySource, await views(py, 'python', pythonZhTranslator, pySource))).toEqual({ 0: undefined, 1: 'f', 2: 'a, b', 3: undefined })
|
||||
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1'].join('\n')
|
||||
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h' })
|
||||
const pySource = ['import os', 'def f(x): pass', 'a, b = 1, 2', 'for i in x: pass', 'class K: pass'].join('\n')
|
||||
expect(own(pySource, await views(py, 'python', pythonZhTranslator, pySource))).toEqual({ 0: undefined, 1: 'f', 2: 'a, b', 3: undefined, 4: 'K' })
|
||||
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1', 'class A {}', 'const B = class C {}', 'export default class {}'].join('\n')
|
||||
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h', 3: 'A', 4: 'B', 5: undefined })
|
||||
})
|
||||
|
||||
it('\\r\\n 和制表符缩进:code 用 \\n 连接,去掉共同的制表符', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ interface BlockViewBase {
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 发给 webview 的一块(可序列化):函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
|
||||
/** 发给 webview 的一块(可序列化):函数、类、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||
|
||||
interface Line { start: number, end: number }
|
||||
@@ -83,12 +83,13 @@ function colorLines(root: Node, source: string, lines: Line[], startRow: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* 这块本身就是哪个名字的定义:函数名,只声明一个变量(或一个解构模式)时的变量名。
|
||||
* 这块本身就是哪个名字的定义:函数名、类名,只声明一个变量(或一个解构模式)时的变量名。
|
||||
* 导入、一条语句声明几个变量时没有,它们的名字各是属性表里的一行(PropertyRow.defines)。
|
||||
*/
|
||||
function ownName(block: CodeBlock): Node | undefined {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
case 'class':
|
||||
return block.facts.name
|
||||
case 'const':
|
||||
case 'variable':
|
||||
|
||||
@@ -42,6 +42,33 @@ export interface FunctionFacts {
|
||||
returnType?: Node
|
||||
}
|
||||
|
||||
/**
|
||||
* 父类的一项:TypeScript `extends Base<T>` 里的 Base(一个表达式)和跟在它后面的泛型实参 `<T>`(语法里是两个节点);
|
||||
* Python 类参数列表里的一项,`metaclass=ABCMeta` 这类关键字参数、`*bases` 也算。
|
||||
*/
|
||||
export interface BaseFacts {
|
||||
value: Node
|
||||
typeArguments?: Node
|
||||
}
|
||||
|
||||
export interface ClassFacts {
|
||||
/** 匿名类(`export default class {}`)没有;赋给变量、类字段的类表达式取变量名、字段名。 */
|
||||
name?: Node
|
||||
/** 类自己的说明,同 FunctionFacts.doc:TypeScript 是紧贴在前面的 JSDoc,Python 是主体开头的 docstring。 */
|
||||
doc?: Node
|
||||
/** 同函数:嵌套在函数体、条件等语句块里的类没有。 */
|
||||
access?: Access
|
||||
/** 抽象类(TypeScript 的 `abstract class`);没有这个概念的语言不填。 */
|
||||
abstract?: boolean
|
||||
decorators: Node[]
|
||||
/** 泛型参数,每个参数一项(TypeScript `<T extends X = Y>` 里的 `T extends X = Y`)。 */
|
||||
typeParameters: Node[]
|
||||
/** 按源码顺序。 */
|
||||
bases: BaseFacts[]
|
||||
/** 实现的接口(TypeScript 的 `implements A, B<T>`),每个接口一项。 */
|
||||
implements: Node[]
|
||||
}
|
||||
|
||||
/** 导入的一项。 */
|
||||
export type ImportItem
|
||||
= | { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
|
||||
@@ -77,7 +104,7 @@ export interface BlockShape {
|
||||
node: Node
|
||||
/**
|
||||
* 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句。
|
||||
* 函数的说明(FunctionFacts.doc)也算在里面:JSDoc 往上延到注释开头,docstring 往下延到字符串结尾。
|
||||
* 函数、类的说明(facts.doc)也算在里面:JSDoc 往上延到注释开头,docstring 往下延到字符串结尾。
|
||||
*/
|
||||
header: SourceSpan
|
||||
children: CodeBlock[]
|
||||
@@ -85,9 +112,13 @@ export interface BlockShape {
|
||||
|
||||
/** 预览成属性表的块:语言插件从语法树读出结构化信息(facts),翻译插件把它排成属性表。 */
|
||||
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
|
||||
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
|
||||
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
|
||||
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
|
||||
export type FactBlock = FunctionBlock | ImportBlock | VariableBlock
|
||||
export type FactBlock = FunctionBlock | ClassBlock | ImportBlock | VariableBlock
|
||||
|
||||
/** 带说明(facts.doc)的块。 */
|
||||
export type DocumentedBlock = FunctionBlock | ClassBlock
|
||||
|
||||
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
|
||||
|
||||
@@ -116,12 +147,12 @@ export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
*/
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明)。 */
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
|
||||
* 声明的变量名)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||
* 声明的变量名、类的泛型参数)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
@@ -154,7 +185,7 @@ export interface Translator {
|
||||
languageIds: string[]
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||
translate: (block: TextBlock, source: string) => PreviewText
|
||||
/** 函数、导入、常量、变量块 → 属性表。 */
|
||||
/** 函数、类、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[]
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,11 @@ describe('行替换', () => {
|
||||
it.each([
|
||||
[createTypeScriptAdapter(WASM_DIR), 'typescript', 'envLock.ts.txt'],
|
||||
[createTypeScriptAdapter(WASM_DIR), 'typescript', 'issueLock.ts.txt'],
|
||||
[createTypeScriptAdapter(WASM_DIR), 'typescript', 'classes.ts.txt'],
|
||||
[createPythonAdapter(WASM_DIR), 'python', 'sample.py'],
|
||||
[createPythonAdapter(WASM_DIR), 'python', 'routes.py'],
|
||||
] as const)('%#: 把预览里的 code 原样写回,文件不变(去缩进与补缩进互逆,函数头部连着说明也一样)', async (adapter, languageId, fixture) => {
|
||||
[createPythonAdapter(WASM_DIR), 'python', 'classes.py'],
|
||||
] as const)('%#: 把预览里的 code 原样写回,文件不变(去缩进与补缩进互逆,函数、类头部连着说明也一样)', async (adapter, languageId, fixture) => {
|
||||
const source = readFixture(fixture)
|
||||
const views = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, source, adapter, undefined))
|
||||
const all = views.flatMap(function flat(view): typeof views {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""领域异常。"""
|
||||
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class AppException(Exception):
|
||||
"""应用异常的基类。
|
||||
|
||||
带上 HTTP 状态码。
|
||||
"""
|
||||
|
||||
status_code: int = 500
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
self.message = message
|
||||
|
||||
|
||||
class NotFound(AppException): # 行尾
|
||||
# 说明前面的注释
|
||||
"""找不到资源。"""
|
||||
status_code = 404
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Meta(Base, metaclass=ABCMeta):
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
class _Hidden:
|
||||
pass
|
||||
|
||||
|
||||
class _Private:
|
||||
pass
|
||||
|
||||
|
||||
def factory():
|
||||
class Local:
|
||||
"""局部类。"""
|
||||
return Local
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from './di'
|
||||
|
||||
/**
|
||||
* 仓库基类。
|
||||
*
|
||||
* 子类实现 `load`。
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {
|
||||
/** 缓存 */
|
||||
private readonly cache = new Map<K, T>()
|
||||
|
||||
abstract load(id: K): Promise<T>
|
||||
|
||||
static Query = class extends QueryBase {
|
||||
run() {}
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
x = 1
|
||||
}
|
||||
|
||||
/** 类表达式 */
|
||||
export const Mixed = class Inner extends mixin(A, B) {}
|
||||
|
||||
function make() {
|
||||
class Local {}
|
||||
return Local
|
||||
}
|
||||
|
||||
@sealed class Plain {}
|
||||
@@ -90,7 +90,7 @@ describe('python 代码块树', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 函数的 docstring', () => {
|
||||
describe('python 函数、类的 docstring', () => {
|
||||
const source = [
|
||||
'class K:',
|
||||
' """类说明"""',
|
||||
@@ -105,10 +105,9 @@ describe('python 函数的 docstring', () => {
|
||||
'def g(): "单行"',
|
||||
].join('\n')
|
||||
|
||||
it('算进函数头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块;类的 docstring 照旧是注释块', async () => {
|
||||
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块', async () => {
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'class', kids: [
|
||||
{ at: 2, kind: 'comment' },
|
||||
{ at: [1, 2], kind: 'class', kids: [
|
||||
{ at: [3, 8], kind: 'function', kids: [
|
||||
{ at: 9, kind: 'comment' },
|
||||
{ at: 10, kind: 'return' },
|
||||
@@ -118,9 +117,11 @@ describe('python 函数的 docstring', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有', async () => {
|
||||
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有;模块开头的 docstring 照旧是注释块', async () => {
|
||||
const found = await facts(source)
|
||||
expect(found[1]).toMatchObject({ name: 'K', doc: '"""类说明"""' })
|
||||
expect(found[3]).toMatchObject({ name: 'run', doc: '"""跑一遍。\n\n 细节。\n """' })
|
||||
expect(await tree('"""模块说明"""\nclass K:\n pass')).toEqual([{ at: 1, kind: 'comment' }, { at: 2, kind: 'class', kids: [{ at: 3, kind: 'statement' }] }])
|
||||
expect(found[11]).toMatchObject({ name: 'g', doc: '"单行"' })
|
||||
expect(await facts('def f():\n x = 1\n """not a docstring"""')).not.toHaveProperty('1.doc')
|
||||
})
|
||||
@@ -236,3 +237,63 @@ describe('python 属性表信息(facts)', () => {
|
||||
expect(found[10]).not.toHaveProperty('access')
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 类', () => {
|
||||
it('classes.py:docstring(连同它前面的注释)算进类头部,类里的类、函数里的类照常成块', async () => {
|
||||
expect(await tree(readFixture('classes.py'))).toEqual([
|
||||
{ at: 1, kind: 'comment' },
|
||||
{ at: 3, kind: 'import' },
|
||||
{ at: [6, 10], kind: 'class', kids: [
|
||||
{ at: 12, kind: 'field' },
|
||||
{ at: 14, kind: 'function', kids: [{ at: 15, kind: 'variable' }] },
|
||||
] },
|
||||
{ at: [18, 20], kind: 'class', kids: [{ at: 21, kind: 'variable' }] },
|
||||
{ at: [24, 25], kind: 'class', kids: [
|
||||
{ at: 26, kind: 'class', kids: [{ at: 27, kind: 'variable' }] },
|
||||
{ at: 29, kind: 'class', kids: [{ at: 30, kind: 'statement' }] },
|
||||
] },
|
||||
{ at: 33, kind: 'class', kids: [{ at: 34, kind: 'statement' }] },
|
||||
{ at: 37, kind: 'function', kids: [
|
||||
{ at: [38, 39], kind: 'class' },
|
||||
{ at: 40, kind: 'return' },
|
||||
] },
|
||||
])
|
||||
})
|
||||
|
||||
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
|
||||
const found = await facts(readFixture('classes.py'))
|
||||
expect(found[6]).toEqual({
|
||||
name: 'AppException',
|
||||
doc: '"""应用异常的基类。\n\n 带上 HTTP 状态码。\n """',
|
||||
access: 'public',
|
||||
decorators: [],
|
||||
typeParameters: [],
|
||||
bases: [{ value: 'Exception' }],
|
||||
implements: [],
|
||||
})
|
||||
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppException' }] })
|
||||
expect(found[24]).toEqual({
|
||||
name: 'Meta',
|
||||
access: 'public',
|
||||
decorators: ['@dataclass(frozen=True)'],
|
||||
typeParameters: [],
|
||||
bases: [{ value: 'Base' }, { value: 'metaclass=ABCMeta' }],
|
||||
implements: [],
|
||||
})
|
||||
expect(found[26]).toMatchObject({ name: 'Config', access: 'public', bases: [] })
|
||||
expect(found[29]).toMatchObject({ name: '_Hidden', access: 'private' })
|
||||
expect(found[33]).toMatchObject({ name: '_Private', access: 'private' })
|
||||
expect(found[38]).toEqual({ name: 'Local', doc: '"""局部类。"""', decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
})
|
||||
|
||||
it('没有「抽象」这个概念;Python 3.12 的泛型参数进 typeParameters;*bases、**kw 也是父类里的一项', async () => {
|
||||
expect((await facts('class G[T](Generic[T], *mixins, **kw):\n pass'))[1]).toEqual({
|
||||
name: 'G',
|
||||
access: 'public',
|
||||
decorators: [],
|
||||
typeParameters: ['T'],
|
||||
bases: [{ value: 'Generic[T]' }, { value: '*mixins' }, { value: '**kw' }],
|
||||
implements: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BlockShape, ClassFacts, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
|
||||
@@ -9,7 +9,7 @@ type Scope = 'module' | 'class' | 'other'
|
||||
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
|
||||
const PARAMETER_SEPARATORS = new Set(['keyword_separator', 'positional_separator'])
|
||||
const PATTERNS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern'])
|
||||
// 只有 # 行注释;函数的说明是主体开头的 docstring,不是写在前面的注释
|
||||
// 只有 # 行注释;函数、类的说明是主体开头的 docstring,不是写在前面的注释
|
||||
const COMMENTS: CommentRules = { isLine: () => true, isDoc: () => false }
|
||||
|
||||
function statements(items: Node[], scope: Scope, afterRow: number, docstring: boolean): CodeBlock[] {
|
||||
@@ -73,14 +73,12 @@ function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBloc
|
||||
/** function_definition / class_definition;outer 是带装饰器的整体时头部从装饰器开始。 */
|
||||
function definitionBlock(definition: Node, outer: Node, scope: Scope): CodeBlock {
|
||||
const body = definition.childForFieldName('body')
|
||||
if (definition.type === 'class_definition')
|
||||
return { kind: 'class', ...bodyShape(outer, body, 'class', true) }
|
||||
const doc = docstringOf(body)
|
||||
return {
|
||||
kind: 'function',
|
||||
...(body && doc ? documentedShape(outer, body, doc) : bodyShape(outer, body, 'other')),
|
||||
facts: functionFacts(definition, outer, scope, doc),
|
||||
}
|
||||
const inner: Scope = definition.type === 'class_definition' ? 'class' : 'other'
|
||||
const shape = body && doc ? documentedShape(outer, body, doc, inner) : bodyShape(outer, body, inner)
|
||||
if (definition.type === 'class_definition')
|
||||
return { kind: 'class', ...shape, facts: classFacts(definition, outer, scope, doc) }
|
||||
return { kind: 'function', ...shape, facts: functionFacts(definition, outer, scope, doc) }
|
||||
}
|
||||
|
||||
/** 主体开头的 docstring:第一条语句(注释不算)是单独一个字符串表达式时,返回那个字符串节点。 */
|
||||
@@ -97,10 +95,10 @@ function docstringIn(statement: Node): Node | undefined {
|
||||
return rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string') ? expression : undefined
|
||||
}
|
||||
|
||||
/** 函数的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
|
||||
function documentedShape(outer: Node, body: Node, doc: Node): BlockShape {
|
||||
/** 函数、类的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
|
||||
function documentedShape(outer: Node, body: Node, doc: Node, scope: Scope): BlockShape {
|
||||
const rest = named(body).filter(item => item.startIndex >= doc.endIndex)
|
||||
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, 'other', afterRow, false))
|
||||
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, scope, afterRow, false))
|
||||
}
|
||||
|
||||
/** 单下划线开头的名字是私有(`_x`、`__x`),前后双下划线的特殊方法(`__init__`)是公共。 */
|
||||
@@ -140,7 +138,7 @@ function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node |
|
||||
doc,
|
||||
access: name ? scopeAccess(name, scope) : undefined,
|
||||
async: children(definition).some(child => child.type === 'async'),
|
||||
decorators: named(outer).filter(child => child.type === 'decorator'),
|
||||
decorators: decoratorsOf(outer),
|
||||
parameters: syntaxChildren(definition.childForFieldName('parameters'))
|
||||
.filter(param => !PARAMETER_SEPARATORS.has(param.type))
|
||||
.map(parameterFacts),
|
||||
@@ -148,6 +146,26 @@ function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node |
|
||||
}
|
||||
}
|
||||
|
||||
/** 父类是类名后面括号里的参数列表,照写的顺序一项一项取,`metaclass=…` 这类关键字参数也在里面。 */
|
||||
function classFacts(definition: Node, outer: Node, scope: Scope, doc: Node | undefined): ClassFacts {
|
||||
const name = definition.childForFieldName('name')
|
||||
return {
|
||||
name: name ?? undefined,
|
||||
doc,
|
||||
access: name ? scopeAccess(name, scope) : undefined,
|
||||
decorators: decoratorsOf(outer),
|
||||
// Python 3.12 的 `class G[T]:`
|
||||
typeParameters: syntaxChildren(definition.childForFieldName('type_parameters')),
|
||||
bases: syntaxChildren(definition.childForFieldName('superclasses')).map(value => ({ value })),
|
||||
implements: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 装饰器在 decorated_definition 里,和被装饰的定义是兄弟节点。 */
|
||||
function decoratorsOf(outer: Node): Node[] {
|
||||
return named(outer).filter(child => child.type === 'decorator')
|
||||
}
|
||||
|
||||
/** 带注解的参数(typed_parameter)语法没给名字起字段名,名字是第一个具名子节点(可能是 `*args`)。 */
|
||||
function parameterFacts(param: Node): ParameterFacts {
|
||||
switch (param.type) {
|
||||
@@ -301,6 +319,9 @@ function identifierRole(leaf: Node): TokenRole {
|
||||
return 'function'
|
||||
if (parent.type === 'class_definition' && sameNode(parent.childForFieldName('name'), leaf))
|
||||
return 'type'
|
||||
// 父类:类名后面括号里直接写的名字
|
||||
if (parent.type === 'argument_list' && parent.parent?.type === 'class_definition')
|
||||
return 'type'
|
||||
if (parent.type === 'decorator' || isCallee(leaf))
|
||||
return 'function'
|
||||
if (parent.type === 'attribute' && sameNode(parent.childForFieldName('attribute'), leaf) && isCallee(parent))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockShape, CodeBlock, FunctionBlock, Node, SourceSpan, TextKind } from '../contract'
|
||||
import type { BlockShape, CodeBlock, DocumentedBlock, Node, SourceSpan, TextKind } from '../contract'
|
||||
|
||||
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
|
||||
|
||||
@@ -89,19 +89,19 @@ export function compoundShape(outer: Node, bodyStart: number, items: Node[], lis
|
||||
export interface CommentRules {
|
||||
/** 行注释:相邻几行合成一块。 */
|
||||
isLine: (comment: Node) => boolean
|
||||
/** 文档注释(JSDoc):紧贴在函数块前面时是这个函数的说明,见 FunctionFacts.doc。 */
|
||||
/** 文档注释(JSDoc):紧贴在函数、类块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc。 */
|
||||
isDoc: (comment: Node) => boolean
|
||||
}
|
||||
|
||||
/** 函数块带上说明:说明进 facts.doc,头部往上延到它开头。 */
|
||||
function documented(block: FunctionBlock, doc: Node): FunctionBlock {
|
||||
/** 函数、类块带上说明:说明进 facts.doc,头部往上延到它开头。 */
|
||||
function documented<B extends DocumentedBlock>(block: B, doc: Node): B {
|
||||
return { ...extendHeader(block, doc), facts: { ...block.facts, doc } }
|
||||
}
|
||||
|
||||
/**
|
||||
* 一串语句 → 块。注释单独成块,相邻行的行注释合成一块;
|
||||
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
|
||||
* 文档注释和下一条语句之间只有空白、下一条语句是函数块时,它是这个函数的说明,不单独成块;
|
||||
* 文档注释和下一条语句之间只有空白、下一条语句是函数或类块时,它是这个函数、类的说明,不单独成块;
|
||||
* 中间隔着的装饰器(不出块的语句)不算隔开。
|
||||
* afterRow 是这串语句之前的最后一行(头部结尾行),模块顶层传 -1;
|
||||
* toBlocks 的 first 表示它是这串里第一条非注释语句(Python 认 docstring 用)。
|
||||
@@ -145,7 +145,7 @@ export function listBlocks(
|
||||
else {
|
||||
const blocks = toBlocks(item, first)
|
||||
const [head] = blocks
|
||||
if (head?.kind === 'function' && doc) {
|
||||
if ((head?.kind === 'function' || head?.kind === 'class') && doc) {
|
||||
blocks[0] = documented(head, doc)
|
||||
doc = undefined
|
||||
}
|
||||
|
||||
@@ -424,3 +424,61 @@ describe('typeScript 属性表信息(facts)', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 类', () => {
|
||||
it('classes.ts:紧贴的 JSDoc(隔着装饰器也算)算进类头部;类表达式赋给变量、类字段也是类块;函数里的类照常成块', async () => {
|
||||
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
|
||||
{ at: 1, kind: 'import' },
|
||||
{ at: [3, 9], kind: 'class', kids: [
|
||||
{ at: 10, kind: 'comment' },
|
||||
{ at: 11, kind: 'field' },
|
||||
{ at: 13, kind: 'function' },
|
||||
{ at: 15, kind: 'class', kids: [{ at: 16, kind: 'function' }] },
|
||||
] },
|
||||
{ at: 20, kind: 'class', kids: [{ at: 21, kind: 'field' }] },
|
||||
{ at: [24, 25], kind: 'class' },
|
||||
{ at: 27, kind: 'function', kids: [
|
||||
{ at: 28, kind: 'class' },
|
||||
{ at: 29, kind: 'return' },
|
||||
] },
|
||||
{ at: 32, kind: 'class' },
|
||||
])
|
||||
})
|
||||
|
||||
it('classes.ts:名称、说明、访问级别、抽象、装饰器、泛型参数、父类(泛型实参归它)、实现的接口', async () => {
|
||||
const found = await facts(readFixture('classes.ts.txt'))
|
||||
expect(found[3]).toEqual({
|
||||
name: 'Repository',
|
||||
doc: '/**\n * 仓库基类。\n *\n * 子类实现 `load`。\n */',
|
||||
access: 'public',
|
||||
abstract: true,
|
||||
decorators: ['@Injectable()'],
|
||||
typeParameters: ['K', 'T extends Entity = Entity'],
|
||||
bases: [{ value: 'Base', typeArguments: '<string>' }],
|
||||
implements: ['Loader<T>', 'Disposable'],
|
||||
})
|
||||
// 类字段的值是类表达式:名称取字段名,访问级别同类成员
|
||||
expect(found[15]).toEqual({ name: 'Query', access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [{ value: 'QueryBase' }], implements: [] })
|
||||
expect(found[20]).toEqual({ access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
// 赋给变量的类表达式:名称取变量名
|
||||
expect(found[24]).toMatchObject({ name: 'Mixed', doc: '/** 类表达式 */', access: 'public', bases: [{ value: 'mixin(A, B)' }] })
|
||||
expect(found[28]).toEqual({ name: 'Local', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
expect(found[32]).toMatchObject({ name: 'Plain', access: 'private', decorators: ['@sealed'] })
|
||||
})
|
||||
|
||||
it('装饰器写在 export 后面也算;export default class Foo 是公共;JavaScript 的父类直接跟在 extends 后面', async () => {
|
||||
const source = [
|
||||
'export @dec class A {}',
|
||||
'export default class B extends C {}',
|
||||
'const D = class {}',
|
||||
].join('\n')
|
||||
expect(await facts(source)).toEqual({
|
||||
1: { name: 'A', access: 'public', abstract: false, decorators: ['@dec'], typeParameters: [], bases: [], implements: [] },
|
||||
2: { name: 'B', access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [{ value: 'C' }], implements: [] },
|
||||
3: { name: 'D', access: 'private', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] },
|
||||
})
|
||||
const js = await facts('class A extends mixin(B) {}\nexport default class extends C {}', 'javascript')
|
||||
expect(js[1]).toMatchObject({ name: 'A', bases: [{ value: 'mixin(B)' }], implements: [] })
|
||||
expect(js[2]).toMatchObject({ access: 'public', bases: [{ value: 'C' }] })
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, 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, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
|
||||
@@ -98,7 +98,7 @@ function exportBlocks(node: Node, top: boolean): CodeBlock[] {
|
||||
return declarationBlocks(declaration, node, top)
|
||||
const value = node.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(value, node)]
|
||||
return [classBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
return [leafBlock('statement', node)]
|
||||
@@ -120,18 +120,20 @@ function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBl
|
||||
return [leafBlock('type', outer)]
|
||||
default: {
|
||||
if (CLASSES.has(declaration.type))
|
||||
return [classBlock(declaration, outer)]
|
||||
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;否则模块顶层的 const → const,其余 → variable。 */
|
||||
/** 唯一声明值是带语句块主体的函数 → 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'
|
||||
@@ -276,29 +278,68 @@ function caseBlock(node: Node): CodeBlock {
|
||||
return { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
|
||||
}
|
||||
|
||||
/** 类:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
|
||||
function classBlock(declaration: Node, outer: Node): CodeBlock {
|
||||
/**
|
||||
* 类块: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 leafBlock('class', outer)
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 类的装饰器写在 export 前面时挂在 export 语句上,写在 class 前面时挂在类声明上;
|
||||
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式。
|
||||
*/
|
||||
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[]): ClassFacts {
|
||||
const holders = sameNode(outer, declaration) ? [declaration] : [outer, declaration]
|
||||
const heritage = syntaxChildren(named(declaration).find(child => child.type === 'class_heritage'))
|
||||
return {
|
||||
kind: 'class',
|
||||
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) => {
|
||||
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)
|
||||
name: name ?? undefined,
|
||||
access,
|
||||
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) => {
|
||||
if (part.type === 'extends_clause')
|
||||
return extendsBases(part)
|
||||
return part.type === 'implements_clause' ? [] : [{ value: part }]
|
||||
}),
|
||||
implements: heritage.filter(part => part.type === 'implements_clause').flatMap(syntaxChildren),
|
||||
}
|
||||
}
|
||||
|
||||
/** extends 后面的一串:每个表达式是一项,跟在它后面的泛型实参 `<T>` 归它。 */
|
||||
function extendsBases(clause: Node): BaseFacts[] {
|
||||
const bases: BaseFacts[] = []
|
||||
for (const part of syntaxChildren(clause)) {
|
||||
const last = bases.at(-1)
|
||||
if (part.type === 'type_arguments' && last)
|
||||
last.typeArguments = part
|
||||
else
|
||||
bases.push({ value: part })
|
||||
}
|
||||
return bases
|
||||
}
|
||||
|
||||
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
|
||||
switch (member.type) {
|
||||
case 'method_definition':
|
||||
@@ -309,6 +350,8 @@ function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
|
||||
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)]
|
||||
@@ -385,7 +428,8 @@ function nameRole(leaf: Node): TokenRole {
|
||||
return 'function'
|
||||
if (parent.type === 'member_expression' && sameNode(parent.childForFieldName('property'), leaf) && isCallee(parent))
|
||||
return 'function'
|
||||
if (parent.type === 'new_expression')
|
||||
// 父类 `extends Base`:TypeScript 在 extends_clause 里,JavaScript 直接在 class_heritage 里
|
||||
if (parent.type === 'new_expression' || parent.type === 'extends_clause' || parent.type === 'class_heritage')
|
||||
return 'type'
|
||||
if (parent.type === 'variable_declarator' && sameNode(parent.childForFieldName('name'), leaf) && functionBody(parent.childForFieldName('value')))
|
||||
return 'function'
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('typeScript 中文直译', () => {
|
||||
'k = [\'a\'] as const',
|
||||
'for (const key in obj) {}',
|
||||
'has = \'k\' in obj',
|
||||
'class A extends B implements C {}',
|
||||
'use(class A extends B implements C {})',
|
||||
'interface I extends J, K {}',
|
||||
'type F<T extends X> = (t: T) => void',
|
||||
'type R<T> = T extends string ? 1 : 2',
|
||||
@@ -191,7 +191,7 @@ describe('typeScript 中文直译', () => {
|
||||
expect(map[4]).toBe('k = [\'a\'] 断言为 常量')
|
||||
expect(map[5]).toBe('遍历 (常量 key 取自 obj)')
|
||||
expect(map[6]).toBe('has = \'k\' 在 obj')
|
||||
expect(map[7]).toBe('类 A 继承 B 实现 C')
|
||||
expect(map[7]).toBe('use(类 A 继承 B 实现 C {})')
|
||||
expect(map[8]).toBe('接口 I 继承 J, K')
|
||||
expect(map[9]).toBe('类型 F<T 限定为 X> = (t: T) => 无')
|
||||
expect(map[10]).toBe('类型 R<T> = T 符合 字符串 ? 1 : 2')
|
||||
@@ -244,7 +244,6 @@ describe('python 中文直译', () => {
|
||||
28: '最终',
|
||||
29: 'skipped.clear()',
|
||||
30: '返回 found',
|
||||
33: '类 EnvLockResult',
|
||||
34: 'total: 整数',
|
||||
35: 'ok: 列表[字符串] = 空列表',
|
||||
36: 'failed: 字典[字符串, 字符串] = 空字典',
|
||||
@@ -390,6 +389,7 @@ describe('属性表', () => {
|
||||
|
||||
const sample = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('sample.py')))
|
||||
expect(sample[10]).toEqual(['名称: find_env_files', '说明: Return every .env file under root.', '访问级别: 公共', '异步: 否', '输入参数: 1 个', ' root | 字符串 | ', '输出参数: 列表[字符串]'])
|
||||
expect(sample[33]).toEqual(['名称: EnvLockResult', '访问级别: 公共'])
|
||||
const port = rowTexts(await describeAll(py, 'python', pythonZhTranslator, '_default_port = 5680 if _role == "beat" else 5679'))
|
||||
expect(port[1]).toEqual(['名称: _default_port', '访问级别: 私有', '初始值: 如果 _role 等于 "beat" 则 5680 否则 5679'])
|
||||
})
|
||||
@@ -555,10 +555,58 @@ describe('属性表', () => {
|
||||
1: ['名称: 匿名', '访问级别: 公共', '异步: 是', '输入参数: 无', '输出参数: 未标注'],
|
||||
2: ['名称: f', '访问级别: 私有', '异步: 否', '输入参数: 2 个', ' b? | 数值 | ', ' c | 未标注 | 默认 1', '输出参数: 无'],
|
||||
3: ['名称: inner', '异步: 否', '输入参数: 无', '输出参数: 未标注'],
|
||||
5: ['名称: K', '访问级别: 私有', '抽象: 否'],
|
||||
6: ['名称: load', '访问级别: 受保护', '异步: 否', '装饰器: @memo()', '输入参数: 无', '输出参数: x is Foo'],
|
||||
})
|
||||
})
|
||||
|
||||
it('类:说明、抽象(只有 TypeScript 有)、装饰器、类型参数、父类、实现的接口有才显示;父类的泛型实参、类型照常翻', async () => {
|
||||
const tsRows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt')))
|
||||
expect(tsRows[3]).toEqual([
|
||||
'名称: Repository',
|
||||
'说明: 仓库基类。\n\n子类实现 `load`。',
|
||||
'访问级别: 公共',
|
||||
'抽象: 是',
|
||||
'装饰器: @Injectable()',
|
||||
'类型参数: K, T 限定为 Entity = Entity',
|
||||
'父类: Base<字符串>',
|
||||
'实现的接口: Loader<T>, Disposable',
|
||||
])
|
||||
expect(tsRows[15]).toEqual(['名称: Query', '访问级别: 公共', '抽象: 否', '父类: QueryBase'])
|
||||
expect(tsRows[20]).toEqual(['名称: 匿名', '访问级别: 公共', '抽象: 否'])
|
||||
expect(tsRows[24]).toEqual(['名称: Mixed', '说明: 类表达式', '访问级别: 公共', '抽象: 否', '父类: mixin(A, B)'])
|
||||
expect(tsRows[28]).toEqual(['名称: Local', '抽象: 否'])
|
||||
expect(tsRows[32]).toEqual(['名称: Plain', '访问级别: 私有', '抽象: 否', '装饰器: @sealed'])
|
||||
|
||||
const pyRows = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('classes.py')))
|
||||
expect(pyRows[6]).toEqual(['名称: AppException', '说明: 应用异常的基类。\n\n带上 HTTP 状态码。', '访问级别: 公共', '父类: Exception'])
|
||||
expect(pyRows[18]).toEqual(['名称: NotFound', '说明: 找不到资源。', '访问级别: 公共', '父类: AppException'])
|
||||
expect(pyRows[24]).toEqual(['名称: Meta', '访问级别: 公共', '装饰器: @dataclass(frozen=True)', '父类: Base, metaclass=ABCMeta'])
|
||||
expect(pyRows[29]).toEqual(['名称: _Hidden', '访问级别: 私有'])
|
||||
expect(pyRows[38]).toEqual(['名称: Local', '说明: 局部类。'])
|
||||
})
|
||||
|
||||
it('类的着色:名称、父类名按类型,说明是 Markdown,抽象按 storage', async () => {
|
||||
const tsRows = colors((await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt'))).get(3) ?? [])
|
||||
expect(tsRows[0].value).toEqual([{ text: 'Repository', role: 'type' }])
|
||||
expect(tsRows[1].value).toEqual({ markdown: '仓库基类。\n\n子类实现 `load`。' })
|
||||
expect(tsRows[3].value).toEqual([{ text: '是', role: 'storage' }])
|
||||
expect(valueOf(tsRows, '父类')).toEqual([
|
||||
{ text: 'Base', role: 'type' },
|
||||
{ text: '<', role: 'plain' },
|
||||
{ text: '字符串', role: 'type' },
|
||||
{ text: '>', role: 'plain' },
|
||||
])
|
||||
const pyRows = colors((await describeAll(py, 'python', pythonZhTranslator, readFixture('classes.py'))).get(24) ?? [])
|
||||
expect(valueOf(pyRows, '父类')).toEqual([
|
||||
{ text: 'Base', role: 'type' },
|
||||
{ text: ', ', role: 'plain' },
|
||||
{ text: 'metaclass', role: 'variable' },
|
||||
{ text: '=', role: 'plain' },
|
||||
{ text: 'ABCMeta', role: 'variable' },
|
||||
])
|
||||
})
|
||||
|
||||
it('变量:一条语句多个变量各占一组,没初始值写无,带语句块主体的回调只翻到主体开始之前', async () => {
|
||||
const source = [
|
||||
'let i = 0, n: number',
|
||||
@@ -675,12 +723,35 @@ describe('源码下标(跳到定义用)', () => {
|
||||
expect(marked(tsSource, tsRows.get(3))).toEqual(['名称: i', '名称: j'])
|
||||
})
|
||||
|
||||
it('类:名称、类型参数、父类、实现的接口里的名字带下标;只有「类型参数」一行标出它讲的定义(这几个泛型参数),名称是块自己的名字、不标', async () => {
|
||||
const source = 'export class Repo<K, T extends Entity> extends Base<T> implements Loader<K>, Disposable {}'
|
||||
const at = (word: string): number => source.indexOf(word)
|
||||
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
const found = rows.get(1) ?? []
|
||||
expect(linked(valueOf(found, '名称'))).toEqual([['Repo', at('Repo')]])
|
||||
expect(linked(valueOf(found, '类型参数'))).toEqual([['K', at('K,')], ['T', at('T extends')], ['Entity', at('Entity')]])
|
||||
expect(linked(valueOf(found, '父类'))).toEqual([['Base', at('Base')], ['T', at('T> implements')]])
|
||||
expect(linked(valueOf(found, '实现的接口'))).toEqual([['Loader', at('Loader')], ['K', at('K>,')], ['Disposable', at('Disposable')]])
|
||||
expect(found.filter(row => row.defines).map(row => [row.label, source.slice(row.defines?.startIndex, row.defines?.endIndex)])).toEqual([['类型参数', 'K, T extends Entity']])
|
||||
|
||||
const pySource = 'class NotFound(errors.AppException, metaclass=ABCMeta):\n pass'
|
||||
const pyRows = await describeAll(py, 'python', pythonZhTranslator, pySource)
|
||||
expect(linked(valueOf(pyRows.get(1), '父类'))).toEqual([
|
||||
['errors', pySource.indexOf('errors')],
|
||||
['AppException', pySource.indexOf('AppException')],
|
||||
['metaclass', pySource.indexOf('metaclass')],
|
||||
['ABCMeta', pySource.indexOf('ABCMeta')],
|
||||
])
|
||||
})
|
||||
|
||||
it('样例全文:带下标的只有照抄的名字和字符串,下标处的源码就是这段;变量名、函数名都带,译词、说明词都不带', async () => {
|
||||
const samples: [LanguageAdapter, string, Translator, string][] = [
|
||||
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'issueLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'classes.ts.txt'],
|
||||
[py, 'python', pythonZhTranslator, 'sample.py'],
|
||||
[py, 'python', pythonZhTranslator, 'routes.py'],
|
||||
[py, 'python', pythonZhTranslator, 'classes.py'],
|
||||
]
|
||||
for (const [adapter, languageId, translator, name] of samples) {
|
||||
const source = readFixture(name)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, ClassFacts, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -307,6 +307,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
return functionRows(block.facts, phrases)
|
||||
case 'class':
|
||||
return classRows(block.facts, phrases)
|
||||
case 'import':
|
||||
return importRows(block.facts, phrases)
|
||||
default:
|
||||
@@ -361,9 +363,9 @@ interface Phrases {
|
||||
verbatim: (node: Node) => Segment[]
|
||||
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
|
||||
module: (node: Node) => Segment[]
|
||||
/** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */
|
||||
/** 源码里的一个名字原样照抄、按 role 着色(函数、类的名称)。 */
|
||||
name: (node: Node, role: TokenRole) => Segment[]
|
||||
/** 函数的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||
doc: (node: Node) => Markdown | undefined
|
||||
}
|
||||
|
||||
@@ -414,16 +416,14 @@ function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segmen
|
||||
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
|
||||
}
|
||||
|
||||
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
/** 函数:名称 / 说明 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
const decorators = facts.decorators.map(say.verbatim)
|
||||
const doc = facts.doc && say.doc(facts.doc)
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||||
...(doc ? [{ label: '说明', value: doc }] : []),
|
||||
...docRows(facts.doc, say),
|
||||
...accessRows(facts.access),
|
||||
{ label: '异步', value: yesNo(facts.async) },
|
||||
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
|
||||
...decoratorRows(facts.decorators, say),
|
||||
{
|
||||
label: '输入参数',
|
||||
value: count(facts.parameters.length),
|
||||
@@ -440,6 +440,36 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
|
||||
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
|
||||
*/
|
||||
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
||||
const listRow = (label: string, parts: Segment[][], defines: Defines = {}): PropertyRow[] =>
|
||||
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||||
...docRows(facts.doc, say),
|
||||
...accessRows(facts.access),
|
||||
...(facts.abstract === undefined ? [] : [{ label: '抽象', value: yesNo(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.implements.map(say.translated)),
|
||||
]
|
||||
}
|
||||
|
||||
/** 函数、类的说明:有文字才有这一行。 */
|
||||
function docRows(doc: Node | undefined, say: Phrases): PropertyRow[] {
|
||||
const markdown = doc && say.doc(doc)
|
||||
return markdown ? [{ label: '说明', value: markdown }] : []
|
||||
}
|
||||
|
||||
/** 装饰器:源码原样照抄,多个换行。 */
|
||||
function decoratorRows(decorators: Node[], say: Phrases): PropertyRow[] {
|
||||
return decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators.map(say.verbatim), '\n') }] : []
|
||||
}
|
||||
|
||||
/** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */
|
||||
function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
const itemText = (item: ImportItem): Segment[] => {
|
||||
|
||||
@@ -62,7 +62,7 @@ export function BlockLevel({ blocks, kind, tree }: { blocks: BlockView[], kind?:
|
||||
)
|
||||
}
|
||||
|
||||
/** 预览状态的内容:函数、导入、常量、变量块是属性表,其余是一行译文。 */
|
||||
/** 预览状态的内容:函数、类、导入、常量、变量块是属性表,其余是一行译文。 */
|
||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||
|
||||
@@ -39,7 +39,7 @@ function PropertyItems({ rowId, items }: { rowId: string, items: PropertyItem[]
|
||||
)
|
||||
}
|
||||
|
||||
/** 函数、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项下面展开子行。 */
|
||||
/** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项下面展开子行。 */
|
||||
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
blockId: string
|
||||
rows: PropertyRow[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
/**
|
||||
* 注释、函数说明的 Markdown 排版:CommonMark 加 GFM 的表格、删除线,带 http(s):// 的网址和邮箱自动成链接。
|
||||
* 注释、函数和类说明的 Markdown 排版:CommonMark 加 GFM 的表格、删除线,带 http(s):// 的网址和邮箱自动成链接。
|
||||
* 源码里的 HTML 当文字显示;链接只认 http / https / mailto,别的写法照原文显示——
|
||||
* webview 会拦下 <a> 的点击交给 VS Code 在外部打开。图片不渲染:webview 的 CSP 不放行外部图片。
|
||||
*/
|
||||
|
||||
@@ -245,6 +245,28 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
expect(await at('line of')).toEqual({ plan: { id: '1.0', expand: ['1'] }, flashed: 'for (const line of lines) {' })
|
||||
})
|
||||
|
||||
it('类:类名是块自己的名字闪整块,泛型参数只闪「类型参数」一行;类默认折叠,跳到类里的方法、参数要先展开它', async () => {
|
||||
const source = [
|
||||
'export class Repo<T> extends Base {',
|
||||
' load(id: T) {',
|
||||
' return id',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n')
|
||||
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, source, needle)
|
||||
expect(await at('Repo')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'export class Repo<T> extends Base {' })
|
||||
expect(await at('T>')).toEqual({ plan: { id: '0', row: '0/p3', expand: [] }, flashed: '类型参数: T' })
|
||||
expect(await at('load')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'load(id: T) {' })
|
||||
expect(await at('id: T')).toEqual({ plan: { id: '0.0', row: '0.0/p3/0', expand: ['0', '0.0/p3'] }, flashed: 'id | T | ' })
|
||||
const views = await withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, source, ts, typescriptZhTranslator))
|
||||
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
|
||||
|
||||
const pySource = ['class NotFound(AppException):', ' """找不到。"""', ' def run(self):', ' pass'].join('\n')
|
||||
const atPy = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, pySource, needle)
|
||||
expect(await atPy('NotFound')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'class NotFound(AppException):' })
|
||||
expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' })
|
||||
})
|
||||
|
||||
it('typeScript:翻译插件没标 defines 时闪整块', async () => {
|
||||
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', withoutDefines(typescriptZhTranslator), TS, needle)
|
||||
expect(await at('path: string')).toEqual({ plan: { id: '1', expand: ['1/p3'] }, flashed: 'export function load(path: string, { encoding }: Options): string {' })
|
||||
|
||||
@@ -37,7 +37,7 @@ function shows(text: PreviewText, offset: number): boolean {
|
||||
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、只声明一个变量时的变量名)→ 整块;
|
||||
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个)→ 只闪它,子行连它所在的行一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开那一行。
|
||||
* 不在这块里是 undefined。
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
/** 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块 */
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明) */
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明) */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/** 源码里一段的下标,end 为开区间 */
|
||||
@@ -53,12 +53,12 @@ interface BlockViewBase {
|
||||
code: string
|
||||
/** 源码着色(拼起来等于 code) */
|
||||
codeTokens: Segment[]
|
||||
/** 块自己的名字(函数名、只声明一个变量时的变量名)在源码里的范围:跳到的定义就是它时闪整块 */
|
||||
/** 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围:跳到的定义就是它时闪整块 */
|
||||
defines?: SourceRange
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文 */
|
||||
/** 函数、类、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文 */
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
@@ -76,12 +76,12 @@ describe('webview 状态', () => {
|
||||
})
|
||||
|
||||
describe('眼下折叠着哪些', () => {
|
||||
it('没点过:函数块(含方法、嵌套函数)折叠,其余块和属性行展开', () => {
|
||||
expect([...collapsedIds(BLOCKS, {})].sort()).toEqual(['1', '1.0.0', '2.0'])
|
||||
it('没点过:函数块(含方法、嵌套函数)、类块折叠,其余块和属性行展开', () => {
|
||||
expect([...collapsedIds(BLOCKS, {})].sort()).toEqual(['1', '1.0.0', '2', '2.0'])
|
||||
})
|
||||
|
||||
it('点过的按用户的:展开的函数保持展开,折叠的其他块、属性行保持折叠', () => {
|
||||
const folds = { '1': false, '2': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
||||
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '2', '2.0'])
|
||||
it('点过的按用户的:展开的函数、类保持展开,折叠的其他块、属性行保持折叠', () => {
|
||||
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
||||
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
|
||||
})
|
||||
})
|
||||
@@ -54,9 +54,9 @@ function readViewState(saved: unknown): ViewState {
|
||||
return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds }
|
||||
}
|
||||
|
||||
/** 块默认折叠吗:函数块收起主体(属性表照常显示),其余块展开。属性行默认都展开。 */
|
||||
/** 块默认折叠吗:函数、类块收起主体(属性表照常显示),其余块展开。属性行默认都展开。 */
|
||||
export function foldedByDefault(kind: BlockKind): boolean {
|
||||
return kind === 'function'
|
||||
return kind === 'function' || kind === 'class'
|
||||
}
|
||||
|
||||
/** 眼下折叠着的块和属性行:用户点过的按他点的,没点过的按 foldedByDefault。 */
|
||||
|
||||
Reference in new issue
Block a user