🐛 fix(vscode): 调用树按钮一直显示,点开时没数到就当场再查
This commit is contained in:
1 parent
6e8be1447a
commit
83d1f4a675
5 files changed
+152
-36
No files matched your search
@@ -100,6 +100,94 @@ describe('调用树棵数在文档发出去之后算', () => {
|
||||
}])
|
||||
session.dispose()
|
||||
})
|
||||
|
||||
it('后台没数出调用者的块,点开时当场再问一次;这回问到了就发树,并补上棵数', async () => {
|
||||
const mask = symbol('mask_phone')
|
||||
const caller = symbol('get_profile')
|
||||
const edge: CallEdge = { caller, callee: mask, at: { line: 2, character: 0, endLine: 2, endCharacter: 3 } }
|
||||
let answering = false
|
||||
let asked = 0
|
||||
const source: CallTreeSource = {
|
||||
version: 5,
|
||||
blocks: [block('fn', 'function', 1)],
|
||||
load: async () => {
|
||||
asked++
|
||||
return { symbol: mask, edges: answering ? [edge] : [] }
|
||||
},
|
||||
}
|
||||
const posted: unknown[] = []
|
||||
const session = new CallTreeSession(message => posted.push(message), () => source)
|
||||
session.refresh()
|
||||
await viFlush()
|
||||
expect(posted).toEqual([{ type: 'callTreeCounts', version: 5, counts: {} }])
|
||||
posted.length = 0
|
||||
answering = true
|
||||
session.open(5, 'fn')
|
||||
await viFlush()
|
||||
expect(asked).toBe(2)
|
||||
expect(posted).toEqual([
|
||||
{ type: 'callTreeCounts', version: 5, counts: { fn: 1 } },
|
||||
{
|
||||
type: 'callTree',
|
||||
version: 5,
|
||||
anchor: 'fn',
|
||||
name: 'mask_phone',
|
||||
where: 'mask_phone.py:1',
|
||||
trees: [expect.objectContaining({ name: 'get_profile', hits: 1 })],
|
||||
},
|
||||
])
|
||||
posted.length = 0
|
||||
session.open(5, 'fn')
|
||||
expect(asked).toBe(2)
|
||||
expect(posted).toEqual([expect.objectContaining({ type: 'callTree', anchor: 'fn' })])
|
||||
session.dispose()
|
||||
})
|
||||
|
||||
it('点开时当场问还是没有调用者,就发一棵空的', async () => {
|
||||
const source: CallTreeSource = {
|
||||
version: 6,
|
||||
blocks: [block('fn', 'function', 1)],
|
||||
load: async () => ({ symbol: symbol('nobody'), edges: [] }),
|
||||
}
|
||||
const posted: unknown[] = []
|
||||
const session = new CallTreeSession(message => posted.push(message), () => source)
|
||||
session.refresh()
|
||||
await viFlush()
|
||||
posted.length = 0
|
||||
session.open(6, 'fn')
|
||||
await viFlush()
|
||||
expect(posted).toEqual([{ type: 'callTree', version: 6, anchor: 'fn', name: '', where: '', trees: [] }])
|
||||
session.dispose()
|
||||
})
|
||||
|
||||
it('当场问到一半文档改了,结果不发', async () => {
|
||||
let release: (() => void) | undefined
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
let version = 7
|
||||
const mask = symbol('mask_phone')
|
||||
const edge: CallEdge = { caller: symbol('get_profile'), callee: mask, at: { line: 2, character: 0, endLine: 2, endCharacter: 3 } }
|
||||
const source: CallTreeSource = {
|
||||
get version() {
|
||||
return version
|
||||
},
|
||||
blocks: [block('fn', 'function', 1)],
|
||||
load: async () => {
|
||||
await gate
|
||||
return { symbol: mask, edges: [edge] }
|
||||
},
|
||||
}
|
||||
const posted: unknown[] = []
|
||||
const session = new CallTreeSession(message => posted.push(message), () => source)
|
||||
session.open(7, 'fn')
|
||||
version = 8
|
||||
session.cancel()
|
||||
release!()
|
||||
await viFlush()
|
||||
expect(posted).toEqual([])
|
||||
session.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
async function viFlush(): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 文档发出去之后,在后台数每个函数、类落在几棵调用树里。
|
||||
* 页面点开某一块时,把这一版已经收好的边收成树发回去。版本对不上就丢掉。
|
||||
* 文档发出去之后,在后台数每个函数、类落在几棵调用树里,有调用者的收起来,数完把棵数发给页面。
|
||||
* 页面点开某一块时,收着就把树发回去;没收着就当场问这一块,问到了补发棵数再发树,没问到发一棵空的。
|
||||
* 问的时候文档版本变了、或者停掉了,结果都丢掉。
|
||||
*/
|
||||
|
||||
import type { ExtensionToWebview } from '../editor/messages'
|
||||
@@ -32,8 +33,8 @@ interface Cached {
|
||||
export class CallTreeSession {
|
||||
private generation = 0
|
||||
private readonly cache = new Map<string, Cached>()
|
||||
private pending: { version: number, anchor: string } | undefined
|
||||
private scannedGeneration = -1
|
||||
/** 这一轮数到的棵数,只记大于 0 的。后台数完、点开时当场问到,发的都是这一份 */
|
||||
private counts: Record<string, number> = {}
|
||||
|
||||
constructor(
|
||||
private readonly post: (message: ExtensionToWebview) => void,
|
||||
@@ -44,6 +45,7 @@ export class CallTreeSession {
|
||||
cancel(): void {
|
||||
this.generation++
|
||||
this.cache.clear()
|
||||
this.counts = {}
|
||||
}
|
||||
|
||||
/** 按眼下这份文档重新数。先停掉上一轮。 */
|
||||
@@ -52,7 +54,7 @@ export class CallTreeSession {
|
||||
void this.scan(this.generation)
|
||||
}
|
||||
|
||||
/** 页面要看某一块。不是这一版文档的不理;还没数完就等这一轮数完再发。 */
|
||||
/** 页面要看某一块。不是这一版文档的不理;收着就直接发;没收着就当场问这一块。 */
|
||||
open(version: number, anchor: string): void {
|
||||
const src = this.source()
|
||||
if (!src || src.version !== version)
|
||||
@@ -62,9 +64,7 @@ export class CallTreeSession {
|
||||
this.postTree(version, anchor, cached)
|
||||
return
|
||||
}
|
||||
this.pending = { version, anchor }
|
||||
if (this.scannedGeneration === this.generation)
|
||||
this.postEmpty(version, anchor)
|
||||
void this.askNow(this.generation, src, anchor)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -76,31 +76,55 @@ export class CallTreeSession {
|
||||
if (!src || generation !== this.generation)
|
||||
return
|
||||
const version = src.version
|
||||
const counts: Record<string, number> = {}
|
||||
for (const block of callBlocks(src.blocks)) {
|
||||
if (generation !== this.generation || this.source()?.version !== version)
|
||||
if (this.stale(generation, version))
|
||||
return
|
||||
if (!this.cache.has(block.anchor))
|
||||
await this.collect(generation, src, block)
|
||||
}
|
||||
if (this.stale(generation, version))
|
||||
return
|
||||
this.postCounts(version)
|
||||
}
|
||||
|
||||
/** 当场问页面点开的那一块:问到了先补发棵数再发树,没问到(找不到这块、没人调用)发一棵空的。 */
|
||||
private async askNow(generation: number, src: CallTreeSource, anchor: string): Promise<void> {
|
||||
const version = src.version
|
||||
const block = callBlocks(src.blocks).find(item => item.anchor === anchor)
|
||||
const cached = block && await this.collect(generation, src, block)
|
||||
if (this.stale(generation, version))
|
||||
return
|
||||
if (!cached) {
|
||||
this.postEmpty(version, anchor)
|
||||
return
|
||||
}
|
||||
this.postCounts(version)
|
||||
this.postTree(version, anchor, cached)
|
||||
}
|
||||
|
||||
/** 问一块有谁调用。有调用者就收起来、记上棵数再交回来;没人调用、语言不提供、问到一半作废了,都交回空。 */
|
||||
private async collect(generation: number, src: CallTreeSource, block: CallTreeBlock): Promise<Cached | undefined> {
|
||||
const version = src.version
|
||||
const loaded = await src.load(block.defines!.startIndex, () => generation !== this.generation)
|
||||
if (generation !== this.generation || this.source()?.version !== version)
|
||||
return
|
||||
if (!loaded)
|
||||
continue
|
||||
if (!loaded || this.stale(generation, version))
|
||||
return undefined
|
||||
const classTarget = block.kind === 'class'
|
||||
const trees = buildCallTrees(loaded.edges, loaded.symbol, { classTarget })
|
||||
if (trees.length === 0)
|
||||
continue
|
||||
const count = buildCallTrees(loaded.edges, loaded.symbol, { classTarget }).length
|
||||
if (count === 0)
|
||||
return undefined
|
||||
const cached: Cached = { symbol: loaded.symbol, edges: loaded.edges, classTarget }
|
||||
this.cache.set(block.anchor, cached)
|
||||
counts[block.anchor] = trees.length
|
||||
if (this.pending?.anchor === block.anchor && this.pending.version === version)
|
||||
this.postTree(version, block.anchor, cached)
|
||||
this.counts[block.anchor] = count
|
||||
return cached
|
||||
}
|
||||
if (generation !== this.generation || this.source()?.version !== version)
|
||||
return
|
||||
this.scannedGeneration = generation
|
||||
this.post({ type: 'callTreeCounts', version, counts })
|
||||
if (this.pending && this.pending.version === version && !this.cache.has(this.pending.anchor))
|
||||
this.postEmpty(version, this.pending.anchor)
|
||||
|
||||
/** 这一轮停掉了,或者文档版本变了:手上的结果不能再用。 */
|
||||
private stale(generation: number, version: number): boolean {
|
||||
return generation !== this.generation || this.source()?.version !== version
|
||||
}
|
||||
|
||||
private postCounts(version: number): void {
|
||||
this.post({ type: 'callTreeCounts', version, counts: { ...this.counts } })
|
||||
}
|
||||
|
||||
private postTree(version: number, anchor: string, cached: Cached): void {
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('块右上角的图标按钮', () => {
|
||||
expect(buttons[2]).toContain('data-tooltip="编辑"')
|
||||
})
|
||||
|
||||
it('调用树棵数大于 0 时排在编辑后面,按钮上是这个数字;棵数是 0 不出现', () => {
|
||||
it('调用树排在编辑后面;棵数大于 0 时按钮上带这个数字,是 0 时只有图标', () => {
|
||||
const html = render(false, { asking: false }, { rewriting: false }, { count: 3 })
|
||||
const buttons = [...html.matchAll(/<button[^>]*>/g)].map(match => match[0])
|
||||
expect(buttons).toHaveLength(4)
|
||||
@@ -74,8 +74,11 @@ describe('块右上角的图标按钮', () => {
|
||||
expect(buttons[3]).toContain('data-tooltip="调用树" aria-label="调用树"')
|
||||
expect(buttons[3]).toContain('data-call-tree="fn"')
|
||||
expect(visibleText(html)).toBe('3')
|
||||
const hidden = render(false, { asking: false }, { rewriting: false }, { count: 0 })
|
||||
expect(hidden).not.toContain('调用树')
|
||||
expect(hidden.match(/<button/g)).toHaveLength(3)
|
||||
const zero = render(false, { asking: false }, { rewriting: false }, { count: 0 })
|
||||
const zeroButtons = [...zero.matchAll(/<button[^>]*>/g)].map(match => match[0])
|
||||
expect(zeroButtons).toHaveLength(4)
|
||||
expect(zeroButtons[3]).toContain('data-tooltip="调用树" aria-label="调用树"')
|
||||
expect(visibleText(zero)).toBe('')
|
||||
expect(zero).not.toContain('cr-call-count')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { Eye, GitBranch, MessageSquareText, Pencil, Sparkles } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** 「重写注释」按钮的样子和动作:fresh 是块还没有注释、点了新写一段;正在重写时不能再点。 */
|
||||
export interface RewriteTool {
|
||||
@@ -23,7 +24,7 @@ export function BlockTools({ editing, onToggleEdit, asking, onToggleAsk, rewrite
|
||||
onToggleAsk?: () => void
|
||||
/** 没给就不显示「按注释规范重写」(不能写注释的块) */
|
||||
rewrite?: RewriteTool
|
||||
/** 没给,或棵数是 0,就不显示调用树(还没查到、没有调用者、语言不提供) */
|
||||
/** 没给就不显示调用树。count 是后台数出的棵数,大于 0 时按钮上带这个数字,是 0 时只有图标(还没数到、没有调用者,点开时会当场再问) */
|
||||
callTree?: { anchor: string, count: number, open: boolean, onOpen: () => void }
|
||||
}): ReactElement {
|
||||
const label = editing ? '预览' : '编辑'
|
||||
@@ -67,10 +68,10 @@ export function BlockTools({ editing, onToggleEdit, asking, onToggleAsk, rewrite
|
||||
>
|
||||
<Icon size={14} strokeWidth={1.8} aria-hidden />
|
||||
</button>
|
||||
{callTree && callTree.count > 0 && (
|
||||
{callTree && (
|
||||
<button
|
||||
type="button"
|
||||
className="cr-tool flex h-5 items-center justify-center gap-px rounded px-1"
|
||||
className={cn('cr-tool flex h-5 items-center justify-center rounded', callTree.count > 0 ? 'gap-px px-1' : 'w-6')}
|
||||
data-call-tree={callTree.anchor}
|
||||
data-active={callTree.open ? '' : undefined}
|
||||
data-tooltip="调用树"
|
||||
@@ -79,7 +80,7 @@ export function BlockTools({ editing, onToggleEdit, asking, onToggleAsk, rewrite
|
||||
onClick={callTree.onOpen}
|
||||
>
|
||||
<GitBranch size={14} strokeWidth={1.8} aria-hidden />
|
||||
<span className="cr-call-count">{callTree.count}</span>
|
||||
{callTree.count > 0 && <span className="cr-call-count">{callTree.count}</span>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -104,10 +104,10 @@ const MANY: CallTree[] = [
|
||||
]
|
||||
|
||||
describe('调用树弹框', () => {
|
||||
it('棵数为 0 的函数没有按钮,类有,语句没有;只读的树一排按钮都没有', async () => {
|
||||
it('函数和类都有按钮,棵数为 0 只有图标、大于 0 带数字,语句没有;只读的树一排按钮都没有', async () => {
|
||||
const counts = new Map([['function', 0], ['class', 2], ['statement', 4]])
|
||||
await render(<BlockLevel blocks={[block('function'), block('class'), block('statement')]} tree={tree({ counts, onOpen: () => {} })} />)
|
||||
expect(container.querySelector('[data-call-tree="function"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="function"]')!.textContent).toBe('')
|
||||
expect(container.querySelector('[data-call-tree="statement"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="class"]')).not.toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="class"]')!.textContent).toBe('2')
|
||||
|
||||
Reference in new issue
Block a user