✨ feat(vscode): 代码阅读函数、导入、常量、变量块预览改成属性表
This commit is contained in:
1 parent
0c4c9b0fba
commit
dd9b39ac5d
19 files changed
+1458
-213
No files matched your search
@@ -1,9 +1,9 @@
|
||||
import type { LanguageAdapter, Segment, Translator } from '../contract'
|
||||
import type { LanguageAdapter, PropertyRow, Segment, Translator } from '../contract'
|
||||
import type { ZhTable } from './zhTemplate'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||
import { readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { propertyLines, readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from './pythonZh'
|
||||
import { typescriptZhTranslator } from './typescriptZh'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
@@ -13,19 +13,40 @@ const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
interface Translated { kind: string, segments: Segment[] }
|
||||
|
||||
/** 每块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。 */
|
||||
/** 一行译文块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, Translated>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (!byLine.has(line))
|
||||
if (!byLine.has(line) && !('facts' in block))
|
||||
byLine.set(line, { kind: block.kind, segments: translator.translate(block, source) })
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
}
|
||||
|
||||
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引。 */
|
||||
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, PropertyRow[]>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, PropertyRow[]>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (!byLine.has(line) && 'facts' in block)
|
||||
byLine.set(line, translator.describe(block, source))
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
}
|
||||
|
||||
function rowTexts(map: Map<number, PropertyRow[]>): Record<number, string[]> {
|
||||
return Object.fromEntries([...map].map(([line, rows]) => [line, propertyLines(rows)]))
|
||||
}
|
||||
|
||||
function valueOf(rows: PropertyRow[] | undefined, label: string): Segment[] {
|
||||
return rows?.find(row => row.label === label)?.value ?? []
|
||||
}
|
||||
|
||||
function text(segments: Segment[] | undefined): string {
|
||||
return (segments ?? []).map(segment => segment.text).join('')
|
||||
}
|
||||
@@ -60,29 +81,22 @@ describe('typeScript 中文直译', () => {
|
||||
it('spec 样例逐字一致', async () => {
|
||||
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
|
||||
expect(text(env.get(23)?.segments)).toBe('公共 异步 函数 findEnvFiles(workspaceRoot: 字符串): Promise<字符串[]>')
|
||||
expect(text(env.get(38)?.segments)).toBe('异步 函数 chmodAll(workspaceRoot: 字符串, mode: 数值): Promise<EnvLockResult>')
|
||||
expect(text(env.get(12)?.segments)).toBe('导入 { promises 作为 fsp } 来自 \'node:fs\'')
|
||||
expect(text(env.get(25)?.segments)).toBe('如果 (非 folder)')
|
||||
expect(text(env.get(26)?.segments)).toBe('返回 空数组')
|
||||
expect(text(env.get(41)?.segments)).toBe('遍历 (常量 f 取自 files)')
|
||||
expect(text(env.get(35)?.segments)).toBe('failed: { path: 字符串, reason: 字符串 }[]')
|
||||
expect(text(env.get(53)?.segments)).toBe('Set every `.env*` file in the workspace to 0o444 (read-only).')
|
||||
expect(text(issue.get(14)?.segments)).toBe('常量 issue = issues.find(i => i.number 全等 issueNumber)')
|
||||
expect(text(issue.get(20)?.segments)).toBe('返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }')
|
||||
})
|
||||
|
||||
it('其余块:结构照抄,复合语句去掉结尾的 {,注释去掉注释符号', async () => {
|
||||
const env = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
|
||||
const issue = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
|
||||
expect(env[24]).toBe('常量 folder = workspace.workspaceFolders?.find(f => f.uri.fsPath 全等 workspaceRoot)')
|
||||
expect(env[28]).toBe('常量 uris = 等待 workspace.findFiles(include, EXCLUDE_PATTERN)')
|
||||
expect(env[32]).toBe('公共 接口 EnvLockResult')
|
||||
expect(env[42]).toBe('尝试')
|
||||
expect(env[46]).toBe('捕获 (err)')
|
||||
expect(env[47]).toBe('result.failed.push({ path: f, reason: 如果 err 属于 Error 则 err.message 否则 String(err) })')
|
||||
expect(env[1]).toBe('.env file lock / unlock helpers. Recursively scans the main workspace folder for `.env*` files (skipping `node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or 0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`. No persistent watcher — re-scan happens on every toggle, so newly created `.env` files are only picked up the next time the user clicks the button.')
|
||||
expect(issue[1]).toBe('导入 类型 { Issue } 来自 \'../gitea/types\'')
|
||||
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
|
||||
expect(issue[9]).toBe('纯判定:工单被未完成的前置挡住才算锁定。快照里找不到工单或前置一律放行, 与 webview 的 `isIssueLocked` 保持同一语义。')
|
||||
expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)')
|
||||
@@ -91,13 +105,12 @@ describe('typeScript 中文直译', () => {
|
||||
it('每个被翻的词单独成段,着色取语法树词性', async () => {
|
||||
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
|
||||
expect(env.get(23)?.segments.filter(segment => segment.role !== 'plain').slice(0, 4)).toEqual([
|
||||
expect(env.get(32)?.segments.filter(segment => segment.role !== 'plain')).toEqual([
|
||||
{ text: '公共', role: 'keyword' },
|
||||
{ text: '异步', role: 'storage' },
|
||||
{ text: '函数', role: 'storage' },
|
||||
{ text: 'findEnvFiles', role: 'function' },
|
||||
{ text: '接口', role: 'storage' },
|
||||
{ text: 'EnvLockResult', role: 'type' },
|
||||
])
|
||||
expect(issue.get(14)?.segments).toContainEqual({ text: '全等', role: 'operator' })
|
||||
expect(issue.get(15)?.segments).toContainEqual({ text: '全等', role: 'operator' })
|
||||
expect(env.get(26)?.segments).toEqual([{ text: '返回', role: 'keyword' }, { text: ' ', role: 'plain' }, { text: '空数组', role: 'literal' }])
|
||||
expectWordsApart(env)
|
||||
expectWordsApart(issue)
|
||||
@@ -105,16 +118,18 @@ describe('typeScript 中文直译', () => {
|
||||
|
||||
it('属性名、对象键、非空断言不被误翻', async () => {
|
||||
const source = [
|
||||
'const o = { if: 1, return: i.number, string: x!.type, new: a.default }',
|
||||
'let n = a !== b && !c || d == e != f',
|
||||
'o = { if: 1, return: i.number, string: x!.type, new: a.default }',
|
||||
'n = a !== b && !c || d == e != f',
|
||||
'type S = string | void',
|
||||
// eslint-disable-next-line no-template-curly-in-string -- 被翻译的源码文本里本来就有模板字符串
|
||||
'const s: string | void = typeof v === \'string\' ? `${v}` : \'number\'',
|
||||
's = typeof v === \'string\' ? `${v}` : \'number\'',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
|
||||
expect(map[1]).toBe('常量 o = { if: 1, return: i.number, string: x!.type, new: a.default }')
|
||||
expect(map[2]).toBe('变量 n = a 不全等 b 并且 非 c 或者 d 等于 e 不等于 f')
|
||||
expect(map[1]).toBe('o = { if: 1, return: i.number, string: x!.type, new: a.default }')
|
||||
expect(map[2]).toBe('n = a 不全等 b 并且 非 c 或者 d 等于 e 不等于 f')
|
||||
expect(map[3]).toBe('类型 S = 字符串 | 无')
|
||||
// eslint-disable-next-line no-template-curly-in-string -- 模板字符串原样照抄进译文
|
||||
expect(map[3]).toBe('常量 s: 字符串 | 无 = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'')
|
||||
expect(map[4]).toBe('s = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'')
|
||||
})
|
||||
|
||||
it('逻辑与、或译成「并且」「或者」', async () => {
|
||||
@@ -133,34 +148,34 @@ describe('typeScript 中文直译', () => {
|
||||
|
||||
it('as / in / extends / default 按所在语法节点换译法', async () => {
|
||||
const source = [
|
||||
'import * as path from \'node:path\'',
|
||||
'export * as path from \'node:path\'',
|
||||
'export { run as start } from \'./run\'',
|
||||
'const v = x as string',
|
||||
'const k = [\'a\'] as const',
|
||||
'v = x as string',
|
||||
'k = [\'a\'] as const',
|
||||
'for (const key in obj) {}',
|
||||
'const has = \'k\' in obj',
|
||||
'has = \'k\' in obj',
|
||||
'class A extends B implements C {}',
|
||||
'interface I extends J, K {}',
|
||||
'function f<T extends X>(t: T) {}',
|
||||
'type F<T extends X> = (t: T) => void',
|
||||
'type R<T> = T extends string ? 1 : 2',
|
||||
'export default function g() {}',
|
||||
'export default g',
|
||||
'switch (x) {',
|
||||
' default:',
|
||||
' break',
|
||||
'}',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
|
||||
expect(map[1]).toBe('导入 * 作为 path 来自 \'node:path\'')
|
||||
expect(map[1]).toBe('公共 * 作为 path 来自 \'node:path\'')
|
||||
expect(map[2]).toBe('公共 { run 作为 start } 来自 \'./run\'')
|
||||
expect(map[3]).toBe('常量 v = x 断言为 字符串')
|
||||
expect(map[4]).toBe('常量 k = [\'a\'] 断言为 常量')
|
||||
expect(map[3]).toBe('v = x 断言为 字符串')
|
||||
expect(map[4]).toBe('k = [\'a\'] 断言为 常量')
|
||||
expect(map[5]).toBe('遍历 (常量 key 取自 obj)')
|
||||
expect(map[6]).toBe('常量 has = \'k\' 在 obj')
|
||||
expect(map[6]).toBe('has = \'k\' 在 obj')
|
||||
expect(map[7]).toBe('类 A 继承 B 实现 C')
|
||||
expect(map[8]).toBe('接口 I 继承 J, K')
|
||||
expect(map[9]).toBe('函数 f<T 限定为 X>(t: T)')
|
||||
expect(map[9]).toBe('类型 F<T 限定为 X> = (t: T) => 无')
|
||||
expect(map[10]).toBe('类型 R<T> = T 符合 字符串 ? 1 : 2')
|
||||
expect(map[11]).toBe('公共 默认 函数 g()')
|
||||
expect(map[11]).toBe('公共 默认 g')
|
||||
expect(map[13]).toBe('默认情况')
|
||||
})
|
||||
|
||||
@@ -168,15 +183,15 @@ describe('typeScript 中文直译', () => {
|
||||
const source = [
|
||||
'err instanceof Error ? err.message : String(err)',
|
||||
'z = x ? (y ? 1 : 2) : 3',
|
||||
'const w = a ? b : c ? d : e',
|
||||
'const v = a !== b && !c ? typeof d : await e',
|
||||
'w = a ? b : c ? d : e',
|
||||
'v = a !== b && !c ? typeof d : await e',
|
||||
].join('\n')
|
||||
const map = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '如果 err 属于 Error 则 err.message 否则 String(err)',
|
||||
2: 'z = 如果 x 则 (如果 y 则 1 否则 2) 否则 3',
|
||||
3: '常量 w = 如果 a 则 b 否则 如果 c 则 d 否则 e',
|
||||
4: '常量 v = 如果 a 不全等 b 并且 非 c 则 类型为 d 否则 等待 e',
|
||||
3: 'w = 如果 a 则 b 否则 如果 c 则 d 否则 e',
|
||||
4: 'v = 如果 a 不全等 b 并且 非 c 则 类型为 d 否则 等待 e',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword')).toEqual([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
@@ -192,18 +207,11 @@ describe('python 中文直译', () => {
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, readFixture('sample.py'))
|
||||
expect(texts(map)).toEqual({
|
||||
1: 'Scan a workspace for .env files.',
|
||||
3: '导入 os',
|
||||
4: '从 pathlib 导入 Path 作为 P',
|
||||
6: 'MAX_DEPTH = 8',
|
||||
7: 'skipped = 空列表',
|
||||
10: '函数 find_env_files(root: 字符串) -> 列表[字符串]',
|
||||
11: 'Return every .env file under root.',
|
||||
12: '跳过备份文件',
|
||||
13: 'found = [p 遍历 p 取自 os.listdir(root) 如果 p.startswith(\'.env\') 并且 非 p.endswith(\'.bak\')]',
|
||||
14: '如果 非 found',
|
||||
15: '返回 空列表',
|
||||
16: '否则如果 len(found) > MAX_DEPTH',
|
||||
17: 'found = found[:MAX_DEPTH]',
|
||||
18: '否则',
|
||||
19: '跳过',
|
||||
20: '遍历 name 取自 found',
|
||||
@@ -221,8 +229,6 @@ describe('python 中文直译', () => {
|
||||
34: 'total: 整数',
|
||||
35: 'ok: 列表[字符串] = 空列表',
|
||||
36: 'failed: 字典[字符串, 字符串] = 空字典',
|
||||
37: 'label = \'env\'',
|
||||
39: '函数 summary(self, verbose: 布尔 = 假) -> 字符串',
|
||||
40: '返回 如果 verbose 不是 空 则 f\'{self.total} files\' 否则 \'\'',
|
||||
})
|
||||
expectWordsApart(map)
|
||||
@@ -233,16 +239,23 @@ describe('python 中文直译', () => {
|
||||
'def pick(items: dict[str, int], list: set) -> tuple:',
|
||||
' return [str(k) for k in items if k not in list]',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
|
||||
expect(map[1]).toBe('函数 pick(items: 字典[字符串, 整数], list: 集合) -> 元组')
|
||||
expect(map[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
|
||||
expect(rowTexts(await describeAll(py, 'python', pythonZhTranslator, source))[1]).toEqual([
|
||||
'名称: pick',
|
||||
'访问级别: 公共',
|
||||
'异步: 否',
|
||||
'输入参数: 2 个',
|
||||
' items | 字典[字符串, 整数] | ',
|
||||
' list | 集合 | ',
|
||||
'输出参数: 元组',
|
||||
])
|
||||
expect(texts(await translateAll(py, 'python', pythonZhTranslator, source))[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
|
||||
})
|
||||
|
||||
it('逻辑与、或译成「并且」「或者」', async () => {
|
||||
expect(texts(await translateAll(py, 'python', pythonZhTranslator, 'a and b or c'))).toEqual({ 1: 'a 并且 b 或者 c' })
|
||||
})
|
||||
|
||||
it('from:导入里「从」,raise … from 里「源自」,yield from 里「自」', async () => {
|
||||
it('from:raise … from 里「源自」,yield from 里「自」', async () => {
|
||||
const source = [
|
||||
'from typing import Annotated',
|
||||
'def walk(nodes):',
|
||||
@@ -250,7 +263,6 @@ describe('python 中文直译', () => {
|
||||
' raise RuntimeError(str(err)) from err',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
|
||||
expect(map[1]).toBe('从 typing 导入 Annotated')
|
||||
expect(map[3]).toBe('产出 自 nodes')
|
||||
expect(map[4]).toBe('抛出 RuntimeError(str(err)) 源自 err')
|
||||
})
|
||||
@@ -289,20 +301,166 @@ describe('python 中文直译', () => {
|
||||
'z = (a if x else b) if y else c',
|
||||
'v = [k for k in items if k not in seen] if not done and ok else None',
|
||||
].join('\n')
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '_default_port = 如果 _role 等于 "beat" 则 5680 否则 5679',
|
||||
2: 'y = 如果 x 则 a 否则 如果 y 则 b 否则 c',
|
||||
3: 'z = 如果 y 则 (如果 x 则 a 否则 b) 否则 c',
|
||||
4: 'v = 如果 非 done 并且 ok 则 [k 遍历 k 取自 items 如果 k 不在 seen] 否则 空',
|
||||
const map = await describeAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(Object.fromEntries([...map].map(([line, rows]) => [line, text(valueOf(rows, '初始值'))]))).toEqual({
|
||||
1: '如果 _role 等于 "beat" 则 5680 否则 5679',
|
||||
2: '如果 x 则 a 否则 如果 y 则 b 否则 c',
|
||||
3: '如果 y 则 (如果 x 则 a 否则 b) 否则 c',
|
||||
4: '如果 非 done 并且 ok 则 [k 遍历 k 取自 items 如果 k 不在 seen] 否则 空',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
|
||||
expect(valueOf(map.get(1), '初始值').filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
{ text: '等于', role: 'operator' },
|
||||
{ text: '则', role: 'keyword' },
|
||||
{ text: '否则', role: 'keyword' },
|
||||
])
|
||||
expectWordsApart(map)
|
||||
})
|
||||
})
|
||||
|
||||
describe('属性表', () => {
|
||||
it('spec 样例逐字一致', async () => {
|
||||
const env = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
|
||||
const issue = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
|
||||
const routes = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('routes.py')))
|
||||
expect(env[23]).toEqual([
|
||||
'名称: findEnvFiles',
|
||||
'访问级别: 公共',
|
||||
'异步: 是',
|
||||
'输入参数: 1 个',
|
||||
' workspaceRoot | 字符串 | ',
|
||||
'输出参数: Promise<字符串[]>',
|
||||
])
|
||||
expect(env[38]).toEqual([
|
||||
'名称: chmodAll',
|
||||
'访问级别: 私有',
|
||||
'异步: 是',
|
||||
'输入参数: 2 个',
|
||||
' workspaceRoot | 字符串 | ',
|
||||
' mode | 数值 | ',
|
||||
'输出参数: Promise<EnvLockResult>',
|
||||
])
|
||||
expect(env[12]).toEqual(['来源: \'node:fs\'', '仅类型: 否', '导入项: 1 个', ' promises 作为 fsp'])
|
||||
expect(env[15]).toEqual(['名称: ENV_PATTERN', '访问级别: 私有', '值: \'**/.env*\''])
|
||||
expect(env[27]).toEqual(['名称: include', '初始值: 新建 RelativePattern(folder, ENV_PATTERN)'])
|
||||
expect(env[28]).toEqual(['名称: uris', '初始值: 等待 workspace.findFiles(include, EXCLUDE_PATTERN)'])
|
||||
expect(env[40]).toEqual(['名称: result', '类型: EnvLockResult', '初始值: { total: files.length, ok: 空数组, failed: 空数组 }'])
|
||||
expect(issue[1]).toEqual(['来源: \'../gitea/types\'', '仅类型: 是', '导入项: 1 个', ' Issue'])
|
||||
expect(issue[14]).toEqual(['名称: issue', '初始值: issues.find(i => i.number 全等 issueNumber)'])
|
||||
expect(routes[19]).toEqual([
|
||||
'来源: .schemas',
|
||||
'导入项: 3 个',
|
||||
' UserAvatarUploadResponse',
|
||||
' UserDetailResponse',
|
||||
' UserUpdateRequest',
|
||||
])
|
||||
expect(routes[29]).toEqual([
|
||||
'名称: get_user',
|
||||
'访问级别: 公共',
|
||||
'异步: 是',
|
||||
'装饰器: @router.get("/{user_id}", response_model=UserDetailResponse)',
|
||||
'输入参数: 3 个',
|
||||
' user_id | Int4Path | ',
|
||||
' db | AsyncSession | 默认 Depends(get_db)',
|
||||
' current_user | User | 默认 Depends(get_current_user)',
|
||||
'输出参数: UserDetailResponse',
|
||||
])
|
||||
expect(routes[111]).toContain(' file | Annotated[UploadFile, File(..., description="头像文件")] | ')
|
||||
expect(routes[118]).toEqual(['名称: payload', '初始值: 等待 file.read()'])
|
||||
|
||||
const sample = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('sample.py')))
|
||||
expect(sample[10]).toEqual(['名称: find_env_files', '访问级别: 公共', '异步: 否', '输入参数: 1 个', ' root | 字符串 | ', '输出参数: 列表[字符串]'])
|
||||
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'])
|
||||
})
|
||||
|
||||
it('属性名是纯文字;值按词性着色,说明词(未标注、N 个、默认)用 hint', async () => {
|
||||
const env = await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
const rows = env.get(23) ?? []
|
||||
expect(rows.map(row => row.label)).toEqual(['名称', '访问级别', '异步', '输入参数', '输出参数'])
|
||||
expect(rows[0].value).toEqual([{ text: 'findEnvFiles', role: 'function' }])
|
||||
expect(rows[1].value).toEqual([{ text: '公共', role: 'keyword' }])
|
||||
expect(rows[2].value).toEqual([{ text: '是', role: 'storage' }])
|
||||
expect(rows[3].value).toEqual([{ text: '1 个', role: 'hint' }])
|
||||
expect(rows[3].items).toEqual([{ cells: [[{ text: 'workspaceRoot', role: 'variable' }], [{ text: '字符串', role: 'type' }], []] }])
|
||||
expect(rows[4].value).toEqual([
|
||||
{ text: 'Promise', role: 'type' },
|
||||
{ text: '<', role: 'plain' },
|
||||
{ text: '字符串', role: 'type' },
|
||||
{ text: '[]>', role: 'plain' },
|
||||
])
|
||||
expect(valueOf(env.get(12), '来源')).toEqual([{ text: '\'node:fs\'', role: 'string' }])
|
||||
expect(env.get(12)?.at(-1)?.items?.[0].cells[0]).toEqual([
|
||||
{ text: 'promises', role: 'variable' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '作为', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'fsp', role: 'variable' },
|
||||
])
|
||||
|
||||
const sample = await describeAll(py, 'python', pythonZhTranslator, readFixture('sample.py'))
|
||||
expect(valueOf(sample.get(3), '来源')).toEqual([{ text: 'os', role: 'type' }])
|
||||
expect(sample.get(39)?.find(row => row.label === '输入参数')?.items).toEqual([
|
||||
{ cells: [[{ text: 'self', role: 'variable' }], [{ text: '未标注', role: 'hint' }], []] },
|
||||
{ cells: [[{ text: 'verbose', role: 'variable' }], [{ text: '布尔', role: 'type' }], [{ text: '默认 ', role: 'hint' }, { text: '假', role: 'storage' }]] },
|
||||
])
|
||||
})
|
||||
|
||||
it('导入:默认导出、全部、整个模块、副作用导入;Python 没有「仅类型」', async () => {
|
||||
const tsSource = [
|
||||
'import d, { b as c } from \'y\'',
|
||||
'import * as ns from \'z\'',
|
||||
'import \'./side\'',
|
||||
'import fs = require(\'fs\')',
|
||||
].join('\n')
|
||||
expect(rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({
|
||||
1: ['来源: \'y\'', '仅类型: 否', '导入项: 2 个', ' 默认导出 作为 d', ' b 作为 c'],
|
||||
2: ['来源: \'z\'', '仅类型: 否', '导入项: 1 个', ' 全部 作为 ns'],
|
||||
3: ['来源: \'./side\'', '仅类型: 否', '导入项: 无'],
|
||||
4: ['来源: \'fs\'', '仅类型: 否', '导入项: 1 个', ' 整个模块 作为 fs'],
|
||||
})
|
||||
const pySource = [
|
||||
'import numpy.linalg as la',
|
||||
'import os, sys as system',
|
||||
'from m import *',
|
||||
].join('\n')
|
||||
expect(rowTexts(await describeAll(py, 'python', pythonZhTranslator, pySource))).toEqual({
|
||||
1: ['来源: numpy.linalg', '导入项: 1 个', ' 整个模块 作为 la'],
|
||||
2: ['来源: os, sys', '导入项: 2 个', ' os', ' sys 作为 system'],
|
||||
3: ['来源: m', '导入项: 1 个', ' 全部'],
|
||||
})
|
||||
})
|
||||
|
||||
it('函数:匿名、可省略参数、嵌套函数没有访问级别、类成员受保护、装饰器照抄源码', async () => {
|
||||
const source = [
|
||||
'export default async function () {}',
|
||||
'function f(b?: number, c = 1): void {',
|
||||
' function inner() {}',
|
||||
'}',
|
||||
'class K {',
|
||||
' @memo()',
|
||||
' protected load(): x is Foo {}',
|
||||
'}',
|
||||
].join('\n')
|
||||
expect(rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, source))).toEqual({
|
||||
1: ['名称: 匿名', '访问级别: 公共', '异步: 是', '输入参数: 无', '输出参数: 未标注'],
|
||||
2: ['名称: f', '访问级别: 私有', '异步: 否', '输入参数: 2 个', ' b? | 数值 | ', ' c | 未标注 | 默认 1', '输出参数: 无'],
|
||||
3: ['名称: inner', '异步: 否', '输入参数: 无', '输出参数: 未标注'],
|
||||
6: ['名称: load', '访问级别: 受保护', '异步: 否', '装饰器: @memo()', '输入参数: 无', '输出参数: x is Foo'],
|
||||
})
|
||||
})
|
||||
|
||||
it('变量:一条语句多个变量各占一组,没初始值写无,带语句块主体的回调只翻到主体开始之前', async () => {
|
||||
const source = [
|
||||
'let i = 0, n: number',
|
||||
'function f() {',
|
||||
' const v = useMemo(() => {',
|
||||
' return 1',
|
||||
' }, [])',
|
||||
'}',
|
||||
].join('\n')
|
||||
const rows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, source))
|
||||
expect(rows[1]).toEqual(['名称: i', '访问级别: 私有', '初始值: 0', '名称: n', '访问级别: 私有', '类型: 数值', '初始值: 无'])
|
||||
expect(rows[3]).toEqual(['名称: v', '初始值: useMemo(() =>'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -339,7 +497,7 @@ describe('中文模板引擎', () => {
|
||||
' if k in seen:',
|
||||
' pass',
|
||||
' else:',
|
||||
' x = a if c else b',
|
||||
' print(a if c else b)',
|
||||
'else:',
|
||||
' pass',
|
||||
].join('\n')
|
||||
@@ -347,7 +505,7 @@ describe('中文模板引擎', () => {
|
||||
expect(map[1]).toBe('for k 乙 items')
|
||||
expect(map[2]).toBe('if k 甲 seen')
|
||||
expect(map[4]).toBe('丁')
|
||||
expect(map[5]).toBe('x = a if c 丙 b')
|
||||
expect(map[5]).toBe('print(a if c 丙 b)')
|
||||
expect(map[6]).toBe('戊')
|
||||
})
|
||||
|
||||
@@ -359,41 +517,53 @@ describe('中文模板引擎', () => {
|
||||
operatorParents: ['comparison_operator'],
|
||||
nodeTemplates: {
|
||||
conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }],
|
||||
assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
|
||||
augmented_assignment: ['令', { field: 'left' }, '加上', { field: 'right' }],
|
||||
},
|
||||
})
|
||||
const source = [
|
||||
'n: int = a if k in seen else b',
|
||||
'n += a if k in seen else b',
|
||||
'print(a if c else b if d else e)',
|
||||
'w = (a # 注释不占序号',
|
||||
' if k == 1 else b)',
|
||||
'print((a # 注释不占序号',
|
||||
' if k == 1 else b))',
|
||||
].join('\n')
|
||||
const map = await translateAll(py, 'python', translator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '令 n 类型 int 为 若 k 在 seen 则 a 不然 b',
|
||||
1: '令 n 加上 若 k 在 seen 则 a 不然 b',
|
||||
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
|
||||
3: 'w = (若 k 等于 1 则 a 不然 b)',
|
||||
3: 'print((若 k 等于 1 则 a 不然 b))',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword').map(segment => segment.text))
|
||||
.toEqual(['令', '类型', '为', '若', '则', '不然'])
|
||||
.toEqual(['令', '加上', '若', '则', '不然'])
|
||||
})
|
||||
|
||||
it('属性表里的类型、值用同一张表和同一套模板翻', async () => {
|
||||
const translator = testTranslator({
|
||||
tokenRole: () => 'variable',
|
||||
keywords: { in: '在' },
|
||||
builtinTypes: { int: '整型' },
|
||||
isTypePosition: leaf => leaf.parent?.type === 'type',
|
||||
nodeTemplates: { conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }] },
|
||||
})
|
||||
const rows = await describeAll(py, 'python', translator, 'n: int = a if k in seen else b')
|
||||
expect(rowTexts(rows)[1]).toEqual(['名称: n', '访问级别: 公共', '类型: 整型', '初始值: 若 k 在 seen 则 a 不然 b'])
|
||||
})
|
||||
|
||||
it('没命中模板的节点照抄源码结构;模板引用的子节点不存在时也不重排', async () => {
|
||||
const translator = testTranslator({
|
||||
keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } },
|
||||
nodeTemplates: {
|
||||
assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
|
||||
augmented_assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
|
||||
},
|
||||
})
|
||||
const source = [
|
||||
'x = a if k in seen else b',
|
||||
'x += a if k in seen else b',
|
||||
'if k in seen:',
|
||||
' pass',
|
||||
'else:',
|
||||
' pass',
|
||||
].join('\n')
|
||||
expect(texts(await translateAll(py, 'python', translator, source))).toEqual({
|
||||
1: 'x = a if k 在 seen 丙 b',
|
||||
1: 'x += a if k 在 seen 丙 b',
|
||||
2: 'if k 在 seen',
|
||||
3: 'pass',
|
||||
4: '丁',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CodeBlock, Node, Segment, SourceSpan, TokenRole, Translator } from '../contract'
|
||||
import type { Access, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
export interface ContextualWord {
|
||||
@@ -210,7 +210,37 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
|
||||
function translateComment(block: CodeBlock): Segment[] {
|
||||
/** 按译文顺序收集 node 落在 span 里的词。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, source: string): Token[] {
|
||||
const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false }
|
||||
collect(node, span, out)
|
||||
return out.tokens
|
||||
}
|
||||
|
||||
/** node 的源码原样照抄:叶子按源码着色,叶子之间的空白、标点照抄。 */
|
||||
function verbatim(node: Node, source: string): Segment[] {
|
||||
const segments: Segment[] = []
|
||||
let cursor = node.startIndex
|
||||
const visit = (current: Node): void => {
|
||||
if (current.startIndex === current.endIndex)
|
||||
return
|
||||
if (current.childCount > 0) {
|
||||
for (const child of current.children) {
|
||||
if (child)
|
||||
visit(child)
|
||||
}
|
||||
return
|
||||
}
|
||||
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain')
|
||||
pushSegment(segments, current.text, table.tokenRole(current))
|
||||
cursor = current.endIndex
|
||||
}
|
||||
visit(node)
|
||||
pushSegment(segments, source.slice(cursor, node.endIndex), 'plain')
|
||||
return segments
|
||||
}
|
||||
|
||||
function translateComment(block: TextBlock): Segment[] {
|
||||
const { startIndex, endIndex } = block.header
|
||||
const root = block.node.tree.rootNode
|
||||
const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node]
|
||||
@@ -218,41 +248,169 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return text ? [{ text, role: 'comment' }] : []
|
||||
}
|
||||
|
||||
function translate(block: CodeBlock, source: string): Segment[] {
|
||||
function translate(block: TextBlock, source: string): Segment[] {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block)
|
||||
const out: Collector = { source, tokens: [], cursor: block.header.startIndex, gap: false }
|
||||
collect(block.node, block.header, out)
|
||||
const { tokens } = out
|
||||
const tokens = tokensIn(block.node, block.header, source)
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
const last = tokens.at(-1)
|
||||
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
|
||||
tokens.pop()
|
||||
|
||||
const segments: Segment[] = []
|
||||
const push = (text: string, role: TokenRole): void => {
|
||||
const prev = segments.at(-1)
|
||||
if (role === 'plain' && prev?.role === 'plain')
|
||||
prev.text += text
|
||||
else
|
||||
segments.push({ text, role })
|
||||
}
|
||||
let gap = false
|
||||
tokens.forEach((token, i) => {
|
||||
gap ||= token.gap
|
||||
const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i))
|
||||
if (token.text === '' || trailingComma) {
|
||||
gap ||= token.role === 'comment'
|
||||
return
|
||||
}
|
||||
if (gap && segments.length > 0)
|
||||
push(' ', 'plain')
|
||||
push(token.text, token.role)
|
||||
// 「非」是个词,后面补一个空格,不和操作数粘在一起
|
||||
gap = token.translated && token.raw === '!'
|
||||
})
|
||||
return segments
|
||||
return joinTokens(tokens)
|
||||
}
|
||||
|
||||
return { id: table.id, label: table.label, languageIds: table.languageIds, translate }
|
||||
function describe(block: FactBlock, source: string): PropertyRow[] {
|
||||
const { header } = block
|
||||
const phrases: Phrases = {
|
||||
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
|
||||
translated: (node) => {
|
||||
const clipped = node.endIndex > header.endIndex
|
||||
const span = {
|
||||
startIndex: node.startIndex,
|
||||
endIndex: clipped ? header.endIndex : node.endIndex,
|
||||
startRow: node.startPosition.row,
|
||||
endRow: clipped ? header.endRow : node.endPosition.row,
|
||||
}
|
||||
return joinTokens(tokensIn(node, span, source))
|
||||
},
|
||||
verbatim: node => verbatim(node, source),
|
||||
module: node => [{ text: node.text, role: stringNodes.has(node.type) ? 'string' : 'type' }],
|
||||
}
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
return functionRows(block.facts, phrases)
|
||||
case 'import':
|
||||
return importRows(block.facts, phrases)
|
||||
default:
|
||||
return variableRows(block, phrases)
|
||||
}
|
||||
}
|
||||
|
||||
return { id: table.id, label: table.label, languageIds: table.languageIds, translate, describe }
|
||||
}
|
||||
|
||||
function pushSegment(segments: Segment[], text: string, role: TokenRole): void {
|
||||
if (!text)
|
||||
return
|
||||
const prev = segments.at(-1)
|
||||
if (role === 'plain' && prev?.role === 'plain')
|
||||
prev.text += text
|
||||
else
|
||||
segments.push({ text, role })
|
||||
}
|
||||
|
||||
/** 词连成译文:源码里隔着空白的词之间空一格,注释不进译文,右括号前的尾逗号去掉。 */
|
||||
function joinTokens(tokens: Token[]): Segment[] {
|
||||
const segments: Segment[] = []
|
||||
let gap = false
|
||||
tokens.forEach((token, i) => {
|
||||
gap ||= token.gap
|
||||
const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i))
|
||||
if (token.text === '' || trailingComma) {
|
||||
gap ||= token.role === 'comment'
|
||||
return
|
||||
}
|
||||
if (gap && segments.length > 0)
|
||||
pushSegment(segments, ' ', 'plain')
|
||||
pushSegment(segments, token.text, token.role)
|
||||
// 「非」是个词,后面补一个空格,不和操作数粘在一起
|
||||
gap = token.translated && token.raw === '!'
|
||||
})
|
||||
return segments
|
||||
}
|
||||
|
||||
// ─── 属性表 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 属性表里值的三种来源。 */
|
||||
interface Phrases {
|
||||
/** 按表翻译,只翻落在块头部里的那一段。 */
|
||||
translated: (node: Node) => Segment[]
|
||||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||||
verbatim: (node: Node) => Segment[]
|
||||
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
|
||||
module: (node: Node) => Segment[]
|
||||
}
|
||||
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
|
||||
function hint(text: string): Segment {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function yesNo(value: boolean): Segment[] {
|
||||
return [{ text: value ? '是' : '否', role: 'storage' }]
|
||||
}
|
||||
|
||||
function count(n: number): Segment[] {
|
||||
return [hint(n > 0 ? `${n} 个` : '无')]
|
||||
}
|
||||
|
||||
function joined(parts: Segment[][], separator: string): Segment[] {
|
||||
return parts.flatMap((part, i) => i > 0 ? [{ text: separator, role: 'plain' as const }, ...part] : part)
|
||||
}
|
||||
|
||||
function accessRows(access: Access | undefined): PropertyRow[] {
|
||||
return access ? [{ label: '访问级别', value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : []
|
||||
}
|
||||
|
||||
function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segment[] {
|
||||
if (!alias)
|
||||
return item
|
||||
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)
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? [{ text: facts.name.text, role: 'function' }] : [hint('匿名')] },
|
||||
...accessRows(facts.access),
|
||||
{ label: '异步', value: yesNo(facts.async) },
|
||||
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
|
||||
{
|
||||
label: '输入参数',
|
||||
value: count(facts.parameters.length),
|
||||
items: facts.parameters.map(param => ({
|
||||
cells: [
|
||||
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
|
||||
param.type ? say.translated(param.type) : [hint('未标注')],
|
||||
param.default ? [hint('默认 '), ...say.translated(param.default)] : [],
|
||||
],
|
||||
})),
|
||||
},
|
||||
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
|
||||
]
|
||||
}
|
||||
|
||||
/** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */
|
||||
function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
const itemText = (item: ImportItem): Segment[] => {
|
||||
switch (item.kind) {
|
||||
case 'named':
|
||||
return aliased(say.translated(item.name), item.alias, say)
|
||||
case 'default':
|
||||
return aliased([hint('默认导出')], item.alias, say)
|
||||
case 'namespace':
|
||||
return aliased([hint('全部')], item.alias, say)
|
||||
case 'module':
|
||||
// 一条语句导入多个模块时(Python 的 import a, b)要写出是哪个模块
|
||||
return aliased(facts.sources.length > 1 ? say.module(item.module) : [hint('整个模块')], item.alias, say)
|
||||
}
|
||||
}
|
||||
return [
|
||||
{ label: '来源', value: facts.sources.length > 0 ? joined(facts.sources.map(say.module), ', ') : [hint('无')] },
|
||||
...(facts.typeOnly === undefined ? [] : [{ label: '仅类型', value: yesNo(facts.typeOnly) }]),
|
||||
{ label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)] })) },
|
||||
]
|
||||
}
|
||||
|
||||
/** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值)。 */
|
||||
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
const valueLabel = block.kind === 'const' ? '值' : '初始值'
|
||||
return block.facts.declarators.flatMap(declarator => [
|
||||
{ label: '名称', value: say.translated(declarator.name) },
|
||||
...accessRows(block.facts.access),
|
||||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||||
{ label: valueLabel, value: declarator.value ? say.translated(declarator.value) : [hint('无')] },
|
||||
])
|
||||
}
|
||||
Reference in new issue
Block a user