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