✨ feat(vscode): 代码阅读把字典、对象字面量的值展开成键值树

This commit is contained in:
cruldra committed 2026-09-25 20:57:27 +08:00
1 parent 51f2ef7bfa
commit 4930dc0d42
20 files changed
+578 -103

No files matched your search

@@ -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>
)
})}