✨ feat(vscode): 代码阅读页面画链式调用卡片:竖轨道、小链、收起的闭包和合成的表

链的各步排在值下面的竖轨道上,方法一列对齐、轨道线是函数色淡到 35%;实参是链的缩进成空心点小链,注释挂在步骤下;收起的闭包是能点的小框,点开那一步才画挂着的子块(折叠 id 记在属性行下面);跳到定义、查找、高度估算、只看改动都算上链里的字和步骤下的子块
This commit is contained in:
cruldra committed 2026-10-15 21:05:07 +11:00
1 parent ee595f3da9
commit 1956879aa1
13 files changed
+531 -47

No files matched your search

@@ -9,9 +9,11 @@ import { fileAttributes } from '../../../src/codeReader/attributes'
import { readBlockViews, toBlockViews } from '../../../src/codeReader/blockView'
import { pageBlocks } from '../../../src/codeReader/editor/pageBlocks'
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
import { createRustAdapter } from '../../../src/codeReader/languages/rust'
import { fastapiRule } from '../../../src/codeReader/rules/fastapi'
import { runtime, WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
import { readFixture, runtime, WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
import { rustZhTranslator } from '../../../src/codeReader/translators/rustZh'
import { BlockTree, treeState } from './BlockTree'
import { applyFilter } from './filter'
import { FilterBar } from './FilterBar'
@@ -19,7 +21,7 @@ import { NO_PLACEMENT, placementOf } from './placement'
import { EMPTY_CHAT } from './qa'
import { staticStore } from './store'
import { NO_SPAN } from './testBlocks'
import { collapsedIds } from './viewState'
import { chainStepId, collapsedIds } from './viewState'
// 编辑框连着和 VS Code 收发消息的那一套,渲染成 HTML 用不着
vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } }))
@@ -323,6 +325,81 @@ describe('一行译文下面拆开的各项', () => {
})
})
describe('链式调用卡片', () => {
/** 一段 Rust 经过宿主转成的发给页面的块:第一个函数和它函数体里的块 */
async function rustBlocks(source: string): Promise<BlockView[]> {
const language = createRustAdapter(WASM_DIR)
const views = await withBlocks(language, 'rust', source, blocks => toBlockViews(blocks, { source, language, markers: [] }, rustZhTranslator))
return pageBlocks(views, source).filter(block => block.kind === 'function')
}
/** 画成页面,函数展开着;open 是点开的那几步(折叠 id) */
async function chainPage(source: string, open: string[] = []): Promise<{ root: HTMLElement, chain: BlockView }> {
const [fn] = await rustBlocks(source)
const folds = Object.fromEntries([[fn.id, false], ...open.map(id => [id, false])])
const root = document.createElement('div')
root.innerHTML = markup([fn], tree({ collapsed: collapsedIds([fn], folds) }))
const chain = fn.children.find(child => 'translation' in child && child.rows?.some(row => row.chain))!
return { root, chain }
}
it('起点、步骤两行,链的各步排在「步骤」那一行的值下面的竖轨道上:一步一个点、方法和值;合成的一步下面是各次实参的表;写在上面的注释挂在那一步下面', async () => {
const { root, chain } = await chainPage(readFixture('rustChain.rs'))
const card = root.querySelector(`[data-id="${chain.id}"]`)!
expect(card.querySelector('.cr-code')?.closest('.cr-prop-row')).not.toBeNull()
const steps = [...card.querySelectorAll('.cr-chain-step')]
expect(steps.map(step => step.querySelector('.cr-chain-line')?.textContent)).toEqual([
'.plugin4 次',
'.setup闭包|app| · 3 句',
'.on_window_event闭包|window, event| · 1 句',
'.manageapp_state',
'.invoke_handlertauri::generate_handler![ device_resume, settings_get, web::preview_open ]',
'.buildtauri::generate_context!()',
'.expect"error while building tauri application"',
'.run闭包|app, event| · match event · 2 支',
])
expect(steps.every(step => step.querySelector(':scope > .cr-chain-rail > .cr-chain-dot'))).toBe(true)
const plugin = steps[0]
expect(plugin.querySelector('.cr-chain-label')?.textContent).toBe('注释')
expect(plugin.querySelector('.cr-table-head')?.textContent).toBe('实参')
expect([...plugin.querySelectorAll('.cr-table-row')].map(row => row.textContent)).toHaveLength(4)
expect(card.closest('.cr-prop-row')).toBeNull()
})
it('收起来的闭包是一个按钮,默认收着、闭包体的子块不画;点开那一步(它的折叠 id 展开)后子块画在那一步下面,别的步照旧收着', async () => {
const source = readFixture('rustChain.rs')
const closed = (await chainPage(source)).root
const setupFold = closed.querySelectorAll('.cr-chain-step')[1].querySelector('button[data-style="fold"]')!
expect(setupFold.getAttribute('aria-expanded')).toBe('false')
expect(setupFold.getAttribute('aria-label')).toBe('展开闭包')
expect(closed.textContent).not.toContain('tunnels.json')
const { root, chain } = await chainPage(source, [chainStepId(chain0(await rustBlocks(source)), { row: 1, step: 1 })])
const [, setup, windowEvent] = root.querySelectorAll('.cr-chain-step')
expect(setup.querySelector('button[data-style="fold"]')?.getAttribute('aria-expanded')).toBe('true')
expect(setup.querySelector('.cr-chain-blocks')?.textContent).toContain('tunnels.json')
expect(windowEvent.querySelector('.cr-chain-blocks')).toBeNull()
expect(root.textContent).not.toContain('window.hide')
// 挂在步骤下面的子块不再排到卡片后面那一层
expect(root.querySelector(`[data-id="${chain.id}"] + .cr-level`)).toBeNull()
})
it('实参本身是链的一步在值下面缩进成一条小链(空心点),一节一行;变量的初始值是起点,链接在下面', async () => {
const source = 'fn f() {\n let names = rows.iter().filter(|r| r.active).collect();\n tauri::Builder::default()\n .plugin(\n opener::Builder::new()\n .open(false)\n .build(),\n )\n .manage(state);\n}\n'
const { root } = await chainPage(source)
const mini = root.querySelector('.cr-chain-mini')!
expect([...mini.querySelectorAll('.cr-chain-step')].map(line => line.textContent)).toEqual(['opener::Builder::new()', '.open(false)', '.build()'])
expect(mini.querySelectorAll('.cr-chain-dot[data-hollow]')).toHaveLength(3)
const value = [...root.querySelectorAll('.cr-prop-row')].find(row => row.textContent?.startsWith('初始值'))!
expect(value.querySelector('.cr-code')?.textContent).toBe('rows')
expect([...value.querySelectorAll('.cr-chain-line')].map(line => line.textContent)).toEqual(['遍历', '筛选|r| r.active', '收集'])
})
})
/** 一个函数体里第一张链式调用卡片的块 id */
function chain0(blocks: BlockView[]): string {
return blocks[0].children.find(child => 'translation' in child && child.rows?.some(row => row.chain))!.id
}
describe('编辑框', () => {
it('编辑着的块:编辑框里是文档源码按它的编辑范围取的整行、去掉共同缩进,着色取自整份源码的着色', async () => {
const source = 'def f(x):\n if x:\n return "a"\n'