🐛 fix(vscode): 代码阅读跳到参数、导入项、多变量声明里的一个时只闪属性表里那一行,按源码位置判断
This commit is contained in:
1 parent
5b4ca903c5
commit
3ac1c783fb
14 files changed
+367
-47
No files matched your search
@@ -1,7 +1,15 @@
|
||||
import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract'
|
||||
import type { RevealPlan } from './reveal'
|
||||
import type { BlockKind, BlockView, 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 { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
|
||||
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
|
||||
import { typescriptZhTranslator } from '../../../src/codeReader/translators/typescriptZh'
|
||||
import { revealPlan, scrollTopToShow } from './reveal'
|
||||
import { collapsedIds } from './viewState'
|
||||
import { collapsedIds, propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
function name(text: string, offset: number): Segment {
|
||||
return { text, role: 'variable', offset }
|
||||
@@ -48,7 +56,7 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||||
})
|
||||
|
||||
it('名字在属性行展开的子行里(参数):连那一行一起展开', () => {
|
||||
it('翻译插件没标 defines、名字在属性行展开的子行里(参数):闪整块,连那一行一起展开', () => {
|
||||
expect(revealPlan(BLOCKS, 2, 57)).toEqual({ id: '1', expand: ['1/p2'] })
|
||||
expect(revealPlan(BLOCKS, 0, 34)).toEqual({ id: '0', expand: ['0/p1'] })
|
||||
})
|
||||
@@ -80,6 +88,159 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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('定义是块自己的名字:闪整块,属性行也标了它也一样', () => {
|
||||
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 {
|
||||
return {
|
||||
...translator,
|
||||
describe: (block, source) => translator.describe(block, source).map(({ label, value, items }) =>
|
||||
items ? { label, value, items: items.map(({ cells }) => ({ cells })) } : { label, value }),
|
||||
}
|
||||
}
|
||||
|
||||
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 = row.items?.find((_, i) => plan.row === propertyItemId(rowId, i))
|
||||
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, adapter, 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:翻译插件没标 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('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()
|
||||
|
||||
Reference in new issue
Block a user