🐛 fix(vscode): 代码阅读跳到参数、导入项、多变量声明里的一个时只闪属性表里那一行,按源码位置判断
This commit is contained in:
1 parent
5b4ca903c5
commit
3ac1c783fb
14 files changed
+367
-47
No files matched your search
@@ -3,7 +3,7 @@ import type { PropertyItem, PropertyRow } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { propertyRowId } from './viewState'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 子行里除最后一列以外各列的宽度(参数的名称、类型),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
||||
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
|
||||
@@ -14,13 +14,18 @@ function columns(count: number): string {
|
||||
}
|
||||
|
||||
/** 展开在一行下面的子行:左边一条竖线,每个子行几列对齐。 */
|
||||
function PropertyItems({ items }: { items: PropertyItem[] }): ReactElement {
|
||||
function PropertyItems({ rowId, items }: { rowId: string, items: PropertyItem[] }): ReactElement {
|
||||
return (
|
||||
<div className="flex gap-2.5 pl-[5px]">
|
||||
<div className="cr-guide w-px shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="grid gap-x-3" style={{ gridTemplateColumns: columns(item.cells.length) }}>
|
||||
<div
|
||||
key={index}
|
||||
className="cr-prop-item grid gap-x-3"
|
||||
data-id={propertyItemId(rowId, index)}
|
||||
style={{ gridTemplateColumns: columns(item.cells.length) }}
|
||||
>
|
||||
{item.cells.map((cell, i) => (
|
||||
<p key={i} className="cr-code min-w-0">
|
||||
<SegmentText segments={cell} />
|
||||
@@ -49,7 +54,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
const Chevron = folded ? ChevronRight : ChevronDown
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div className="flex gap-3">
|
||||
<div className="cr-prop-row flex gap-3" data-id={id}>
|
||||
{items.length > 0
|
||||
? (
|
||||
<button
|
||||
@@ -68,7 +73,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
<SegmentText segments={row.value} />
|
||||
</p>
|
||||
</div>
|
||||
{items.length > 0 && !folded && <PropertyItems items={items} />}
|
||||
{items.length > 0 && !folded && <PropertyItems rowId={id} items={items} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -94,7 +94,16 @@ body.vscode-high-contrast-light {
|
||||
background: var(--kind);
|
||||
}
|
||||
|
||||
/* 跳到定义的目标块:边框和底色往聚焦色闪三下(共 1.5 秒)再复原;减少动态效果时改成静止高亮同样长的时间 */
|
||||
/* 属性行、子行平时看不出边框;往外扩出一圈,跳到定义只闪它时亮起来不贴着字,排版不动 */
|
||||
.cr-prop-row,
|
||||
.cr-prop-item {
|
||||
margin: -1px -4px;
|
||||
padding: 0 3px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 跳到定义的目标(整块,或属性表里的一行 / 子行):边框和底色往聚焦色闪三下(共 1.5 秒)再复原;减少动态效果时改成静止高亮同样长的时间 */
|
||||
@keyframes cr-flash {
|
||||
50% {
|
||||
background: color-mix(in srgb, var(--vscode-focusBorder) 25%, transparent);
|
||||
@@ -107,7 +116,7 @@ body.vscode-high-contrast-light {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
}
|
||||
.cr-block[data-flash] {
|
||||
:is(.cr-block, .cr-prop-row, .cr-prop-item)[data-flash] {
|
||||
animation: cr-flash 500ms ease-in-out 3;
|
||||
}
|
||||
|
||||
@@ -116,7 +125,7 @@ body.vscode-high-contrast-light {
|
||||
.cr-guide {
|
||||
transition: none;
|
||||
}
|
||||
.cr-block[data-flash] {
|
||||
:is(.cr-block, .cr-prop-row, .cr-prop-item)[data-flash] {
|
||||
animation: cr-flash-still 1500ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { BlockView, Segment } from './types'
|
||||
import { propertyRowId } from './viewState'
|
||||
import type { BlockView, Segment, SourceRange } from './types'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 跳到定义时要滚到的块,以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */
|
||||
export interface RevealPlan {
|
||||
id: string
|
||||
/** 属性行 id(propertyRowId)或子行 id(propertyItemId);没有时闪整块 */
|
||||
row?: string
|
||||
expand: string[]
|
||||
}
|
||||
|
||||
/** 目标在一块里的哪儿:row、expand 同 RevealPlan,expand 不含祖先块。 */
|
||||
type Spot = Omit<RevealPlan, 'id'>
|
||||
|
||||
interface Placed {
|
||||
block: BlockView
|
||||
/** 从顶层到父块的块 id */
|
||||
@@ -21,34 +26,53 @@ function* placed(blocks: BlockView[], ancestors: string[] = []): Generator<Place
|
||||
}
|
||||
}
|
||||
|
||||
function covers(range: SourceRange | undefined, offset: number): boolean {
|
||||
return range !== undefined && range.startIndex <= offset && offset < range.endIndex
|
||||
}
|
||||
|
||||
function shows(segments: Segment[], offset: number): boolean {
|
||||
return segments.some(segment => segment.offset === offset)
|
||||
}
|
||||
|
||||
/** 块里哪儿显示着 offset 处的名字:'value' 是一行译文或属性行的值,数字是带子行的属性行序号(名字在它展开的子行里),不在这块里是 undefined。 */
|
||||
function locate(block: BlockView, offset: number): 'value' | number | undefined {
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个)→ 只闪它,子行连它所在的行一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开那一行。
|
||||
* 不在这块里是 undefined。
|
||||
*/
|
||||
function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
if (covers(block.defines, offset))
|
||||
return { expand: [] }
|
||||
if ('translation' in block)
|
||||
return shows(block.translation, offset) ? 'value' : undefined
|
||||
for (const [index, row] of block.properties.entries()) {
|
||||
return shows(block.translation, offset) ? { expand: [] } : undefined
|
||||
const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) }))
|
||||
for (const { row, id } of rows) {
|
||||
if (covers(row.defines, offset))
|
||||
return { row: id, expand: [] }
|
||||
const item = row.items?.findIndex(entry => covers(entry.defines, offset)) ?? -1
|
||||
if (item >= 0)
|
||||
return { row: propertyItemId(id, item), expand: [id] }
|
||||
}
|
||||
for (const { row, id } of rows) {
|
||||
if (shows(row.value, offset))
|
||||
return 'value'
|
||||
return { expand: [] }
|
||||
if (row.items?.some(item => item.cells.some(cell => shows(cell, offset))))
|
||||
return index
|
||||
return { expand: [id] }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳到定义:目标是显示着定义处那个名字的块,名字在属性行的子行里时连这一行一起展开;
|
||||
* 页面上找不到这个名字(例如定义是整个模块,落在文件开头)就取包含 line 的块,规则同 blockForLine。
|
||||
* 跳到定义:目标见 locate;页面上找不到这处定义(例如定义是整个模块,落在文件开头)就取包含 line 的块,规则同 blockForLine。
|
||||
* 目标的祖先块都要展开。
|
||||
*/
|
||||
export function revealPlan(blocks: BlockView[], line: number, definition: number): RevealPlan | undefined {
|
||||
let byLine: Placed | undefined
|
||||
for (const item of placed(blocks)) {
|
||||
const where = locate(item.block, definition)
|
||||
if (where !== undefined)
|
||||
return { id: item.block.id, expand: typeof where === 'number' ? [...item.ancestors, propertyRowId(item.block.id, where)] : item.ancestors }
|
||||
const spot = locate(item.block, definition)
|
||||
if (spot)
|
||||
return { id: item.block.id, ...spot, expand: [...item.ancestors, ...spot.expand] }
|
||||
if (item.block.startLine <= line && (!byLine || item.block.startLine > byLine.block.startLine))
|
||||
byLine = item
|
||||
}
|
||||
|
||||
@@ -17,11 +17,20 @@ export type TokenRole
|
||||
/** offset:照抄自源码里的名字、字符串时它在源码里的起始下标,⌘ / Ctrl 悬停、点击时拿它查定义;译词、提示词没有 */
|
||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
|
||||
/** 源码里一段的下标,end 为开区间 */
|
||||
export interface SourceRange { startIndex: number, endIndex: number }
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字(参数名、导入项、声明的变量名)在源码里的范围。
|
||||
* 跳到的定义落在里面就只闪这一行;翻译插件没填就闪整块
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值 */
|
||||
export interface PropertyItem { cells: Segment[][] }
|
||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||
|
||||
/** 属性表的一行:左列属性名,右列着色的值;items 是展开在这一行下面的子行(输入参数、导入项) */
|
||||
export interface PropertyRow {
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
value: Segment[]
|
||||
items?: PropertyItem[]
|
||||
@@ -38,6 +47,8 @@ interface BlockViewBase {
|
||||
code: string
|
||||
/** 源码着色(拼起来等于 code) */
|
||||
codeTokens: Segment[]
|
||||
/** 块自己的名字(函数名、只声明一个变量时的变量名)在源码里的范围:跳到的定义就是它时闪整块 */
|
||||
defines?: SourceRange
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
@@ -48,7 +59,7 @@ export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
/**
|
||||
* 滚到包含 line 的块。definition 只在跳到定义时有:定义处那个名字在源码里的下标(Segment.offset),
|
||||
* 要展开把它折叠住的块和属性行,再让那块闪几下
|
||||
* 要展开把它折叠住的块和属性行,再让就是这处定义的那一块或属性表里那一行闪几下
|
||||
*/
|
||||
export interface Reveal {
|
||||
line: number
|
||||
|
||||
@@ -4,7 +4,7 @@ import { revealPlan, scrollTopToShow } from './reveal'
|
||||
import { blockForLine } from './scrollPosition'
|
||||
import { measureBlocks } from './useScrollPosition'
|
||||
|
||||
/** 让一块闪几下(codeReader.css 的 [data-flash]);正在闪的同一块再跳一次时从头闪。 */
|
||||
/** 让一块或属性表里的一行闪几下(codeReader.css 的 [data-flash]);正在闪的同一处再跳一次时从头闪。 */
|
||||
function flash(el: HTMLElement): void {
|
||||
const running = el.getAnimations().filter(animation => animation instanceof CSSAnimation)
|
||||
if (running.length > 0) {
|
||||
@@ -25,7 +25,7 @@ function flash(el: HTMLElement): void {
|
||||
/**
|
||||
* 滚到 reveal 指的位置,处理完调 done:
|
||||
* ① 从文本编辑器切过来(没有 definition):包含那一行的块顶到视口顶部,折叠着的不展开;
|
||||
* ② 跳到定义:先展开把目标藏起来的块和属性行(expand 之后重新渲染,再进来一次),再把目标块滚进视口、闪几下。
|
||||
* ② 跳到定义:先展开把目标藏起来的块和属性行(expand 之后重新渲染,再进来一次),再把目标(整块,或就是这处定义的属性行 / 子行)滚进视口、闪几下。
|
||||
*/
|
||||
export function useReveal(
|
||||
doc: DocumentMessage | null,
|
||||
@@ -50,7 +50,9 @@ export function useReveal(
|
||||
expand(hidden)
|
||||
return
|
||||
}
|
||||
const el = plan && document.querySelector<HTMLElement>(`.cr-block[data-id="${plan.id}"]`)
|
||||
// 块正在编辑时页面上没有属性表,就闪整块
|
||||
const row = plan?.row === undefined ? null : document.querySelector<HTMLElement>(`:is(.cr-prop-row, .cr-prop-item)[data-id="${plan.row}"]`)
|
||||
const el = row ?? (plan && document.querySelector<HTMLElement>(`.cr-block[data-id="${plan.id}"]`))
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const top = scrollTopToShow({ top: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY }, window.scrollY, window.innerHeight)
|
||||
|
||||
@@ -80,3 +80,8 @@ export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Recor
|
||||
export function propertyRowId(blockId: string, rowIndex: number): string {
|
||||
return `${blockId}/p${rowIndex}`
|
||||
}
|
||||
|
||||
/** 属性行展开的第几个子行,例如 '6.1.0/p3/1'。只用来在页面上找到它(跳到定义时闪它),子行不折叠、不进 folds。 */
|
||||
export function propertyItemId(rowId: string, itemIndex: number): string {
|
||||
return `${rowId}/${itemIndex}`
|
||||
}
|
||||
Reference in new issue
Block a user