🐛 fix(vscode): 代码阅读跳到参数、导入项、多变量声明里的一个时只闪属性表里那一行,按源码位置判断

This commit is contained in:
cruldra committed 2026-09-25 14:11:44 +08:00
1 parent 5b4ca903c5
commit 3ac1c783fb
14 files changed
+367 -47

No files matched your search

+163 -2
View File
@@ -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()