✨ feat(vscode): 代码阅读预览里实参值、return 这类一行译文里跨几行写的字典 / 列表按项拆开,各项带自己的实参表
This commit is contained in:
1 parent
b2c5309d1f
commit
907c4092c8
18 files changed
+515
-102
No files matched your search
@@ -1,3 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { BlockTreeState, RewriteTree } from './BlockTree'
|
||||
import type { Ask, ChatState } from './qa'
|
||||
import type { BlockView, KindBadge } from './types'
|
||||
@@ -240,3 +242,85 @@ describe('「注释」一行和「重写注释」按钮', () => {
|
||||
expect(html(view('4', 'statement', 'save()'))).not.toContain('按注释规范')
|
||||
})
|
||||
})
|
||||
|
||||
describe('一行译文下面拆开的各项', () => {
|
||||
/** 一段 Python 里函数体的第一块(return)经过宿主转成的视图数据;Filter 的定义是类。 */
|
||||
async function returned(source: string): Promise<BlockView> {
|
||||
const language = createPythonAdapter(WASM_DIR)
|
||||
const views = await withBlocks(language, 'python', source, blocks => toBlockViews(blocks, { source, language, markers: [], calleeKind: call => call.callee.text === 'Filter' ? 'class' : undefined }, pythonZhTranslator))
|
||||
return views[0].children[0]
|
||||
}
|
||||
|
||||
const SOURCE = [
|
||||
'def list_filters(archived, current_user):',
|
||||
' return [',
|
||||
' Filter(field="user_id", value=current_user.id),',
|
||||
' Filter(',
|
||||
' field="archived_at",',
|
||||
' value=None,',
|
||||
' ),',
|
||||
' {"k": v},',
|
||||
' ]',
|
||||
].join('\n')
|
||||
|
||||
/** 画成页面(只画这一块),collapsed 是折叠着的 id */
|
||||
function page(block: BlockView, collapsed: ReadonlySet<string> = new Set(), diff?: BlockTreeState['diff']): HTMLElement {
|
||||
const root = document.createElement('div')
|
||||
root.innerHTML = renderToStaticMarkup(<BlockLevel blocks={[block]} tree={{ ...state(collapsed), ...(diff && { diff }) }} />)
|
||||
return root
|
||||
}
|
||||
|
||||
function byId(root: HTMLElement, id: string): HTMLElement {
|
||||
const found = root.querySelector<HTMLElement>(`[data-id="${id}"]`)
|
||||
if (!found)
|
||||
throw new Error(`页面上没有 ${id}`)
|
||||
return found
|
||||
}
|
||||
|
||||
/** a 在页面上排在 b 前面 */
|
||||
function before(a: Element, b: Element): boolean {
|
||||
return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
||||
}
|
||||
|
||||
it('各项排在译文下面、往右缩一层、左边一条竖线,和变量初始值的各项一样;项下面挂着它的实参表,排在下一项上面', async () => {
|
||||
const block = await returned(SOURCE)
|
||||
const root = page(block)
|
||||
const line = root.querySelector('.cr-code')
|
||||
expect(line?.textContent).toBe('返回 列表 3 项')
|
||||
const first = byId(root, `${block.id}/i/0`)
|
||||
const second = byId(root, `${block.id}/i/1`)
|
||||
expect(first.textContent).toBe('第 1 项新建 Filter · 2 个参数')
|
||||
expect(second.textContent).toBe('第 2 项新建 Filter · 2 个参数')
|
||||
const cell = byId(root, `${block.id}/i/1/t/0`)
|
||||
expect(cell.textContent).toBe('field"archived_at"')
|
||||
expect(before(line!, first) && before(second, cell) && before(cell, byId(root, `${block.id}/i/2`))).toBe(true)
|
||||
expect(first.closest('.cr-table')).toBeNull()
|
||||
let level = first.parentElement
|
||||
while (level && !level.firstElementChild?.classList.contains('cr-guide'))
|
||||
level = level.parentElement
|
||||
expect(level?.contains(second)).toBe(true)
|
||||
expect(level?.contains(line)).toBe(false)
|
||||
})
|
||||
|
||||
it('项下面的下一层(字典)能折叠,折着时只收起那一层;实参表总是展开的', async () => {
|
||||
const block = await returned(SOURCE)
|
||||
const map = `${block.id}/i/2`
|
||||
const open = page(block)
|
||||
expect(byId(open, map).querySelector('button')?.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(byId(open, `${map}/0`).textContent).toBe('"k"v')
|
||||
const folded = page(block, new Set([map]))
|
||||
expect(folded.querySelector(`[data-id="${map}/0"]`)).toBeNull()
|
||||
expect(folded.querySelector(`[data-id="${block.id}/i/0/t/0"]`)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('标差异:新加的项淡绿底,删掉的项放回原来的位置、划掉', async () => {
|
||||
const block = await returned(SOURCE)
|
||||
const gone = { cells: [[{ text: '第 3 项', role: 'hint' as const }], [{ text: 'old', role: 'variable' as const }]] }
|
||||
const changes = new Map([[block.id, { change: 'mod' as const, own: true as const, marks: { items: { changed: { 2: { added: true as const } }, removed: [{ after: 1, value: gone }] } } }]])
|
||||
const root = page(block, new Set(), { changes, removed: new Map(), current: true })
|
||||
expect(byId(root, `${block.id}/i/2`).dataset.diff).toBe('add')
|
||||
const removed = root.querySelector<HTMLElement>('[data-diff="del"]')
|
||||
expect(removed?.textContent).toBe('第 3 项old')
|
||||
expect(before(byId(root, `${block.id}/i/1`), removed!) && before(removed!, byId(root, `${block.id}/i/2`))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -13,10 +13,10 @@ import { CodeEditor } from './CodeEditor'
|
||||
import { DiffText } from './DiffText'
|
||||
import { removedTitle } from './diffView'
|
||||
import { KindIcon } from './kindIcons'
|
||||
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
||||
import { PropertyGrid, PropertyItems, PropertyTable } from './PropertyGrid'
|
||||
import { asksOf, historyOf, unfinished } from './qa'
|
||||
import { TextPreview } from './TextPreview'
|
||||
import { tableId } from './viewState'
|
||||
import { listId, tableId } from './viewState'
|
||||
|
||||
/**
|
||||
* 块树里改代码的那部分:哪些块在编辑、点「编辑」。页面上的块树才有;定义卡片浮窗里的块树没有,
|
||||
@@ -234,17 +234,24 @@ function BlockRewrite({ anchor, rewrite }: { anchor: string, rewrite: RewriteTre
|
||||
|
||||
/**
|
||||
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
|
||||
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明、几行属性(「注释」一行)。标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
|
||||
* 译文下面依次接着拆开的各项(跨几行写的字面量,排法同变量初始值下面的)、摊开实参的表(跨几行写的调用)、说明、几行属性(「注释」一行)。
|
||||
* 标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
|
||||
*/
|
||||
function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTreeState, marks?: BlockMarks }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} slots={cardSlots(block, tree)} />
|
||||
const folding = { collapsed: tree.collapsed, onToggleFold: tree.onToggleFold }
|
||||
return (
|
||||
<>
|
||||
{marks?.translation ? <DiffText diff={marks.translation} /> : <TextPreview text={block.translation} />}
|
||||
{block.items && (
|
||||
<div className="mt-[3px]">
|
||||
<PropertyItems parentId={listId(block.id)} items={block.items} folding={folding} marks={marks?.items} />
|
||||
</div>
|
||||
)}
|
||||
{block.table && (
|
||||
<div className="mt-[3px]">
|
||||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} folding={{ collapsed: tree.collapsed, onToggleFold: tree.onToggleFold }} />
|
||||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} folding={folding} />
|
||||
</div>
|
||||
)}
|
||||
{marks?.doc ? <DiffText diff={marks.doc} className="mt-0.5" /> : block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||||
|
||||
@@ -133,12 +133,12 @@ function Nested({ children }: { children: ReactNode }): ReactElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
|
||||
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。一行译文下面拆开的各项也用它。
|
||||
* 带下一层的子行(字典 / 对象、跨几行写的列表的各项),下一层展开在它下面、再往右缩进一层,默认展开,能折叠;
|
||||
* 带实参表的子行(列表里整项是调用),表排在它下面、同样往右缩进一层,总是展开。
|
||||
* 标差异时(marks)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
|
||||
*/
|
||||
function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
|
||||
export function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
|
||||
const nested = items.some(item => (item.items?.length ?? 0) > 0)
|
||||
/** 一个子行:删掉的(change 是 del)不带 id、下面的子行总是展开 */
|
||||
const itemRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
|
||||
|
||||
@@ -134,6 +134,23 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(occurrencePlan(blocks, { start: 140, end: 147, kind: 'read' }, () => false)).toEqual({ id: '1', expand: [] })
|
||||
})
|
||||
|
||||
it('名字在一行译文下面拆开的各项里:各项总是展开的,讲这个定义的项(字典的键)只闪它;项下面的下一层要展开那一项,实参表总是展开的', () => {
|
||||
const blocks: BlockView[] = [
|
||||
{ ...textBlock('0', 'return', 0, [word('返回'), word('列表 2 项')]), items: [
|
||||
{ cells: [[word('第 1 项')], [word('新建'), name('Filter', 10)]], table: { columns: ['参数', '值'], items: [{ cells: [[name('field', 20)], [name('user_id', 30)]] }] } },
|
||||
{ cells: [[word('第 2 项')], [word('字典 1 项')]], items: [{ cells: [[name('key', 40)], [name('value', 45)]], defines: { startIndex: 40, endIndex: 43 } }] },
|
||||
] },
|
||||
{ ...textBlock('1', 'return', 5, [word('返回'), word('对象 1 项')]), items: [{ cells: [[name('locked', 60)], [name('flag', 70)]], defines: { startIndex: 60, endIndex: 66 } }] },
|
||||
]
|
||||
expect(revealPlan(blocks, 0, 10)).toEqual({ id: '0', expand: [] })
|
||||
expect(revealPlan(blocks, 0, 30)).toEqual({ id: '0', expand: [] })
|
||||
expect(revealPlan(blocks, 0, 40)).toEqual({ id: '0', row: '0/i/1/0', expand: ['0/i/1'] })
|
||||
expect(revealPlan(blocks, 0, 45)).toEqual({ id: '0', expand: ['0/i/1'] })
|
||||
expect(revealPlan(blocks, 5, 60)).toEqual({ id: '1', row: '1/i/0', expand: [] })
|
||||
expect(occurrencePlan(blocks, { start: 45, end: 50, kind: 'read' }, () => false)).toEqual({ id: '0', expand: ['0/i/1'] })
|
||||
expect(occurrencePlan(blocks, { start: 30, end: 37, kind: 'read' }, () => false)).toEqual({ id: '0', expand: [] })
|
||||
})
|
||||
|
||||
it('名字在框里(套了几层都算)也是显示着它', () => {
|
||||
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
|
||||
const blocks: BlockView[] = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BlockView, Inline, Occurrence, PreviewTable, PreviewText, PropertyItem, PropertyRow, SourceRange } from './types'
|
||||
import { blockPath } from './filter'
|
||||
import { showsOccurrence, sourceOf } from './occurrences'
|
||||
import { propertyItemId, propertyRowId, tableId, unfoldToShow } from './viewState'
|
||||
import { listId, propertyItemId, propertyRowId, tableId, unfoldToShow } from './viewState'
|
||||
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */
|
||||
export interface RevealPlan {
|
||||
@@ -87,34 +87,46 @@ function rowItems(row: PropertyRow, id: string): Generator<PlacedItem> {
|
||||
return nestedItems(row.items, id, [id], !row.columns)
|
||||
}
|
||||
|
||||
/** 一行译文下面那张实参表(总是展开的)里一层层的子行。 */
|
||||
function tableItems(blockId: string, table: PreviewTable | undefined): Generator<PlacedItem> {
|
||||
return nestedItems(table?.items, tableId(blockId), [], false)
|
||||
/** 一行译文下面一层层的子行:拆开的各项(这一层总是展开的,项下面的下一层折叠在项上),摊开实参的表(总是展开的)。 */
|
||||
function* textItems(blockId: string, items: PropertyItem[] | undefined, table: PreviewTable | undefined): Generator<PlacedItem> {
|
||||
yield* nestedItems(items, listId(blockId), [], true)
|
||||
yield* nestedItems(table?.items, tableId(blockId), [], false)
|
||||
}
|
||||
|
||||
/** 一层层的子行里,讲 offset 处这个定义的那一个(defines 盖住 offset):只闪它,展开它上面的各层。 */
|
||||
function definedItem(items: Iterable<PlacedItem>, offset: number): Spot | undefined {
|
||||
for (const { item, id, expand } of items) {
|
||||
if (item.defines && covers(item.defines, offset))
|
||||
return { row: id, expand, span: width(item.defines) }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它,
|
||||
* ② 属性表、一行译文下面的子行里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它,
|
||||
* 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开;
|
||||
* ③ 块里只是显示着这个名字(一段源码范围盖住 offset:一行译文和它下面的表里的名字、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* ③ 块里只是显示着这个名字(一段源码范围盖住 offset:一行译文和它下面拆开的各项、表里的名字,没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* span 是盖住 offset 的那段有多长。不在这块里是 undefined。
|
||||
*/
|
||||
function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
if (block.defines && covers(block.defines, offset))
|
||||
return { expand: [], span: width(block.defines) }
|
||||
if ('translation' in block) {
|
||||
const defined = definedItem(textItems(block.id, block.items, block.table), offset)
|
||||
if (defined)
|
||||
return defined
|
||||
const span = textSpan(block.translation, offset)
|
||||
return span === undefined ? shownItem(tableItems(block.id, block.table), offset) : { expand: [], span }
|
||||
return span === undefined ? shownItem(textItems(block.id, block.items, block.table), offset) : { expand: [], span }
|
||||
}
|
||||
const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) }))
|
||||
for (const { row, id } of rows) {
|
||||
if (row.defines && covers(row.defines, offset))
|
||||
return { row: id, expand: [], span: width(row.defines) }
|
||||
for (const { item, id: itemId, expand } of rowItems(row, id)) {
|
||||
if (item.defines && covers(item.defines, offset))
|
||||
return { row: itemId, expand, span: width(item.defines) }
|
||||
}
|
||||
const defined = definedItem(rowItems(row, id), offset)
|
||||
if (defined)
|
||||
return defined
|
||||
}
|
||||
for (const { row, id } of rows) {
|
||||
const value = textSpan(row.value, offset)
|
||||
@@ -186,10 +198,10 @@ export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap<string, str
|
||||
return owner === undefined ? plan : { id: owner, expand: blockPath(owner).slice(0, -1) }
|
||||
}
|
||||
|
||||
/** 块里哪儿显示着满足 test 的文字(译文和它下面的表、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
/** 块里哪儿显示着满足 test 的文字(译文和它下面拆开的各项、表,属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
function showingSpot(block: BlockView, test: (text: PreviewText) => boolean): string[] | undefined {
|
||||
if ('translation' in block)
|
||||
return test(block.translation) ? [] : showingItem(tableItems(block.id, block.table), test)
|
||||
return test(block.translation) ? [] : showingItem(textItems(block.id, block.items, block.table), test)
|
||||
for (const [index, row] of block.properties.entries()) {
|
||||
if (test(row.value))
|
||||
return []
|
||||
|
||||
@@ -181,9 +181,10 @@ export type CommentTarget
|
||||
|
||||
/**
|
||||
* 函数、类、枚举、导入、常量、变量块和库规则的卡片预览时是属性表,文件开头的说明是 Markdown,其余是一行译文;
|
||||
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(table),块的说明(doc,Markdown 原文),几行属性(rows:「注释」一行)
|
||||
* 一行译文下面依次是:跨几行写的字面量拆开的各项(items),跨几行写的调用摊开各个实参的表(table),块的说明(doc,Markdown 原文),
|
||||
* 几行属性(rows:「注释」一行)
|
||||
*/
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } | { properties: PropertyRow[] })
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, items?: PropertyItem[], table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } | { properties: PropertyRow[] })
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
@@ -407,10 +408,11 @@ export interface RowDiff {
|
||||
items?: ItemsDiff
|
||||
}
|
||||
|
||||
/** 一块自己的内容(不算子块)改了哪些:属性表(一行译文的卡片是下面那几行 rows)逐行,一行译文逐截,摊开实参的表逐行,说明逐词 */
|
||||
/** 一块自己的内容(不算子块)改了哪些:属性表(一行译文的卡片是下面那几行 rows)逐行,一行译文逐截,译文下面拆开的各项、摊开实参的表逐行,说明逐词 */
|
||||
export interface BlockMarks {
|
||||
rows?: RowsDiff
|
||||
translation?: TextDiff
|
||||
items?: ItemsDiff
|
||||
table?: ItemsDiff
|
||||
doc?: TextDiff
|
||||
}
|
||||
|
||||
@@ -124,6 +124,14 @@ export function tableId(ownerId: string): string {
|
||||
return `${ownerId}/t`
|
||||
}
|
||||
|
||||
/**
|
||||
* 一行译文下面拆开的各项(跨几行写的列表、字典)的上一层 id:`<块 id>/i`,各项的 id 是 propertyItemId(它, 项号)。
|
||||
* 这一层总是展开的,项下面的下一层照常折叠在项的 id 上。
|
||||
*/
|
||||
export function listId(blockId: string): string {
|
||||
return `${blockId}/i`
|
||||
}
|
||||
|
||||
/**
|
||||
* 父块看得见时,要让它的一个子块也看得见得展开的(块 id、属性行 id):一般的子块是父块自己(收起子块时不画);
|
||||
* 挂在父卡片子行上的子块(BlockSlot)是它所在的那一行和它自己。
|
||||
|
||||
Reference in new issue
Block a user