diff --git a/vscode/src/codeReader/callTree/session.test.ts b/vscode/src/codeReader/callTree/session.test.ts index 55e6e75..c188c36 100644 --- a/vscode/src/codeReader/callTree/session.test.ts +++ b/vscode/src/codeReader/callTree/session.test.ts @@ -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((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 { diff --git a/vscode/src/codeReader/callTree/session.ts b/vscode/src/codeReader/callTree/session.ts index 6eca451..6dde917 100644 --- a/vscode/src/codeReader/callTree/session.ts +++ b/vscode/src/codeReader/callTree/session.ts @@ -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() - private pending: { version: number, anchor: string } | undefined - private scannedGeneration = -1 + /** 这一轮数到的棵数,只记大于 0 的。后台数完、点开时当场问到,发的都是这一份 */ + private counts: Record = {} 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 = {} for (const block of callBlocks(src.blocks)) { - if (generation !== this.generation || this.source()?.version !== version) + if (this.stale(generation, version)) return - const loaded = await src.load(block.defines!.startIndex, () => generation !== this.generation) - if (generation !== this.generation || this.source()?.version !== version) - 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 (!this.cache.has(block.anchor)) + await this.collect(generation, src, block) } - if (generation !== this.generation || this.source()?.version !== version) + if (this.stale(generation, 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) + this.postCounts(version) + } + + /** 当场问页面点开的那一块:问到了先补发棵数再发树,没问到(找不到这块、没人调用)发一棵空的。 */ + private async askNow(generation: number, src: CallTreeSource, anchor: string): Promise { + 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 { + 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 { diff --git a/vscode/webview-ui/src/codeReader/BlockTools.test.tsx b/vscode/webview-ui/src/codeReader/BlockTools.test.tsx index c1e7af6..0dd7666 100644 --- a/vscode/webview-ui/src/codeReader/BlockTools.test.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTools.test.tsx @@ -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(/]*>/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(/ - {callTree && callTree.count > 0 && ( + {callTree && ( )} diff --git a/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx b/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx index 2fe2da2..cfca9ea 100644 --- a/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx +++ b/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx @@ -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( {} })} />) - 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')