✨ feat(vscode): React 钩子卡片按变量讲,属性表加操作列、调用树能查所有引用

- 钩子调用讲成状态、引用、记忆值、记忆函数、上下文值、过渡、副作用、钩子返回值、钩子调用几种卡片,行照签名表 reactHooks.ts 排,去掉 react.hook
- 解构接住时名称一行写出解构,展开是「第几项 | 名称 | 说明」表,每项一个调用树操作;单个名字的按钮在名称一行最右边
- 属性表的行、子行可以带操作(RowAction),表格多一列「操作」,宿主按块记号加序号起键
- 调用树服务按键数次数,新增查所有引用(references):按所在函数分组,去掉定义处和依赖数组里的
- 钩子收的函数参数都收成小框,函数体是卡片子块,直接返回的表达式由 functionBody 排成「返回 …」卡片;小框和卡片折叠箭头同一个开关
- 规则卡片可指定语言种类图标(languageKind)、种类图标悬停字和颜色(KindBadge.tooltip / iconColor)
This commit is contained in:
cruldra committed 2026-10-16 22:16:09 +11:00
1 parent a139341132
commit 86beaac371
38 files changed
+1971 -813

No files matched your search

@@ -3,6 +3,8 @@
import type { BlockTreeHandle, BlockTreeState, RewriteTree } from './BlockTree'
import type { Ask, ChatState } from './qa'
import type { BlockView, KindBadge } from './types'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { fileAttributes } from '../../../src/codeReader/attributes'
@@ -151,13 +153,64 @@ describe('路由筛选后展开函数体', () => {
})
describe('库规则卡片的淡底', () => {
it('卡片给了 tint 时淡底用它、描边仍是种类色(React 钩子卡片按来源分底色):两个颜色都写在卡片上', () => {
const tinted: BlockView = { ...view('0', 'react.hook', 'useRouter'), badge: BADGE, tint: { dark: '#e5c07b', light: '#9a6a00' } }
expect(markup([tinted], tree())).toContain('data-kind="react.hook" data-badge="" data-tint="" style="--kind-dark:#61dafb;--kind-light:#0a7ea4;--tint-dark:#e5c07b;--tint-light:#9a6a00"')
it('卡片给了 tint 时淡底用它、描边仍是种类色(React 钩子返回值按来源分底色):两个颜色都写在卡片上', () => {
const tinted: BlockView = { ...view('0', 'react.hookValue', 'useRouter'), badge: BADGE, tint: { dark: '#e5c07b', light: '#9a6a00' } }
expect(markup([tinted], tree())).toContain('data-kind="react.hookValue" data-badge="" data-tint="" style="--kind-dark:#61dafb;--kind-light:#0a7ea4;--tint-dark:#e5c07b;--tint-light:#9a6a00"')
expect(markup([{ ...tinted, tint: undefined }], tree())).not.toContain('data-tint')
})
})
describe('卡片里收起来的函数小框', () => {
/** React 记忆值卡片:「计算」一行是收起来的函数,函数体(一张返回卡片)是卡片下面那一层的子块 */
const memo = (children: BlockView[]): BlockView => ({
...view('0', 'react.memo', ''),
badge: BADGE,
properties: [
{ label: '名称', value: [{ text: 'visible', role: 'variable' }] },
{ label: '计算', value: [{ style: 'fold', label: '函数', segments: [{ text: '· 1 句', role: 'hint' }] }] },
],
children,
})
const RETURN = view('0.0', 'return', '返回 results.filter(…)')
const chip = (html: string): string => html.match(/<button[^>]*data-style="fold"[^>]*>/)?.[0] ?? ''
it('卡片有下面那一层的子块时,小框能点、和卡片的折叠箭头同开同关', () => {
const closed = chip(markup([memo([RETURN])], tree({ collapsed: new Set(['0']) })))
expect(closed).toContain('aria-expanded="false"')
expect(closed).toContain('aria-label="展开函数"')
expect(closed).not.toContain('disabled')
const open = chip(markup([memo([RETURN])], tree({ collapsed: new Set() })))
expect(open).toContain('aria-expanded="true"')
expect(open).toContain('data-open=""')
})
it('点小框切换的是卡片自己的折叠', async () => {
const toggled: string[] = []
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
await act(async () => root.render(<BlockTree blocks={[memo([RETURN])]} tree={{ ...tree({ collapsed: new Set(['0']) }), onToggleFold: id => toggled.push(id) }} />))
await act(async () => container.querySelector<HTMLButtonElement>('[data-style="fold"]')!.click())
expect(toggled).toEqual(['0'])
await act(async () => root.unmount())
container.remove()
vi.unstubAllGlobals()
})
it('卡片没有子块、子块都挂在子行上时,小框只是不能点的小框,也不沿用外层卡片的开关', () => {
expect(chip(markup([memo([])], tree()))).toContain('disabled')
const slotted = view('0.0', 'demo.method', '', { slot: { row: 0, item: 0 } })
expect(chip(markup([memo([slotted])], tree()))).toContain('disabled')
// 挂在外层卡片子行上的子卡片画在外层卡片的属性表里,外层卡片自己有下面那一层的子块
const inner: BlockView = { ...memo([]), id: '0.0', slot: { row: 1, item: 0 } }
const outer: BlockView = { ...CARD, children: [inner, view('0.2', 'statement', '留下的语句')] }
const html = markup([outer], tree({ collapsed: new Set() }))
expect(html).toContain('计算')
expect(chip(html)).toContain('disabled')
})
})
describe('挂在卡片子行上的子块', () => {
it('默认不画,子行的名称是展开它的按钮;卡片收着时留下的子块不画', () => {
const html = render({})