🐛 fix(vscode): 按种类筛选后仍能展开匹配块的完整主体,接口改叫路由
This commit is contained in:
1 parent
3abc87dbef
commit
6723c1dcc3
5 files changed
+152
-16
No files matched your search
@@ -2,7 +2,15 @@ import type { BlockTreeState } from './BlockTree'
|
||||
import type { BlockView, KindBadge } from './types'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { fileAttributes } from '../../../src/codeReader/attributes'
|
||||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
import { fastapiRule } from '../../../src/codeReader/rules/fastapi'
|
||||
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
|
||||
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
|
||||
import { BlockLevel } from './BlockTree'
|
||||
import { applyFilter } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
import { collapsedIds } from './viewState'
|
||||
|
||||
// 高亮的上下文所在的模块、编辑框都连着和 VS Code 收发消息的那一套,渲染成 HTML 用不着
|
||||
@@ -77,6 +85,62 @@ function render(folds: Record<string, boolean>): string {
|
||||
return renderToStaticMarkup(<BlockLevel blocks={[CARD]} tree={state(collapsedIds([CARD], folds))} />)
|
||||
}
|
||||
|
||||
describe('路由筛选后展开函数体', () => {
|
||||
it('真实 FastAPI 路由保留 if 和 return 子块,收起时有箭头,展开后显示完整主体及请求响应,菜单和标签统一叫路由', async () => {
|
||||
const source = [
|
||||
'from fastapi import APIRouter',
|
||||
'router = APIRouter()',
|
||||
'@router.put("/account")',
|
||||
'async def put_miniprogram_account(enabled: bool):',
|
||||
' if enabled:',
|
||||
' return "enabled-body"',
|
||||
' return "disabled-body"',
|
||||
'def unrelated():',
|
||||
' return "unrelated-body"',
|
||||
].join('\n')
|
||||
const language = createPythonAdapter(WASM_DIR)
|
||||
const blocks = await withBlocks(language, 'python', source, parsed => toBlockViews(
|
||||
parsed,
|
||||
{ source, language, markers: [] },
|
||||
pythonZhTranslator,
|
||||
{ rules: [fastapiRule], report: (message) => { throw new Error(message) } },
|
||||
))
|
||||
const route = blocks.find(block => block.kind === 'fastapi.route')!
|
||||
expect(route.children.map(block => block.kind)).toEqual(['if', 'return'])
|
||||
expect(route.children[0].children.map(block => block.kind)).toEqual(['return'])
|
||||
const attributes = fileAttributes(blocks, pythonZhTranslator.attributeNames)
|
||||
const excluded = { kind: attributes.find(attribute => attribute.id === 'kind')!.values.map(value => value.id).filter(kind => kind !== 'fastapi.route') }
|
||||
const filtered = applyFilter(blocks, excluded, new Set())
|
||||
const tree = { ...state(collapsedIds(blocks, {})), ...filtered }
|
||||
const closed = renderToStaticMarkup(<BlockLevel blocks={blocks} tree={tree} />)
|
||||
expect(closed).toContain('aria-label="展开子块"')
|
||||
expect(closed).not.toContain('enabled-body')
|
||||
const opened = renderToStaticMarkup(<BlockLevel blocks={blocks} tree={{ ...tree, collapsed: new Set() }} />)
|
||||
expect(opened).toContain('enabled-body')
|
||||
expect(opened).toContain('disabled-body')
|
||||
expect(opened).not.toContain('unrelated-body')
|
||||
expect(opened).toContain('请求')
|
||||
expect(opened).toContain('响应')
|
||||
expect(route.badge?.label).toBe('路由')
|
||||
expect(route.concepts.find(concept => concept.id === 'fastapi.route')?.label).toBe('路由')
|
||||
const bar = renderToStaticMarkup(
|
||||
<FilterBar
|
||||
attributes={attributes}
|
||||
excluded={excluded}
|
||||
view={filtered}
|
||||
highlight={undefined}
|
||||
onChange={() => {}}
|
||||
onMoveOccurrence={() => {}}
|
||||
translators={[]}
|
||||
translatorId="zh"
|
||||
onSelectTranslator={() => {}}
|
||||
/>,
|
||||
)
|
||||
expect(bar).toContain('种类:路由')
|
||||
expect(bar).not.toContain('接口')
|
||||
})
|
||||
})
|
||||
|
||||
describe('挂在卡片子行上的子块', () => {
|
||||
it('默认不画,子行的名称是展开它的按钮;卡片收着时留下的子块不画', () => {
|
||||
const html = render({})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { BlockKind, BlockView, FileAttribute } from './types'
|
||||
import type { BlockView, FileAttribute } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { applyFilter, blockPath, excludedBy, facetLabel, facetsOf, keptWhileEditing, setChecked, toggleValue } from './filter'
|
||||
|
||||
function block(id: string, kind: BlockKind, extra: Record<string, string> = {}, children: BlockView[] = []): BlockView {
|
||||
function block(id: string, kind: BlockView['kind'], extra: Record<string, string> = {}, children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind, ...extra }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
|
||||
}
|
||||
|
||||
@@ -41,15 +41,69 @@ describe('按筛选藏块', () => {
|
||||
|
||||
it('同一属性里勾着的值满足一个就显示,不同属性要同时满足', () => {
|
||||
const kinds = { kind: ['comment', 'if', 'return'] }
|
||||
expect([...applyFilter(BLOCKS, kinds, NONE).hidden].sort()).toEqual(['1.0', '1.0.0', '2.0', '3'])
|
||||
expect([...applyFilter(BLOCKS, kinds, NONE).hidden].sort()).toEqual(['3'])
|
||||
const both = applyFilter(BLOCKS, { access: ['private'], async: ['false'] }, NONE)
|
||||
expect([...both.hidden].sort()).toEqual(['0', '1.1', '2', '2.0'])
|
||||
expect(excludedBy(BLOCKS[1], { access: ['private'], async: ['false'] })).toBe(false)
|
||||
expect(excludedBy(BLOCKS[1], { async: ['true'] })).toBe(true)
|
||||
})
|
||||
|
||||
it('选中的函数保留完整主体,种类筛选不再截断其子孙;其他属性仍然逐块生效', () => {
|
||||
const kinds = { kind: ['const', 'if', 'return', 'comment'] }
|
||||
const view = applyFilter(BLOCKS, kinds, NONE)
|
||||
expect([...view.hidden].sort()).toEqual(['0', '3'])
|
||||
expect(view.hiddenCount).toBe(2)
|
||||
expect(view.temporary).toEqual(NONE)
|
||||
const both = applyFilter(BLOCKS, { ...kinds, access: ['private'] }, NONE)
|
||||
expect([...both.hidden].sort()).toEqual(['0', '1.1', '2', '2.0', '3'])
|
||||
expect(both.hiddenCount).toBe(4)
|
||||
})
|
||||
|
||||
it('嵌套路由保留通向它的祖先与完整主体,不带出祖先的其他子块或兄弟路由', () => {
|
||||
const blocks = [
|
||||
block('0', 'function', {}, [
|
||||
block('0.0', 'const'),
|
||||
block('0.1', 'function', {}, [
|
||||
block('0.1.0', 'fastapi.route', { httpMethod: 'PUT' }, [
|
||||
block('0.1.0.0', 'if', {}, [block('0.1.0.0.0', 'return')]),
|
||||
block('0.1.0.1', 'return'),
|
||||
]),
|
||||
block('0.1.1', 'return'),
|
||||
]),
|
||||
block('0.2', 'fastapi.route', { httpMethod: 'GET' }, [block('0.2.0', 'return')]),
|
||||
]),
|
||||
block('1', 'function', {}, [block('1.0', 'return')]),
|
||||
]
|
||||
const excluded = { kind: ['function', 'const', 'if', 'return'], httpMethod: ['GET'] }
|
||||
const view = applyFilter(blocks, excluded, NONE)
|
||||
expect([...view.hidden].sort()).toEqual(['0.0', '0.1.1', '0.2', '0.2.0', '1', '1.0'])
|
||||
expect(view.hiddenCount).toBe(4)
|
||||
expect(view.total).toBe(12)
|
||||
expect(view.temporary).toEqual(NONE)
|
||||
})
|
||||
|
||||
it('只看 return 时保留祖先路径,不显示路径旁的 const 和没有 return 的函数', () => {
|
||||
const view = applyFilter(BLOCKS, { kind: ['const', 'function', 'if', 'comment'] }, NONE)
|
||||
expect([...view.hidden].sort()).toEqual(['0', '1.1', '2', '2.0', '3'])
|
||||
expect(view.hiddenCount).toBe(4)
|
||||
})
|
||||
|
||||
it('种类全部排除后仍可临时显示目标路径,不带出被筛掉的兄弟', () => {
|
||||
const view = applyFilter(BLOCKS, { kind: ['const', 'function', 'if', 'return', 'comment'] }, new Set(blockPath('1.0.0')))
|
||||
expect([...view.hidden].sort()).toEqual(['0', '1.1', '2', '2.0', '3'])
|
||||
expect([...view.temporary]).toEqual(['1', '1.0', '1.0.0'])
|
||||
expect(view.hiddenCount).toBe(4)
|
||||
})
|
||||
|
||||
it('没有选中种类时全部隐藏,全选后全部恢复', () => {
|
||||
const all = ['const', 'function', 'if', 'return', 'comment']
|
||||
const excluded = setChecked({}, 'kind', all, false)
|
||||
expect(applyFilter(BLOCKS, excluded, NONE).hidden.size).toBe(8)
|
||||
expect(applyFilter(BLOCKS, setChecked(excluded, 'kind', all, true), NONE).hidden).toEqual(NONE)
|
||||
})
|
||||
|
||||
it('藏着的块里又被筛掉的子块不重复计数', () => {
|
||||
expect(applyFilter(BLOCKS, { kind: ['function', 'if'] }, NONE).hiddenCount).toBe(2)
|
||||
expect(applyFilter(BLOCKS, { kind: ['const', 'function', 'if', 'return'] }, NONE).hiddenCount).toBe(3)
|
||||
})
|
||||
|
||||
it('跳到定义临时露出来的:藏着的祖先下面只露出通向目标的路径,本来藏着的记进 temporary;已隐藏的块数照旧按筛选算', () => {
|
||||
@@ -57,10 +111,10 @@ describe('按筛选藏块', () => {
|
||||
expect([...view.hidden].sort()).toEqual(['0', '2', '2.0'])
|
||||
expect([...view.temporary]).toEqual(['1.1'])
|
||||
expect(view.hiddenCount).toBe(3)
|
||||
const path = applyFilter(BLOCKS, { kind: ['function'] }, new Set(blockPath('1.0')))
|
||||
expect([...path.hidden].sort()).toEqual(['1.1', '2', '2.0'])
|
||||
const path = applyFilter(BLOCKS, { access: ['public', 'private'] }, new Set(blockPath('1.0')))
|
||||
expect([...path.hidden].sort()).toEqual(['0', '1.1', '2', '2.0'])
|
||||
expect([...path.temporary].sort()).toEqual(['1', '1.0'])
|
||||
expect(path.hiddenCount).toBe(2)
|
||||
expect(path.hiddenCount).toBe(3)
|
||||
const visible = applyFilter(BLOCKS, { access: ['private'] }, new Set(blockPath('1.0')))
|
||||
expect(visible.temporary).toEqual(new Set())
|
||||
})
|
||||
@@ -69,9 +123,10 @@ describe('按筛选藏块', () => {
|
||||
const view = applyFilter(BLOCKS, { access: ['private'] }, new Set(blockPath('2')))
|
||||
expect([...view.temporary]).toEqual(['2'])
|
||||
expect([...view.hidden].sort()).toEqual(['0', '1.1'])
|
||||
const inner = applyFilter(BLOCKS, { kind: ['function', 'return'] }, new Set(blockPath('1')))
|
||||
expect([...inner.hidden].sort()).toEqual(['1.0.0', '2', '2.0'])
|
||||
expect(inner.hiddenCount).toBe(2)
|
||||
const inner = applyFilter(BLOCKS, { access: ['public', 'private'] }, new Set(blockPath('1')))
|
||||
expect([...inner.hidden].sort()).toEqual(['0', '1.1', '2', '2.0'])
|
||||
expect([...inner.temporary]).toEqual(['1'])
|
||||
expect(inner.hiddenCount).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -28,11 +28,28 @@ export function excludedBy(block: BlockView, excluded: Excluded): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按筛选算出哪些块不画:块被筛掉,连同它的子孙一起藏。keep 是跳到定义时要临时露出来的块(目标和它的各层祖先),
|
||||
* 其中本来藏着的照画、记进 temporary:藏着的祖先下面只露出通向目标的那条路径,目标自己和没临时显示的块一样,
|
||||
* 它的子块照常按筛选显示(函数照样能展开看主体)。
|
||||
* 种类筛选保留匹配块的完整主体及通向它的祖先,其余属性仍能隐藏整棵子树,让选中的卡片可以展开阅读。
|
||||
*
|
||||
* @param blocks 文档的顶层代码块。
|
||||
* @param excluded 每个筛选属性不显示的值。
|
||||
* @param keep 跳到定义时临时显示的目标及祖先,原本隐藏的块会记入 `temporary`,目标的子块仍按筛选显示。
|
||||
*/
|
||||
export function applyFilter(blocks: readonly BlockView[], excluded: Excluded, keep: ReadonlySet<string>): FilterView {
|
||||
const { kind: excludedKinds = [], ...otherAttributes } = excluded
|
||||
const kindVisible = new Set<string>()
|
||||
const selectKinds = (list: readonly BlockView[], matchedAbove: boolean): boolean => {
|
||||
let anyVisible = false
|
||||
for (const block of list) {
|
||||
const matched = matchedAbove || !excludedKinds.includes(block.attributes.kind)
|
||||
const childVisible = selectKinds(block.children, matched)
|
||||
if (matched || childVisible) {
|
||||
kindVisible.add(block.id)
|
||||
anyVisible = true
|
||||
}
|
||||
}
|
||||
return anyVisible
|
||||
}
|
||||
selectKinds(blocks, false)
|
||||
const hidden = new Set<string>()
|
||||
const temporary = new Set<string>()
|
||||
let hiddenCount = 0
|
||||
@@ -41,7 +58,7 @@ export function applyFilter(blocks: readonly BlockView[], excluded: Excluded, ke
|
||||
const visit = (list: readonly BlockView[], filteredAbove: boolean, hiddenAbove: boolean): void => {
|
||||
for (const block of list) {
|
||||
total += 1
|
||||
const out = excludedBy(block, excluded)
|
||||
const out = !kindVisible.has(block.id) || excludedBy(block, otherAttributes)
|
||||
if (out && !filteredAbove)
|
||||
hiddenCount += 1
|
||||
const hiddenHere = hiddenAbove || out
|
||||
|
||||
Reference in new issue
Block a user