✨ feat(vscode): 代码阅读把推导式换成按读的顺序重排的遍历小框
This commit is contained in:
1 parent
677fb7da1f
commit
6416cdf714
16 files changed
+641
-91
No files matched your search
@@ -1,9 +1,9 @@
|
||||
import type { LanguageAdapter, PropertyItem, PropertyRow, Segment, Translator } from '../contract'
|
||||
import type { Inline, LanguageAdapter, PropertyItem, 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 { propertyLines, readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { flatSegments, propertyLines, readFixture, segmentText, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from './pythonZh'
|
||||
import { typescriptZhTranslator } from './typescriptZh'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
@@ -12,7 +12,7 @@ const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
/** 一块的译文:一行译文在 segments 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||
interface Translated { kind: string, segments: Segment[], markdown?: string }
|
||||
interface Translated { kind: string, segments: Inline[], markdown?: string }
|
||||
|
||||
/** 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
||||
@@ -48,15 +48,21 @@ 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[] {
|
||||
function valueOf(rows: PropertyRow[] | undefined, label: string): Inline[] {
|
||||
const value = rows?.find(row => row.label === label)?.value ?? []
|
||||
if ('markdown' in value)
|
||||
throw new Error(`「${label}」是 Markdown`)
|
||||
return value
|
||||
}
|
||||
|
||||
function text(segments: Segment[] | undefined): string {
|
||||
return (segments ?? []).map(segment => segment.text).join('')
|
||||
/** 译文写成文字,框写成 `⟦框头|框里的文字⟧`。 */
|
||||
function text(segments: Inline[] | undefined): string {
|
||||
return segmentText(segments ?? [])
|
||||
}
|
||||
|
||||
/** 带下标的段(框里的也算),按出现顺序写成 [文字, 下标]。 */
|
||||
function linked(segments: Inline[] | undefined): [string, number][] {
|
||||
return flatSegments(segments ?? []).flatMap(segment => segment.offset === undefined ? [] : [[segment.text, segment.offset] as [string, number]])
|
||||
}
|
||||
|
||||
function texts(map: Map<number, Translated>): Record<number, string> {
|
||||
@@ -72,14 +78,15 @@ const CJK = /\p{Script=Han}/u
|
||||
const CJK_ONLY = /^\p{Script=Han}+$/u
|
||||
const WORD_CHAR = /[\p{Script=Han}\w]/u
|
||||
|
||||
/** 被翻的词单独成段,而且不和前后的字母、数字、汉字粘在一起。 */
|
||||
/** 被翻的词单独成段,而且不和前后的字母、数字、汉字粘在一起;框里的词和框前后的词也一样。 */
|
||||
function expectWordsApart(map: Map<number, Translated>): void {
|
||||
for (const [line, { kind, segments }] of map) {
|
||||
if (kind === 'comment')
|
||||
continue
|
||||
const full = text(segments)
|
||||
const flat = flatSegments(segments)
|
||||
const full = flat.map(segment => segment.text).join('')
|
||||
let at = 0
|
||||
for (const segment of segments) {
|
||||
for (const segment of flat) {
|
||||
if (segment.role !== 'string' && CJK.test(segment.text)) {
|
||||
expect(segment.text, `L${line} ${full}`).toMatch(CJK_ONLY)
|
||||
for (const neighbor of [full[at - 1], full[at + segment.text.length]])
|
||||
@@ -125,7 +132,7 @@ 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(colors(env.get(32)?.segments.filter(segment => segment.role !== 'plain'))).toEqual([
|
||||
expect(colors(flatSegments(env.get(32)?.segments ?? []).filter(segment => segment.role !== 'plain'))).toEqual([
|
||||
{ text: '公共', role: 'keyword' },
|
||||
{ text: '接口', role: 'storage' },
|
||||
{ text: 'EnvLockResult', role: 'type' },
|
||||
@@ -213,7 +220,7 @@ describe('typeScript 中文直译', () => {
|
||||
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([
|
||||
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword')).toEqual([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
{ text: '则', role: 'keyword' },
|
||||
{ text: '否则', role: 'keyword' },
|
||||
@@ -252,7 +259,7 @@ describe('python 中文直译', () => {
|
||||
expectWordsApart(map)
|
||||
})
|
||||
|
||||
it('内置类型只在注解里翻,in 按位置区分取自 / 在', async () => {
|
||||
it('内置类型只在注解里翻;比较里的 not in 是「不在」,推导式里也是', async () => {
|
||||
const source = [
|
||||
'def pick(items: dict[str, int], list: set) -> tuple:',
|
||||
' return [str(k) for k in items if k not in list]',
|
||||
@@ -266,7 +273,7 @@ describe('python 中文直译', () => {
|
||||
' list | 集合 | ',
|
||||
'输出参数: 元组',
|
||||
])
|
||||
expect(texts(await translateAll(py, 'python', pythonZhTranslator, source))[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
|
||||
expect(texts(await translateAll(py, 'python', pythonZhTranslator, source))[2]).toBe('返回 ⟦列表|遍历 items 的每个 k,仅当 k 不在 list,取 str(k)⟧')
|
||||
})
|
||||
|
||||
it('逻辑与、或译成「并且」「或者」', async () => {
|
||||
@@ -324,9 +331,9 @@ describe('python 中文直译', () => {
|
||||
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] 否则 空',
|
||||
4: '如果 非 done 并且 ok 则 ⟦列表|遍历 items 的每个 k,仅当 k 不在 seen,取 k⟧ 否则 空',
|
||||
})
|
||||
expect(valueOf(map.get(1), '初始值').filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
|
||||
expect(flatSegments(valueOf(map.get(1), '初始值')).filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
{ text: '等于', role: 'operator' },
|
||||
{ text: '则', role: 'keyword' },
|
||||
@@ -335,6 +342,123 @@ describe('python 中文直译', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 写在表达式里的遍历(推导式)', () => {
|
||||
it('spec 样例逐字一致:原地换成一个框,框里按读的顺序重排;调用和生成器共用的括号写在框外', async () => {
|
||||
const source = [
|
||||
'def invite_code(length):',
|
||||
' return "".join(secrets.choice(ALPHABET) for _ in range(length))',
|
||||
'def active_ids(users):',
|
||||
' return [u.id for u in users if u.is_active]',
|
||||
'def by_id(users):',
|
||||
' return {u.id: u for u in users}',
|
||||
].join('\n')
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
2: '返回 "".join(⟦生成器|遍历 range(length) 的每个 _,取 secrets.choice(ALPHABET)⟧)',
|
||||
4: '返回 ⟦列表|遍历 users 的每个 u,仅当 u.is_active,取 u.id⟧',
|
||||
6: '返回 ⟦字典|遍历 users 的每个 u,取 u.id: u⟧',
|
||||
})
|
||||
expectWordsApart(map)
|
||||
})
|
||||
|
||||
it('框是 iteration 样式、框头是结果类型;遍历 / 的每个 / 仅当 / 取 按关键字着色,框里的名字照常着色', async () => {
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, 'return [u.id for u in users if u.is_active]')
|
||||
expect(colors(map.get(1)?.segments)).toEqual([
|
||||
{ text: '返回', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{
|
||||
style: 'iteration',
|
||||
label: '列表',
|
||||
segments: [
|
||||
{ text: '遍历', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'users', role: 'variable' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '的每个', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'u', role: 'variable' },
|
||||
{ text: ',', role: 'plain' },
|
||||
{ text: '仅当', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'u', role: 'variable' },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'is_active', role: 'variable' },
|
||||
{ text: ',', role: 'plain' },
|
||||
{ text: '取', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'u', role: 'variable' },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'id', role: 'variable' },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('紧挨着的几节 for 用顿号接着写,条件在它的位置,异步的写「异步遍历」;集合、套着的框、别的参数旁边的生成器', async () => {
|
||||
const source = [
|
||||
'a = [c for xs in grid for c in xs]',
|
||||
'b = [c for row in grid if row for c in row if c]',
|
||||
'c = [v async for v in stream()]',
|
||||
'd = [(x, y) for x in xs async for y in ys]',
|
||||
'e = {n for n, _ in pairs}',
|
||||
'f = [[y for y in row] for row in grid]',
|
||||
'g = pick(a, (y for y in b))',
|
||||
'h = sum(',
|
||||
' n # 每一项',
|
||||
' for n in counts',
|
||||
')',
|
||||
].join('\n')
|
||||
const map = await describeAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(Object.fromEntries([...map].map(([line, rows]) => [line, text(valueOf(rows, '初始值'))]))).toEqual({
|
||||
1: '⟦列表|遍历 grid 的每个 xs、xs 的每个 c,取 c⟧',
|
||||
2: '⟦列表|遍历 grid 的每个 row,仅当 row,遍历 row 的每个 c,仅当 c,取 c⟧',
|
||||
3: '⟦列表|异步遍历 stream() 的每个 v,取 v⟧',
|
||||
4: '⟦列表|遍历 xs 的每个 x,异步遍历 ys 的每个 y,取 (x, y)⟧',
|
||||
5: '⟦集合|遍历 pairs 的每个 n, _,取 n⟧',
|
||||
6: '⟦列表|遍历 grid 的每个 row,取 ⟦列表|遍历 row 的每个 y,取 y⟧⟧',
|
||||
7: 'pick(a, ⟦生成器|遍历 b 的每个 y,取 y⟧)',
|
||||
8: 'sum(⟦生成器|遍历 counts 的每个 n,取 n⟧)',
|
||||
})
|
||||
const async = valueOf(map.get(3), '初始值')[0]
|
||||
expect('segments' in async && async.segments.filter(piece => 'role' in piece && piece.role === 'keyword')).toEqual([
|
||||
{ text: '异步遍历', role: 'keyword' },
|
||||
{ text: '的每个', role: 'keyword' },
|
||||
{ text: '取', role: 'keyword' },
|
||||
])
|
||||
})
|
||||
|
||||
it('属性表的值里也换:参数默认值、字典一项的值', async () => {
|
||||
const source = [
|
||||
'def f(ids=[u.id for u in users]): pass',
|
||||
'CONFIG = {"ids": {k: v for k, v in pairs}}',
|
||||
].join('\n')
|
||||
const rows = rowTexts(await describeAll(py, 'python', pythonZhTranslator, source))
|
||||
expect(rows[1]).toContain(' ids | 未标注 | 默认 ⟦列表|遍历 users 的每个 u,取 u.id⟧')
|
||||
expect(rows[2]).toEqual(['名称: CONFIG', '访问级别: 公共', '值: 字典 1 项', ' "ids" | ⟦字典|遍历 pairs 的每个 k, v,取 k: v⟧'])
|
||||
})
|
||||
|
||||
it('框里的名字带源码下标,和框外的一样指回源码原文', async () => {
|
||||
const source = 'return "".join(secrets.choice(ALPHABET) for _ in range(length))'
|
||||
const at = (word: string): number => source.indexOf(word)
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(linked(map.get(1)?.segments)).toEqual([
|
||||
['""', at('""')],
|
||||
['join', at('join')],
|
||||
['range', at('range')],
|
||||
['length', at('length')],
|
||||
['_', at('_')],
|
||||
['secrets', at('secrets')],
|
||||
['choice', at('choice')],
|
||||
['ALPHABET', at('ALPHABET')],
|
||||
])
|
||||
})
|
||||
|
||||
it('typeScript 没有这种写法,不认', async () => {
|
||||
const map = await translateAll(ts, 'typescript', typescriptZhTranslator, 'return items.map(x => x.id)')
|
||||
expect(text(map.get(1)?.segments)).toBe('返回 items.map(x => x.id)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('属性表', () => {
|
||||
it('spec 样例逐字一致', async () => {
|
||||
const env = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
|
||||
@@ -703,18 +827,13 @@ describe('属性表', () => {
|
||||
})
|
||||
|
||||
describe('源码下标(跳到定义用)', () => {
|
||||
/** 带下标的段,按出现顺序写成 [文字, 下标]。 */
|
||||
function linked(segments: Segment[] | undefined): [string, number][] {
|
||||
return (segments ?? []).flatMap(segment => segment.offset === undefined ? [] : [[segment.text, segment.offset] as [string, number]])
|
||||
}
|
||||
|
||||
function itemCells(rows: PropertyRow[] | undefined, label: string): Segment[][] {
|
||||
function itemCells(rows: PropertyRow[] | undefined, label: string): Inline[][] {
|
||||
return rows?.find(row => row.label === label)?.items?.flatMap(item => item.cells) ?? []
|
||||
}
|
||||
|
||||
function propertySegments(rows: PropertyRow[]): Segment[] {
|
||||
const itemSegments = (items: PropertyItem[] | undefined): Segment[] => (items ?? []).flatMap(item => [...item.cells.flat(), ...itemSegments(item.items)])
|
||||
return rows.flatMap(row => [...('markdown' in row.value ? [] : row.value), ...itemSegments(row.items)])
|
||||
const itemSegments = (items: PropertyItem[] | undefined): Segment[] => (items ?? []).flatMap(item => [...flatSegments(item.cells.flat()), ...itemSegments(item.items)])
|
||||
return rows.flatMap(row => [...('markdown' in row.value ? [] : flatSegments(row.value)), ...itemSegments(row.items)])
|
||||
}
|
||||
|
||||
it('python:点分模块名每一节、导入项、别名各自带下标,点号不带;前面有中文注释也对得上', async () => {
|
||||
@@ -867,7 +986,7 @@ describe('源码下标(跳到定义用)', () => {
|
||||
]
|
||||
for (const [adapter, languageId, translator, name] of samples) {
|
||||
const source = readFixture(name)
|
||||
const translated = [...(await translateAll(adapter, languageId, translator, source)).values()].flatMap(block => block.segments)
|
||||
const translated = [...(await translateAll(adapter, languageId, translator, source)).values()].flatMap(block => flatSegments(block.segments))
|
||||
const described = [...(await describeAll(adapter, languageId, translator, source)).values()].flatMap(propertySegments)
|
||||
const segments = [...translated, ...described]
|
||||
expect(linked(segments).length, name).toBeGreaterThan(10)
|
||||
@@ -956,7 +1075,7 @@ describe('中文模板引擎', () => {
|
||||
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
|
||||
3: 'print((若 k 等于 1 则 a 不然 b))',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword').map(segment => segment.text))
|
||||
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword').map(segment => segment.text))
|
||||
.toEqual(['令', '加上', '若', '则', '不然'])
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user