✨ feat(vscode): 代码阅读把字典、对象字面量的值展开成键值树
This commit is contained in:
1 parent
51f2ef7bfa
commit
4930dc0d42
20 files changed
+578
-103
No files matched your search
@@ -30,7 +30,7 @@ export function App(): ReactElement | null {
|
||||
/** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */
|
||||
const [reveal, setReveal] = useState<Reveal | null>(null)
|
||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds])
|
||||
/** 记下这几块、属性行折叠还是展开,从此不再按默认 */
|
||||
/** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */
|
||||
const setFolded = useCallback((ids: string[], folded: boolean) => setView(prev => ({
|
||||
...prev,
|
||||
folds: { ...prev.folds, ...Object.fromEntries(ids.map(id => [id, folded])) },
|
||||
|
||||
@@ -29,7 +29,7 @@ const KIND_LABEL: Record<BlockKind, string> = {
|
||||
export interface BlockTreeState {
|
||||
version: number
|
||||
editing: ReadonlySet<string>
|
||||
/** 眼下折叠着的块 id 和属性行 id(propertyRowId):用户的选择叠在默认上,见 collapsedIds */
|
||||
/** 眼下折叠着的块 id、属性行 id 和子行 id(propertyRowId、propertyItemId):用户的选择叠在默认上,见 collapsedIds */
|
||||
collapsed: ReadonlySet<string>
|
||||
/** 刚点了「编辑」、要把光标放进去的块 */
|
||||
focusId: string | null
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { PropertyItem, PropertyRow } from './types'
|
||||
import type { PropertyItem, PropertyRow, Segment } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { TextPreview } from './TextPreview'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 子行里除最后一列以外各列的宽度(参数的名称、类型),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
||||
/** 子行里除最后一列以外各列的宽度(参数的名称、类型,字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
||||
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
|
||||
|
||||
function columns(count: number): string {
|
||||
@@ -14,32 +14,89 @@ function columns(count: number): string {
|
||||
return [...fixed, 'minmax(0, 1fr)'].join(' ')
|
||||
}
|
||||
|
||||
/** 展开在一行下面的子行:左边一条竖线,每个子行几列对齐。 */
|
||||
function PropertyItems({ rowId, items }: { rowId: string, items: PropertyItem[] }): ReactElement {
|
||||
/** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
|
||||
interface Folding {
|
||||
collapsed: ReadonlySet<string>
|
||||
onToggleFold: (id: string) => void
|
||||
}
|
||||
|
||||
function segmentsText(segments: Segment[]): string {
|
||||
return segments.map(segment => segment.text).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||||
*/
|
||||
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Segment[], foldable: boolean, folding: Folding }): ReactElement {
|
||||
if (!foldable) {
|
||||
return (
|
||||
<div className="flex min-w-0 gap-1">
|
||||
<span className="w-3.5 shrink-0" />
|
||||
<p className="cr-code min-w-0">
|
||||
<SegmentText segments={cell} />
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const folded = folding.collapsed.has(id)
|
||||
const Chevron = folded ? ChevronRight : ChevronDown
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="group flex min-w-0 items-start gap-1 rounded-sm text-left"
|
||||
aria-label={`${folded ? '展开' : '折叠'}${segmentsText(cell)}`}
|
||||
aria-expanded={!folded}
|
||||
onClick={() => folding.onToggleFold(id)}
|
||||
>
|
||||
<span className="flex h-5 w-3.5 shrink-0 items-center">
|
||||
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
||||
</span>
|
||||
<span className="cr-code min-w-0">
|
||||
<SegmentText segments={cell} />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
|
||||
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
|
||||
*/
|
||||
function PropertyItems({ parentId, items, folding }: { parentId: string, items: PropertyItem[], folding: Folding }): ReactElement {
|
||||
const nested = items.some(item => (item.items?.length ?? 0) > 0)
|
||||
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="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} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{items.map((item, index) => {
|
||||
const id = propertyItemId(parentId, index)
|
||||
const children = item.items ?? []
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div
|
||||
className="cr-prop-item grid gap-x-3"
|
||||
data-id={id}
|
||||
style={{ gridTemplateColumns: columns(item.cells.length) }}
|
||||
>
|
||||
{item.cells.map((cell, i) => i === 0 && nested
|
||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0} folding={folding} />
|
||||
: (
|
||||
<p key={i} className="cr-code min-w-0">
|
||||
<SegmentText segments={cell} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{children.length > 0 && !folding.collapsed.has(id) && <PropertyItems parentId={id} items={children} folding={folding} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项下面展开子行。 */
|
||||
/** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行。 */
|
||||
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
blockId: string
|
||||
rows: PropertyRow[]
|
||||
@@ -72,7 +129,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
: <span className="cr-prop-label flex h-5 w-[76px] shrink-0 items-center">{row.label}</span>}
|
||||
<TextPreview text={row.value} className="min-w-0 flex-1" />
|
||||
</div>
|
||||
{items.length > 0 && !folded && <PropertyItems rowId={id} items={items} />}
|
||||
{items.length > 0 && !folded && <PropertyItems parentId={id} items={items} folding={{ collapsed, onToggleFold }} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract'
|
||||
import type { RevealPlan } from './reveal'
|
||||
import type { BlockKind, BlockView, PropertyRow, Segment } from './types'
|
||||
import type { BlockKind, BlockView, PropertyItem, PropertyRow, Segment } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
@@ -138,13 +138,25 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
|
||||
/** 不填 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: items.map(({ cells }) => ({ cells })) } : { label, value }),
|
||||
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)])
|
||||
}
|
||||
@@ -163,7 +175,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
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))
|
||||
const item = findItem(row.items, rowId, plan.row)
|
||||
if (item)
|
||||
return item.cells.map(text).join(' | ')
|
||||
}
|
||||
@@ -267,6 +279,27 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' })
|
||||
})
|
||||
|
||||
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 {' })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BlockView, PreviewText, SourceRange } from './types'
|
||||
import type { BlockView, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */
|
||||
export interface RevealPlan {
|
||||
id: string
|
||||
/** 属性行 id(propertyRowId)或子行 id(propertyItemId);没有时闪整块 */
|
||||
@@ -38,8 +38,9 @@ function shows(text: PreviewText, offset: number): boolean {
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个)→ 只闪它,子行连它所在的行一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开那一行。
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它,
|
||||
* 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* 不在这块里是 undefined。
|
||||
*/
|
||||
function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
@@ -51,15 +52,41 @@ function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
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] }
|
||||
const item = definingItem(row.items, id, offset)
|
||||
if (item)
|
||||
return item
|
||||
}
|
||||
for (const { row, id } of rows) {
|
||||
if (shows(row.value, offset))
|
||||
return { expand: [] }
|
||||
if (row.items?.some(item => item.cells.some(cell => shows(cell, offset))))
|
||||
return { expand: [id] }
|
||||
const expand = showingItem(row.items, id, offset)
|
||||
if (expand)
|
||||
return { expand }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** parentId 下面一层层的子行里,defines 盖住 offset 的那个:闪它,展开它上面的各层(parentId 和它所在的那串上层子行)。 */
|
||||
function definingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): Spot | undefined {
|
||||
for (const [index, item] of (items ?? []).entries()) {
|
||||
const id = propertyItemId(parentId, index)
|
||||
if (covers(item.defines, offset))
|
||||
return { row: id, expand: [parentId] }
|
||||
const inner = definingItem(item.items, id, offset)
|
||||
if (inner)
|
||||
return { ...inner, expand: [parentId, ...inner.expand] }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** parentId 下面一层层的子行里,哪个显示着 offset 处的名字:要展开的各层(parentId 到那个子行所在的那一层)。 */
|
||||
function showingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): string[] | undefined {
|
||||
for (const [index, item] of (items ?? []).entries()) {
|
||||
if (item.cells.some(cell => shows(cell, offset)))
|
||||
return [parentId]
|
||||
const inner = showingItem(item.items, propertyItemId(parentId, index), offset)
|
||||
if (inner)
|
||||
return [parentId, ...inner]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -27,15 +27,19 @@ export type PreviewText = Segment[] | Markdown
|
||||
export interface SourceRange { startIndex: number, endIndex: number }
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字(参数名、导入项、声明的变量名)在源码里的范围。
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字(参数名、导入项、声明的变量名、字典 / 对象一项的键)在源码里的范围。
|
||||
* 跳到的定义落在里面就只闪这一行;翻译插件没填就闪整块
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值 */
|
||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值 */
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: Segment[][]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
||||
items?: PropertyItem[]
|
||||
}
|
||||
|
||||
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项) */
|
||||
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项) */
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
value: PreviewText
|
||||
|
||||
@@ -25,7 +25,7 @@ function flash(el: HTMLElement): void {
|
||||
/**
|
||||
* 滚到 reveal 指的位置,处理完调 done:
|
||||
* ① 从文本编辑器切过来(没有 definition):包含那一行的块顶到视口顶部,折叠着的不展开;
|
||||
* ② 跳到定义:先展开把目标藏起来的块和属性行(expand 之后重新渲染,再进来一次),再把目标(整块,或就是这处定义的属性行 / 子行)滚进视口、闪几下。
|
||||
* ② 跳到定义:先展开把目标藏起来的块、属性行和子行(expand 之后重新渲染,再进来一次),再把目标(整块,或就是这处定义的属性行 / 子行)滚进视口、闪几下。
|
||||
*/
|
||||
export function useReveal(
|
||||
doc: DocumentMessage | null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BlockKind, BlockView } from './types'
|
||||
import type { ViewStateStorage } from './viewState'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collapsedIds, createViewStateStore, propertyRowId } from './viewState'
|
||||
import { collapsedIds, createViewStateStore, propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 模拟 VS Code 的 webview 状态:存进去的要能序列化,页面重新加载后原样取回;也能塞进旧版本存的东西 */
|
||||
function memoryStorage(initial?: unknown): ViewStateStorage {
|
||||
@@ -73,6 +73,32 @@ describe('webview 状态', () => {
|
||||
createViewStateStore(storage).update({ folds: { 6.1: false, [propertyRowId('6.1.0', 3)]: true } })
|
||||
expect(createViewStateStore(storage).get().folds).toEqual({ '6.1': false, '6.1.0/p3': true })
|
||||
})
|
||||
|
||||
it('子行 id 是上一层的 id 加子行序号,一层层往后接;不同位置的不会撞上,也不会和块 id、属性行 id 撞上', () => {
|
||||
const row = propertyRowId('6.1.0', 3)
|
||||
expect(propertyItemId(row, 1)).toBe('6.1.0/p3/1')
|
||||
expect(propertyItemId(propertyItemId(row, 1), 0)).toBe('6.1.0/p3/1/0')
|
||||
const ids = [
|
||||
'6',
|
||||
'6.1',
|
||||
propertyRowId('6', 1),
|
||||
propertyRowId('6', 11),
|
||||
propertyItemId(propertyRowId('6', 1), 1),
|
||||
propertyItemId(propertyRowId('6', 11), 1),
|
||||
propertyItemId(propertyItemId(propertyRowId('6', 1), 1), 1),
|
||||
propertyItemId(propertyItemId(propertyRowId('6', 1), 11), 1),
|
||||
propertyItemId(propertyItemId(propertyRowId('6', 1), 1), 11),
|
||||
propertyItemId(propertyRowId('6.1', 1), 1),
|
||||
]
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
})
|
||||
|
||||
it('嵌套子行的折叠选择也存在 folds 里,页面重新加载后还在', () => {
|
||||
const storage = memoryStorage()
|
||||
const nested = propertyItemId(propertyItemId(propertyRowId('2', 2), 0), 1)
|
||||
createViewStateStore(storage).update({ folds: { [nested]: true, [propertyItemId(propertyRowId('2', 2), 0)]: false } })
|
||||
expect(createViewStateStore(storage).get().folds).toEqual({ '2/p2/0/1': true, '2/p2/0': false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('眼下折叠着哪些', () => {
|
||||
@@ -84,4 +110,11 @@ describe('眼下折叠着哪些', () => {
|
||||
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
||||
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
|
||||
})
|
||||
|
||||
it('带下一层的子行默认展开,用户折叠过的按他的', () => {
|
||||
const outer = propertyItemId(propertyRowId('0', 2), 0)
|
||||
const inner = propertyItemId(outer, 3)
|
||||
expect(collapsedIds(BLOCKS, {}).has(outer)).toBe(false)
|
||||
expect([...collapsedIds(BLOCKS, { [outer]: false, [inner]: true })].sort()).toEqual(['0/p2/0/3', '1', '1.0.0', '2', '2.0'])
|
||||
})
|
||||
})
|
||||
@@ -3,11 +3,11 @@ import type { BlockKind, BlockView } from './types'
|
||||
|
||||
/**
|
||||
* 标签页切走时 VS Code 会销毁 webview、切回来重新加载,靠它恢复:哪些块停在「编辑」、
|
||||
* 用户把哪些块和属性行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行 id 见 propertyRowId),以及滚到了哪。
|
||||
* 用户把哪些块、属性行和子行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行、子行 id 见 propertyRowId、propertyItemId),以及滚到了哪。
|
||||
*/
|
||||
export interface ViewState {
|
||||
editing: string[]
|
||||
/** 用户自己点过折叠箭头的块和属性行:id → 折叠着没有。没点过的按默认,见 collapsedIds */
|
||||
/** 用户自己点过折叠箭头的块、属性行和子行:id → 折叠着没有。没点过的按默认,见 collapsedIds */
|
||||
folds: Record<string, boolean>
|
||||
scroll?: ScrollAnchor
|
||||
}
|
||||
@@ -54,12 +54,12 @@ function readViewState(saved: unknown): ViewState {
|
||||
return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds }
|
||||
}
|
||||
|
||||
/** 块默认折叠吗:函数、类块收起主体(属性表照常显示),其余块展开。属性行默认都展开。 */
|
||||
/** 块默认折叠吗:函数、类块收起主体(属性表照常显示),其余块展开。属性行和带下一层的子行默认都展开。 */
|
||||
export function foldedByDefault(kind: BlockKind): boolean {
|
||||
return kind === 'function' || kind === 'class'
|
||||
}
|
||||
|
||||
/** 眼下折叠着的块和属性行:用户点过的按他点的,没点过的按 foldedByDefault。 */
|
||||
/** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault。 */
|
||||
export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Record<string, boolean>>): Set<string> {
|
||||
const collapsed = new Set(Object.keys(folds).filter(id => folds[id]))
|
||||
const visit = (list: readonly BlockView[]): void => {
|
||||
@@ -74,14 +74,19 @@ export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Recor
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性表里带子行的那一行(输入参数、导入项)的折叠 id,和块 id 放在同一份 folds 里:
|
||||
* 属性表里带子行的那一行(输入参数、导入项、字典 / 对象的值)的折叠 id,和块 id 放在同一份 folds 里:
|
||||
* `<块 id>/p<行序号>`,例如 '6.1.0/p3'。块 id 只由数字和点组成,带 '/' 的不会和任何块 id 撞上。
|
||||
*/
|
||||
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}`
|
||||
/**
|
||||
* 一行或一个子行(parentId)下面展开的第几个子行:`<上一层的 id>/<子行序号>`,一层套一层往后接,
|
||||
* 例如 '6.1.0/p3/1' 是第 3 行的第 1 个子行,'6.1.0/p3/1/0' 是它下面的第 0 个。跳到定义时拿它在页面上找到要闪的子行;
|
||||
* 带下一层子行的子行(字典 / 对象的值又是字典 / 对象)也拿它当折叠 id,和属性行一样记进 folds、默认展开。
|
||||
* 只有行那一节带 'p',后面各节都是纯数字,不同位置的 id 不会撞上,也不会和块 id、属性行 id 撞上。
|
||||
*/
|
||||
export function propertyItemId(parentId: string, itemIndex: number): string {
|
||||
return `${parentId}/${itemIndex}`
|
||||
}
|
||||
Reference in new issue
Block a user