481 lines
28 KiB
TypeScript
481 lines
28 KiB
TypeScript
import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract'
|
||
import type { RevealPlan } from './reveal'
|
||
import type { BlockKind, BlockView, Inline, Occurrence, PropertyItem, PropertyRow, Segment } from './types'
|
||
import { describe, expect, it } from 'vitest'
|
||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||
import { createTypeScriptAdapter } from '../../../src/codeReader/languages/typescript'
|
||
import { sqlalchemyRule } from '../../../src/codeReader/rules/sqlalchemy'
|
||
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
|
||
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
|
||
import { typescriptZhTranslator } from '../../../src/codeReader/translators/typescriptZh'
|
||
import { occurrencePlan, revealPlan, scrollTopToShow } from './reveal'
|
||
import { collapsedIds, propertyItemId, propertyRowId } from './viewState'
|
||
|
||
function name(text: string, offset: number): Segment {
|
||
return { text, role: 'variable', offset }
|
||
}
|
||
|
||
function word(text: string): Segment {
|
||
return { text, role: 'keyword' }
|
||
}
|
||
|
||
function textBlock(id: string, kind: BlockKind, startLine: number, translation: Inline[], children: BlockView[] = []): BlockView {
|
||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation }
|
||
}
|
||
|
||
function factBlock(id: string, kind: BlockKind, startLine: number, properties: PropertyRow[], children: BlockView[] = []): BlockView {
|
||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties }
|
||
}
|
||
|
||
/**
|
||
* 0 from luffy_agent.core.db import get_db
|
||
* 2 def load(path, user):
|
||
* 3 if not user:
|
||
* 4 return get_db(path)
|
||
* 8 LIMIT = 3
|
||
*/
|
||
const BLOCKS: BlockView[] = [
|
||
factBlock('0', 'import', 0, [
|
||
{ label: '来源', value: [name('luffy_agent', 5), { text: '.', role: 'type' }, name('core', 17)] },
|
||
{ label: '导入项', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('get_db', 34)]] }] },
|
||
]),
|
||
factBlock('1', 'function', 2, [
|
||
{ label: '名称', value: [{ text: 'load', role: 'function', offset: 46 }] },
|
||
{ label: '异步', value: [{ text: '否', role: 'storage' }] },
|
||
{ label: '输入参数', value: [{ text: '2 个', role: 'hint' }], items: [{ cells: [[name('path', 51)], []] }, { cells: [[name('user', 57)], []] }] },
|
||
], [
|
||
textBlock('1.0', 'if', 3, [word('如果'), word('非'), name('user', 78)], [
|
||
textBlock('1.0.0', 'return', 4, [word('返回'), name('get_db', 102), name('path', 109)]),
|
||
]),
|
||
]),
|
||
factBlock('2', 'const', 8, [{ label: '名称', value: [name('LIMIT', 130)] }]),
|
||
]
|
||
|
||
describe('用键盘跳到高亮的一处出现:滚到哪块、展开哪些', () => {
|
||
const nowhere = (): boolean => false
|
||
const at = (start: number, end: number): Occurrence => ({ start, end, kind: 'read' })
|
||
|
||
it('在嵌套块的译文里:目标是那块,展开它所有的祖先块', () => {
|
||
expect(occurrencePlan(BLOCKS, at(102, 108), nowhere)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||
})
|
||
|
||
it('在属性行展开的子行里(导入项、参数):连那一行一起展开;在属性行的值里不用展开', () => {
|
||
expect(occurrencePlan(BLOCKS, at(34, 40), nowhere)).toEqual({ id: '0', expand: ['0/p1'] })
|
||
expect(occurrencePlan(BLOCKS, at(51, 55), nowhere)).toEqual({ id: '1', expand: ['1/p2'] })
|
||
expect(occurrencePlan(BLOCKS, at(130, 135), nowhere)).toEqual({ id: '2', expand: [] })
|
||
})
|
||
|
||
it('和段重叠就算(点分模块名的一节、比名字长的段);页面上哪儿都没有时没有', () => {
|
||
expect(occurrencePlan(BLOCKS, at(18, 20), nowhere)).toEqual({ id: '0', expand: [] })
|
||
expect(occurrencePlan(BLOCKS, at(62, 70), nowhere)).toBeUndefined()
|
||
})
|
||
|
||
it('被筛掉的、正在编辑的、被盖住的块里的不算;只跳过那一块,它下面画出来的子块照常算', () => {
|
||
const hidden = new Set(['1.0', '1.0.0'])
|
||
expect(occurrencePlan(BLOCKS, at(102, 108), id => hidden.has(id))).toBeUndefined()
|
||
expect(occurrencePlan(BLOCKS, at(51, 55), id => id === '1')).toBeUndefined()
|
||
expect(occurrencePlan(BLOCKS, at(102, 108), id => id === '1')).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||
})
|
||
|
||
it('同一处在页面上显示几次:取页面上的第一处;Markdown 里的不算', () => {
|
||
const blocks: BlockView[] = [
|
||
factBlock('0', 'function', 0, [{ label: '说明', value: { markdown: '见 `get_db`' } }], [
|
||
textBlock('0.0', 'return', 1, [word('返回'), name('get_db', 30)]),
|
||
]),
|
||
factBlock('1', 'class', 2, [{ label: '字段', value: [{ text: '1 个', role: 'hint' }], columns: ['字段'], items: [{ cells: [[name('get_db', 30)]] }] }]),
|
||
]
|
||
expect(occurrencePlan(blocks, at(30, 36), nowhere)).toEqual({ id: '0.0', expand: ['0'] })
|
||
expect(occurrencePlan(blocks, at(30, 36), id => id === '0.0')).toEqual({ id: '1', expand: ['1/p0'] })
|
||
})
|
||
})
|
||
|
||
describe('跳到定义:滚到哪块、展开哪些', () => {
|
||
it('名字在嵌套块的译文里:目标是那块,展开它所有的祖先块', () => {
|
||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||
})
|
||
|
||
it('名字在一行译文下面那张表里(跨几行写的调用的实参):表总是展开的,目标是那块,不用展开什么', () => {
|
||
const call: BlockView = {
|
||
...textBlock('0.0', 'return', 1, [word('返回'), word('新建'), name('UserDetailResponse', 10)]),
|
||
table: { columns: ['参数', '值'], items: [{ cells: [[name('id', 40)], [name('user', 43), { text: '.', role: 'plain' }, name('id', 48)]] }] },
|
||
}
|
||
const blocks = [textBlock('0', 'if', 0, [word('如果'), name('user', 3)], [call])]
|
||
expect(revealPlan(blocks, 9, 48)).toEqual({ id: '0.0', expand: ['0'] })
|
||
expect(occurrencePlan(blocks, { start: 40, end: 42, kind: 'read' }, () => false)).toEqual({ id: '0.0', expand: ['0'] })
|
||
})
|
||
|
||
it('名字在值那一格里套着的实参表里(套了几层都算):一行译文下面的不用展开,变量初始值那一行下面的展开那一行', () => {
|
||
const leaf = { columns: ['参数', '值'], items: [{ cells: [[name('mp_app_secret', 70)], [name('secret', 90)]] }] }
|
||
const nested = { columns: ['参数', '值'], items: [{ cells: [[name('credentials', 50)], [word('新建'), name('CredentialPresence', 62)]], table: { columns: ['参数', '值'], items: [{ cells: [[name('inner', 66)], [word('新建')]], table: leaf }] } }] }
|
||
const blocks: BlockView[] = [
|
||
{ ...textBlock('0', 'return', 0, [word('返回'), word('新建'), name('Diagnostic', 10)]), table: nested },
|
||
factBlock('1', 'variable', 5, [{ label: '初始值', value: [word('新建'), name('Diagnostic', 110)], columns: ['参数', '值'], items: nested.items }]),
|
||
]
|
||
expect(revealPlan(blocks, 9, 90)).toEqual({ id: '0', expand: [] })
|
||
expect(occurrencePlan(blocks, { start: 70, end: 83, kind: 'read' }, id => id === '0')).toEqual({ id: '1', expand: ['1/p0'] })
|
||
})
|
||
|
||
it('名字在框里(套了几层都算)也是显示着它', () => {
|
||
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
|
||
const blocks: BlockView[] = [
|
||
textBlock('0', 'return', 0, [word('返回'), { style: 'iteration', label: '列表', segments: [word('遍历'), name('grid', 20), word('取'), inner] }]),
|
||
]
|
||
expect(revealPlan(blocks, 5, 40)).toEqual({ id: '0', expand: [] })
|
||
expect(revealPlan(blocks, 5, 20)).toEqual({ id: '0', expand: [] })
|
||
})
|
||
|
||
it('注释块、函数「说明」的 Markdown 里的名字不带下标,不算显示着定义', () => {
|
||
const blocks: BlockView[] = [
|
||
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'comment' }, concepts: [{ id: 'comment', label: '注释', icon: 'message-square-text' }], children: [], translation: { markdown: '见 `get_db`' } },
|
||
factBlock('1', 'function', 1, [
|
||
{ label: '说明', value: { markdown: '同 `get_db`' } },
|
||
{ label: '输入参数', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('get_db', 30)]] }] },
|
||
]),
|
||
]
|
||
expect(revealPlan(blocks, 0, 30)).toEqual({ id: '1', expand: ['1/p1'] })
|
||
})
|
||
|
||
it('翻译插件没标 defines、名字在属性行展开的子行里(参数):闪整块,连那一行一起展开', () => {
|
||
expect(revealPlan(BLOCKS, 2, 57)).toEqual({ id: '1', expand: ['1/p2'] })
|
||
expect(revealPlan(BLOCKS, 0, 34)).toEqual({ id: '0', expand: ['0/p1'] })
|
||
})
|
||
|
||
it('名字在属性行的值里:不用展开属性行', () => {
|
||
expect(revealPlan(BLOCKS, 2, 46)).toEqual({ id: '1', expand: [] })
|
||
expect(revealPlan(BLOCKS, 8, 130)).toEqual({ id: '2', expand: [] })
|
||
})
|
||
|
||
it('页面上没有这个名字:按行找包含它的块,也展开祖先', () => {
|
||
expect(revealPlan(BLOCKS, 3, 999)).toEqual({ id: '1.0', expand: ['1'] })
|
||
expect(revealPlan(BLOCKS, 4, 999)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||
expect(revealPlan(BLOCKS, 8, 999)).toEqual({ id: '2', expand: [] })
|
||
})
|
||
|
||
it('一行落在后一块,不选已经结束的前一块;都包住时取最小的那块', () => {
|
||
const blocks = [
|
||
{ ...textBlock('0', 'class', 3, [word('类')]), endLine: 4 },
|
||
{ ...textBlock('1', 'function', 1, [word('函数')]), endLine: 8 },
|
||
]
|
||
expect(revealPlan(blocks, 6, 999)).toEqual({ id: '1', expand: [] })
|
||
const inner = textBlock('0.0', 'return', 2, [word('返回')])
|
||
const outer = { ...textBlock('0', 'function', 1, [word('函数')], [inner]), endLine: 8 }
|
||
expect(revealPlan([outer], 2, 999)).toEqual({ id: '0.0', expand: ['0'] })
|
||
expect(revealPlan([outer], 6, 999)).toEqual({ id: '0', expand: [] })
|
||
})
|
||
|
||
it('函数块默认折叠:目标在函数体里时,要展开的块里有这个函数;展开过一次就记住,不再挡着', () => {
|
||
const plan = revealPlan(BLOCKS, 4, 102)
|
||
const hidden = (folds: Record<string, boolean>): string[] => plan?.expand.filter(id => collapsedIds(BLOCKS, folds).has(id)) ?? []
|
||
expect(hidden({})).toEqual(['1'])
|
||
expect(hidden({ 1: false })).toEqual([])
|
||
expect(hidden({ '1': false, '1.0': true })).toEqual(['1.0'])
|
||
})
|
||
|
||
it('挂在卡片子行上的子块:展开它所在的那一行和它自己,不展开卡片自己', () => {
|
||
const statement = { ...textBlock('0.1.0', 'statement', 5, [name('listen', 300)]), slot: { row: 2, item: 0 } }
|
||
const start = { ...factBlock('0.1', 'function', 4, [{ label: '名称', value: [name('start', 200)] }], [statement]), slot: { row: 3, item: 1 } }
|
||
const create = { ...factBlock('0.0', 'function', 2, [{ label: '名称', value: [name('new', 100)] }]), slot: { row: 3, item: 0 } }
|
||
const card = factBlock('0', 'class', 1, [{ label: '名称', value: [name('Server', 10)] }], [create, start])
|
||
expect(revealPlan([card], 0, 100)).toEqual({ id: '0.0', expand: [propertyRowId('0', 3), '0.0'] })
|
||
expect(revealPlan([card], 0, 200)).toEqual({ id: '0.1', expand: [propertyRowId('0', 3), '0.1'] })
|
||
expect(revealPlan([card], 0, 300)).toEqual({ id: '0.1.0', expand: [propertyRowId('0', 3), '0.1', propertyRowId('0.1', 2), '0.1.0'] })
|
||
})
|
||
|
||
it('按行找:同一行有几块、一样大时取外层那块;行在所有块之前取第一块;没有块时没有目标', () => {
|
||
const sameLine = [textBlock('0', 'if', 1, [word('如果')], [textBlock('0.0', 'return', 1, [word('返回')])])]
|
||
expect(revealPlan(sameLine, 1, 999)).toEqual({ id: '0', expand: [] })
|
||
expect(revealPlan(sameLine, 0, 999)).toEqual({ id: '0', expand: [] })
|
||
expect(revealPlan([], 0, 0)).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
describe('跳到定义:闪哪一处(defines)', () => {
|
||
const range = (startIndex: number, endIndex: number): { startIndex: number, endIndex: number } => ({ startIndex, endIndex })
|
||
/**
|
||
* 0 def f(*args): f 在 4,*args 在 6..11
|
||
* 1 a = 1; b = 2 一条语句声明两个变量,a 在 20,b 在 27
|
||
*/
|
||
const blocks: BlockView[] = [
|
||
{
|
||
...factBlock('0', 'function', 0, [
|
||
{ label: '名称', value: [name('f', 4)], defines: range(4, 5) },
|
||
{ label: '输入参数', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('*args', 6)]], defines: range(6, 11) }] },
|
||
]),
|
||
defines: range(4, 5),
|
||
},
|
||
factBlock('1', 'variable', 1, [
|
||
{ label: '名称', value: [name('a', 20)], defines: range(20, 21) },
|
||
{ label: '名称', value: [name('b', 27)], defines: range(27, 28) },
|
||
]),
|
||
]
|
||
|
||
it('父块的大范围盖住定义下标时,停在后面那个显示着这个名字的函数上,不闪父块', () => {
|
||
const parent = {
|
||
...factBlock('0', 'class', 1, [{ label: '名称', value: [name('DeployActionError', 10)] }]),
|
||
endLine: 12,
|
||
defines: range(0, 400),
|
||
}
|
||
const fn = factBlock('1', 'function', 14, [{ label: '名称', value: [name('fetchAppDeployments', 80)] }])
|
||
expect(revealPlan([parent, fn], 14, 80)).toEqual({ id: '1', expand: [] })
|
||
})
|
||
|
||
it('定义是块自己的名字:闪整块,属性行也标了它也一样', () => {
|
||
expect(revealPlan(blocks, 0, 4)).toEqual({ id: '0', expand: [] })
|
||
})
|
||
|
||
it('defines 是一段范围:定义落在中间也算(*args 的定义在 args 上)', () => {
|
||
expect(revealPlan(blocks, 0, 7)).toEqual({ id: '0', row: '0/p1/0', expand: ['0/p1'] })
|
||
})
|
||
|
||
it('一条语句声明几个变量:只闪那个变量的名称行', () => {
|
||
expect(revealPlan(blocks, 1, 27)).toEqual({ id: '1', row: '1/p1', expand: [] })
|
||
})
|
||
})
|
||
|
||
describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||
const py = createPythonAdapter(WASM_DIR)
|
||
|
||
/** 不填 defines 的第三方翻译插件:属性表照旧,行和子行都不标讲的是哪个定义。 */
|
||
function withoutDefines(translator: Translator): Translator {
|
||
const strip = (items: PropertyItem[]): PropertyItem[] => items.map(({ cells, items: nested }) => nested ? { cells, items: strip(nested) } : { cells })
|
||
return {
|
||
...translator,
|
||
describe: (block, source) => translator.describe(block, source).map(({ label, value, items }) =>
|
||
items ? { label, value, items: strip(items) } : { label, value }),
|
||
}
|
||
}
|
||
|
||
/** parentId 下面一层层的子行里 id 是 target 的那个。 */
|
||
function findItem(items: PropertyItem[] | undefined, parentId: string, target: string): PropertyItem | undefined {
|
||
for (const [index, item] of (items ?? []).entries()) {
|
||
const id = propertyItemId(parentId, index)
|
||
const found = id === target ? item : findItem(item.items, id, target)
|
||
if (found)
|
||
return found
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
function flatten(list: BlockView[]): BlockView[] {
|
||
return list.flatMap(view => [view, ...flatten(view.children)])
|
||
}
|
||
|
||
function text(segments: Segment[]): string {
|
||
return segments.map(segment => segment.text).join('')
|
||
}
|
||
|
||
/** 闪的那一处:整块写成它的第一行源码,属性行写成「属性名: 值」,子行各列用 | 隔开。 */
|
||
function flashed(views: BlockView[], plan: RevealPlan | undefined): string {
|
||
const block = flatten(views).find(view => view.id === plan?.id)
|
||
if (!block || !plan)
|
||
return ''
|
||
if (plan.row !== undefined && 'properties' in block) {
|
||
for (const [index, row] of block.properties.entries()) {
|
||
const rowId = propertyRowId(block.id, index)
|
||
if (plan.row === rowId)
|
||
return `${row.label}: ${text(row.value)}`
|
||
const item = findItem(row.items, rowId, plan.row)
|
||
if (item)
|
||
return item.cells.map(text).join(' | ')
|
||
}
|
||
}
|
||
return block.code.split('\n')[0]
|
||
}
|
||
|
||
/** 定义提供者说定义在 source 里 needle 开头那个名字上:预览滚到哪、展开哪些、闪哪一处。 */
|
||
async function jump(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, needle: string): Promise<{ plan: RevealPlan | undefined, flashed: string }> {
|
||
const offset = source.indexOf(needle)
|
||
expect(offset, needle).toBeGreaterThanOrEqual(0)
|
||
const views: BlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator))
|
||
const plan = revealPlan(views, source.slice(0, offset).split('\n').length - 1, offset)
|
||
return { plan, flashed: flashed(views, plan) }
|
||
}
|
||
|
||
const PY = [
|
||
'from app.util import fmt, clean as tidy',
|
||
'',
|
||
'def mask_phone(phone: str) -> str:',
|
||
' if len(phone) < 9:',
|
||
' return phone',
|
||
' for i in range(3):',
|
||
' print(i)',
|
||
' return fmt(phone)',
|
||
'',
|
||
'LIMIT = 3',
|
||
'a, b = 1, 2',
|
||
].join('\n')
|
||
|
||
const TS = [
|
||
'import { readFile as rf, stat } from \'node:fs\'',
|
||
'',
|
||
'export function load(path: string, { encoding }: Options): string {',
|
||
' for (const line of lines) {',
|
||
' use(line, stat)',
|
||
' }',
|
||
' return rf(path, encoding)',
|
||
'}',
|
||
'',
|
||
'let a = 1, b = 2',
|
||
'const LIMIT = 3',
|
||
].join('\n')
|
||
|
||
it('python:参数只闪「输入参数」下它那个子行,导入项只闪它那一项', async () => {
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, PY, needle)
|
||
expect(await at('phone: str')).toEqual({ plan: { id: '1', row: '1/p3/0', expand: ['1/p3'] }, flashed: 'phone | 字符串 | ' })
|
||
expect(await at('tidy')).toEqual({ plan: { id: '0', row: '0/p1/1', expand: ['0/p1'] }, flashed: 'clean 作为 tidy' })
|
||
expect(await at('clean')).toEqual({ plan: { id: '0', row: '0/p1/1', expand: ['0/p1'] }, flashed: 'clean 作为 tidy' })
|
||
})
|
||
|
||
it('python:函数名、变量名(一个解构模式也算一个)是块自己的名字,循环变量在一行译文里:都闪整块', async () => {
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, PY, needle)
|
||
expect(await at('mask_phone')).toEqual({ plan: { id: '1', expand: [] }, flashed: 'def mask_phone(phone: str) -> str:' })
|
||
expect(await at('LIMIT')).toEqual({ plan: { id: '2', expand: [] }, flashed: 'LIMIT = 3' })
|
||
expect(await at('b = 1')).toEqual({ plan: { id: '3', expand: [] }, flashed: 'a, b = 1, 2' })
|
||
expect(await at('i in range')).toEqual({ plan: { id: '1.1', expand: ['1'] }, flashed: 'for i in range(3):' })
|
||
})
|
||
|
||
it('python:推导式的变量只在框里显示:闪框所在的那块', async () => {
|
||
const source = ['def ids(users):', ' return [u.id for u in users]'].join('\n')
|
||
expect(await jump(py, 'python', pythonZhTranslator, source, 'u in users')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'return [u.id for u in users]' })
|
||
})
|
||
|
||
it('python:翻译插件没标 defines 时闪整块,名字在子行里照样展开那一行', async () => {
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(py, 'python', withoutDefines(pythonZhTranslator), PY, needle)
|
||
expect(await at('phone: str')).toEqual({ plan: { id: '1', expand: ['1/p3'] }, flashed: 'def mask_phone(phone: str) -> str:' })
|
||
expect(await at('tidy')).toEqual({ plan: { id: '0', expand: ['0/p1'] }, flashed: 'from app.util import fmt, clean as tidy' })
|
||
})
|
||
|
||
it('typeScript:参数(解构参数里的名字也算)、导入项、多变量声明里的一个,只闪它那一行', async () => {
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, TS, needle)
|
||
expect(await at('path: string')).toEqual({ plan: { id: '1', row: '1/p3/0', expand: ['1/p3'] }, flashed: 'path | 字符串 | ' })
|
||
expect(await at('encoding }')).toEqual({ plan: { id: '1', row: '1/p3/1', expand: ['1/p3'] }, flashed: '{ encoding } | Options | ' })
|
||
expect(await at('rf,')).toEqual({ plan: { id: '0', row: '0/p2/0', expand: ['0/p2'] }, flashed: 'readFile 作为 rf' })
|
||
expect(await at('stat }')).toEqual({ plan: { id: '0', row: '0/p2/1', expand: ['0/p2'] }, flashed: 'stat' })
|
||
expect(await at('b = 2')).toEqual({ plan: { id: '2', row: '2/p3', expand: [] }, flashed: '名称: b' })
|
||
})
|
||
|
||
it('typeScript:函数名、只声明一个的常量是块自己的名字,循环变量在一行译文里:都闪整块', async () => {
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, TS, needle)
|
||
expect(await at('load')).toEqual({ plan: { id: '1', expand: [] }, flashed: 'export function load(path: string, { encoding }: Options): string {' })
|
||
expect(await at('LIMIT')).toEqual({ plan: { id: '3', expand: [] }, flashed: 'const LIMIT = 3' })
|
||
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, language: ts, markers: [] }, typescriptZhTranslator))
|
||
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
|
||
|
||
const pySource = ['class NotFound(AppResponse):', ' """找不到。"""', ' 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(AppResponse):' })
|
||
expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' })
|
||
})
|
||
|
||
it('异常块:类名闪整块,属性名只闪「属性」表里它那一行,构造函数的参数只闪「构造参数」表里它那一行(都先展开那一行);留下的构造函数照常是子块,异常块默认折叠', async () => {
|
||
const source = [
|
||
'class Failed(Exception):',
|
||
' status = 500',
|
||
'',
|
||
' def __init__(self, message: str) -> None:',
|
||
' self.message = message',
|
||
].join('\n')
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, source, needle)
|
||
expect(await at('Failed')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'class Failed(Exception):' })
|
||
expect(await at('status')).toEqual({ plan: { id: '0', row: '0/p3/0', expand: ['0/p3'] }, flashed: 'status | 500' })
|
||
expect(await at('message: str')).toEqual({ plan: { id: '0', row: '0/p4/0', expand: ['0/p4'] }, flashed: 'message | 字符串 | ' })
|
||
expect(await at('__init__')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def __init__(self, message: str) -> None:' })
|
||
|
||
const tsSource = 'export class ApiError extends Error {\n status = 500\n constructor(public code: string) {\n super(code)\n }\n}'
|
||
const atTs = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, tsSource, needle)
|
||
expect(await atTs('status')).toEqual({ plan: { id: '0', row: '0/p3/0', expand: ['0/p3'] }, flashed: 'status | 500' })
|
||
expect(await atTs('code:')).toEqual({ plan: { id: '0', row: '0/p4/0', expand: ['0/p4'] }, flashed: 'code | 字符串 | ' })
|
||
const views = await withBlocks(ts, 'typescript', tsSource, blocks => toBlockViews(blocks, { source: tsSource, language: ts, markers: [] }, typescriptZhTranslator))
|
||
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
|
||
})
|
||
|
||
it('库规则认领的块:类名闪整块,字段名只闪字段表里它那一行(先展开「字段」那一行);留下的方法照常,卡片默认折叠', async () => {
|
||
const source = [
|
||
'from sqlalchemy.orm import Mapped, mapped_column',
|
||
'',
|
||
'class User(Base):',
|
||
' __tablename__ = "users"',
|
||
' name: Mapped[str] = mapped_column(comment="名字")',
|
||
'',
|
||
' def greet(self):',
|
||
' return self.name',
|
||
].join('\n')
|
||
const views = await withBlocks(py, 'python', source, blocks =>
|
||
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [sqlalchemyRule], report: () => {} }))
|
||
const at = (needle: string): { plan: RevealPlan | undefined, flashed: string } => {
|
||
const offset = source.indexOf(needle)
|
||
const plan = revealPlan(views, source.slice(0, offset).split('\n').length - 1, offset)
|
||
return { plan, flashed: flashed(views, plan) }
|
||
}
|
||
expect(at('User')).toEqual({ plan: { id: '1', expand: [] }, flashed: 'class User(Base):' })
|
||
expect(at('name:')).toEqual({ plan: { id: '1', row: '1/p3/0', expand: ['1/p3'] }, flashed: 'name | 字符串 | | 名字' })
|
||
expect(at('greet')).toEqual({ plan: { id: '1.0', expand: ['1'] }, flashed: 'def greet(self):' })
|
||
expect([...collapsedIds(views, {})].sort()).toEqual(['1', '1.0'])
|
||
})
|
||
|
||
it('字典 / 对象:定义落在对象的属性名上只闪那一项,嵌套的连外层各层一起展开;值里只是显示着的名字闪整块,展开到它那一层', async () => {
|
||
const source = [
|
||
'export const routes = {',
|
||
' home: { path: \'/\', page: Home },',
|
||
' about,',
|
||
'}',
|
||
'use(routes.home.path)',
|
||
].join('\n')
|
||
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, source, needle)
|
||
expect(await at('home:')).toEqual({ plan: { id: '0', row: '0/p2/0', expand: ['0/p2'] }, flashed: 'home | 对象 2 项' })
|
||
expect(await at('path:')).toEqual({ plan: { id: '0', row: '0/p2/0/0', expand: ['0/p2', '0/p2/0'] }, flashed: 'path | \'/\'' })
|
||
expect(await at('about,')).toEqual({ plan: { id: '0', row: '0/p2/1', expand: ['0/p2'] }, flashed: 'about | about' })
|
||
expect(await at('Home')).toEqual({ plan: { id: '0', expand: ['0/p2', '0/p2/0'] }, flashed: 'export const routes = {' })
|
||
const plain = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', withoutDefines(typescriptZhTranslator), source, needle)
|
||
expect(await plain('path:')).toEqual({ plan: { id: '0', expand: ['0/p2', '0/p2/0'] }, flashed: 'export const routes = {' })
|
||
|
||
const pySource = 'CONFIG = {"db": {"host": host}}'
|
||
const atPy = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, pySource, needle)
|
||
expect(await atPy('host}')).toEqual({ plan: { id: '0', expand: ['0/p2', '0/p2/0'] }, flashed: 'CONFIG = {"db": {"host": host}}' })
|
||
})
|
||
|
||
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 {' })
|
||
expect(await at('b = 2')).toEqual({ plan: { id: '2', expand: [] }, flashed: 'let a = 1, b = 2' })
|
||
})
|
||
})
|
||
|
||
describe('把目标块滚进视口', () => {
|
||
it('整块已经在视口里:不滚', () => {
|
||
expect(scrollTopToShow({ top: 120, bottom: 160 }, 100, 500)).toBeUndefined()
|
||
})
|
||
|
||
it('在视口外:放到视口正中', () => {
|
||
expect(scrollTopToShow({ top: 1000, bottom: 1040 }, 0, 500)).toBe(770)
|
||
expect(scrollTopToShow({ top: 80, bottom: 120 }, 400, 500)).toBe(0)
|
||
})
|
||
|
||
it('露了一半也算在视口外;比视口还高就顶部对齐', () => {
|
||
expect(scrollTopToShow({ top: 580, bottom: 620 }, 100, 500)).toBe(350)
|
||
expect(scrollTopToShow({ top: 900, bottom: 1600 }, 0, 500)).toBe(900)
|
||
})
|
||
|
||
it('筛选栏挡住时,滚动位置让目标出现在筛选栏下面', () => {
|
||
expect(scrollTopToShow({ top: 900, bottom: 1600 }, 0, 548, 48)).toBe(852)
|
||
})
|
||
})
|