diff --git a/vscode/src/codeReader/callTree/session.test.ts b/vscode/src/codeReader/callTree/session.test.ts index 4f4c8f1..b0f666c 100644 --- a/vscode/src/codeReader/callTree/session.test.ts +++ b/vscode/src/codeReader/callTree/session.test.ts @@ -46,7 +46,7 @@ describe('调用树棵数在文档发出去之后算', () => { }, } const posted: { type: string, counts?: Record }[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() expect(posted).toEqual([{ type: 'callTreeCounts', version: 3, counts: { fn: 1, inner: 1 } }]) @@ -71,7 +71,7 @@ describe('调用树棵数在文档发出去之后算', () => { }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() version = 2 session.documentSent() @@ -91,7 +91,7 @@ describe('调用树棵数在文档发出去之后算', () => { load: async () => ({ symbol: mask, edges: [edge] }), } const posted: { type: string, version?: number, anchor?: string, trees?: { name: string }[] }[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.open(3, 'fn') expect(posted).toEqual([]) session.documentSent() @@ -124,7 +124,7 @@ describe('调用树棵数在文档发出去之后算', () => { }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() expect(posted).toEqual([{ type: 'callTreeCounts', version: 5, counts: {} }]) @@ -158,7 +158,7 @@ describe('调用树棵数在文档发出去之后算', () => { load: async () => ({ symbol: symbol('nobody'), edges: [] }), } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() posted.length = 0 @@ -187,7 +187,7 @@ describe('调用树棵数在文档发出去之后算', () => { }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() session.open(7, 'fn') version = 8 @@ -210,7 +210,7 @@ describe('调用树棵数在文档发出去之后算', () => { }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() expect(asked).toEqual([endpoint]) @@ -231,7 +231,7 @@ describe('调用树棵数在文档发出去之后算', () => { load: async () => ({ symbol: route, edges: callers, contractMiss: { files: ['web/src/lib/api/schema.d.ts'] } }), } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() expect(posted).toEqual([{ type: 'callTreeCounts', version: 10, counts: {} }]) @@ -277,7 +277,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发 }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() expect(asked).toEqual(['fn', 'alone', 'unknown']) @@ -314,7 +314,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发 }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() expect(posted).toEqual([]) @@ -346,7 +346,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发 }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() posted.length = 0 @@ -372,7 +372,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发 }, } const posted: unknown[] = [] - const session = new CallTreeSession(message => posted.push(message), () => source) + const session = new CallTreeSession(message => posted.push(message), () => source, () => 0) session.documentSent() await viFlush() source.blocks = [route('/api/v1/agents'), block('fn', 'function', 1)] @@ -388,6 +388,24 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发 }) }) +describe('后台数的顺序', () => { + it('离视口近的块先问', async () => { + const asked: number[] = [] + const source: CallTreeSource = { + version: 1, + blocks: [block('a', 'function', 10), block('b', 'function', 500), block('c', 'class', 300)], + load: async ({ defines }) => { + asked.push(defines?.startIndex ?? -1) + return { symbol: symbol('nobody'), edges: [] } + }, + } + const session = new CallTreeSession(() => {}, () => source, offset => Math.abs(offset - 320)) + session.documentSent() + await viFlush() + expect(asked).toEqual([300, 500, 10]) + }) +}) + async function viFlush(): Promise { await new Promise(resolve => setTimeout(resolve, 0)) await new Promise(resolve => setTimeout(resolve, 0)) diff --git a/vscode/src/codeReader/callTree/session.ts b/vscode/src/codeReader/callTree/session.ts index 4284da2..4d7514b 100644 --- a/vscode/src/codeReader/callTree/session.ts +++ b/vscode/src/codeReader/callTree/session.ts @@ -58,6 +58,8 @@ export class CallTreeSession { constructor( private readonly post: (message: ExtensionToWebview) => void, private readonly source: () => CallTreeSource | undefined, + /** 名字起点在 offset 的块离页面视口多远:后台先数近的 */ + private readonly distance: (offset: number) => number, ) {} /** 文档发出去了:还是手上这一版就接着用,换了一版就从头数。 */ @@ -104,7 +106,10 @@ export class CallTreeSession { void this.scan(version) } - /** 一块一块问还没问过的,每问完一块按眼下发出去的块树再找下一块(数的时候文档可能又按同一版重发了);问了才在数完时发棵数。 */ + /** + * 一块一块问还没问过的,先问离视口近的;每问完一块按眼下发出去的块树、眼下的视口再挑下一块(数的时候文档可能又按同一版重发了、页面可能滚了)。 + * 问了才在数完时发棵数。 + */ private async scan(version: number): Promise { this.scanning = version let asked = false @@ -113,7 +118,11 @@ export class CallTreeSession { const src = this.source() if (!src || this.stale(version)) return - const next = callBlocks(src.blocks).find(block => !this.answerOf(block)) + let next: NamedCallBlock | undefined + for (const block of callBlocks(src.blocks)) { + if (!this.answerOf(block) && (!next || this.distance(block.defines.startIndex) < this.distance(next.defines.startIndex))) + next = block + } if (!next) break asked = true @@ -199,11 +208,11 @@ export class CallTreeSession { } /** 宿主标了能看调用树(callTree)、有名字起点的块,含嵌在别的块里面的。 */ -function callBlocks(blocks: readonly CallTreeBlock[]): CallTreeBlock[] { - const found: CallTreeBlock[] = [] +function callBlocks(blocks: readonly CallTreeBlock[]): NamedCallBlock[] { + const found: NamedCallBlock[] = [] const walk = (list: readonly CallTreeBlock[]) => { for (const block of list) { - if (block.callTree && block.defines) + if (isNamedCallBlock(block)) found.push(block) if (block.children?.length) walk(block.children) @@ -213,6 +222,13 @@ function callBlocks(blocks: readonly CallTreeBlock[]): CallTreeBlock[] { return found } +/** 能看调用树、有名字起点的块。 */ +type NamedCallBlock = CallTreeBlock & Required> + +function isNamedCallBlock(block: CallTreeBlock): block is NamedCallBlock { + return !!block.callTree && !!block.defines +} + /** 一块怎么问:名字起点和接口。同一版文档按新规则重发时接口可能变了(查到了挂载前缀),变了就得重问。 */ function targetOf(block: CallTreeBlock): string { return JSON.stringify([block.defines?.startIndex, block.callTree?.endpoint]) diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index 2634b60..e73f5fd 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -30,6 +30,7 @@ import { DefinitionKinds } from '../definitionKinds' import { PreviewDiff } from '../diff/previewDiff' import { blockReplacement } from '../editText' import { locate } from '../folderStore' +import { LanguageRequests } from '../languageRequests' import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols' import { DocumentNames, resolveNames } from '../nameResolution' import { PATH_ASK_ANCHOR } from '../qa/pathQa' @@ -208,7 +209,9 @@ class Preview { visibleLine: number | undefined /** 最近一次发出去的文档(版本、块树、源码):问 AI 时按块的记号在里面找那块代码,对比基准变了时拿它重新比 */ private sent: SentDocument | undefined - /** 能看调用树的块(函数、类、接口卡片)落在几棵调用树里。文档发出去之后再数,不挡住第一次预览;同一版文档数过的页面重新加载后接着用 */ + /** 发文档后在后台问语言扩展的请求(中文名、库规则查的定义、调用的种类)排队发:在途的有上限,离视口近的先发,文档改了排着的作废 */ + private readonly requests = new LanguageRequests(() => this.document.version, () => this.visibleLine) + /** 能看调用树的块(函数、类、接口卡片)落在几棵调用树里。文档发出去之后再数,不挡住第一次预览,离视口近的先数;同一版文档数过的页面重新加载后接着用 */ private readonly callTrees = new CallTreeSession( message => this.post(message), () => { @@ -220,6 +223,7 @@ class Preview { load: (block, cancelled) => loadBlockCalls(this.document, block, this.owner.indexed, cancelled), } }, + offset => this.requests.distance(this.document.positionAt(offset).line), ) /** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */ @@ -430,9 +434,9 @@ class Preview { void this.diff.update(this.sent) // 文档在解析期间又改了的,等下一次发文档再查 if (version === this.document.version) { - this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(), report)) - this.lookups.resolve(version, session.requests(), this.lookupSources()) - this.callees.resolve(version, callees.requests(), offset => this.calleeKind(offset)) + this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(version), report)) + this.lookups.resolve(version, session.requests(), this.lookupSources(version)) + this.callees.resolve(version, callees.requests(), offset => this.queued(version, offset, () => this.calleeKind(offset), undefined)) this.callTrees.documentSent() } } @@ -501,25 +505,31 @@ class Preview { .map(({ uri, range }) => ({ uri: uri.toString(), line: range.start.line, character: range.start.character })) } - /** 找中文名落在哪些名字上时问的:这份文档的语言扩展、工作区里的中文名索引。 */ - private nameSources(): NameSources { + /** 找 version 这一版的中文名落在哪些名字上时问的:这份文档的语言扩展(排队问)、工作区里的中文名索引、页面的视口。 */ + private nameSources(version: number): NameSources { const { document } = this return { uri: document.uri.toString(), - occurrences: async offset => (await this.occurrences(offset)).map(({ start, end }) => ({ start, end })), - definitions: offset => this.definitionStarts(offset), + occurrences: offset => this.queued(version, offset, async () => (await this.occurrences(offset)).map(({ start, end }) => ({ start, end })), []), + definitions: offset => this.queued(version, offset, () => this.definitionStarts(offset), []), indexed: name => this.owner.indexedNames(document.languageId, name), + visible: offset => this.requests.visible(document.positionAt(offset).line), } } - /** 查库规则问的名字时问的:规则的工作区索引、这份文档的语言扩展。 */ - private lookupSources(): LookupSources { + /** 查 version 这一版库规则问的名字时问的:规则的工作区索引、这份文档的语言扩展(排队问)。 */ + private lookupSources(version: number): LookupSources { return { find: (index, key) => this.owner.indexed(index, key), - definitions: offset => this.definitionStarts(offset), + definitions: offset => this.queued(version, offset, () => this.definitionStarts(offset), []), } } + /** 排进后台请求的队(见 LanguageRequests):version 这一版文档 offset 处要问的,没发就作废的交回 dropped(当语言扩展没答)。 */ + private queued(version: number, offset: number, request: () => Promise, dropped: T): Promise { + return this.requests.run(version, this.document.positionAt(offset).line, request, dropped) + } + /** 跳到高亮着的名字的下一处 / 上一处出现;页面还没 ready 就算了(还没有高亮)。 */ moveOccurrence(step: 1 | -1): void { if (this.ready) @@ -552,6 +562,7 @@ class Preview { dispose(): void { this.callTrees.dispose() + this.requests.dispose() clearTimeout(this.timer) this.names.dispose() this.lookups.dispose() diff --git a/vscode/src/codeReader/languageRequests.test.ts b/vscode/src/codeReader/languageRequests.test.ts new file mode 100644 index 0000000..2722a0b --- /dev/null +++ b/vscode/src/codeReader/languageRequests.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest' +import { LanguageRequests } from './languageRequests' + +/** 一个手动答的请求:started 记下发出去的顺序,answer 让它答。 */ +function requests() { + const started: number[] = [] + const answers = new Map void, reject: (err: Error) => void }>() + const request = (line: number) => (): Promise => { + started.push(line) + return new Promise((resolve, reject) => answers.set(line, { resolve, reject })) + } + const settle = async (): Promise => { + for (let i = 0; i < 5; i++) + await Promise.resolve() + } + return { started, answers, request, settle } +} + +describe('后台问语言扩展的请求排队(LanguageRequests)', () => { + it('同时在途的最多 limit 个,答完一个补发一个', async () => { + const { started, answers, request, settle } = requests() + const queue = new LanguageRequests(() => 1, () => 0, 2) + const results = [10, 20, 30, 40].map(line => queue.run(1, line, request(line), 'dropped')) + expect(started).toEqual([10, 20]) + answers.get(10)?.resolve('a') + await settle() + expect(started).toEqual([10, 20, 30]) + answers.get(20)?.reject(new Error('坏了')) + await settle() + expect(started).toEqual([10, 20, 30, 40]) + answers.get(30)?.resolve('c') + answers.get(40)?.resolve('d') + expect(await Promise.allSettled(results)).toEqual([ + { status: 'fulfilled', value: 'a' }, + { status: 'rejected', reason: new Error('坏了') }, + { status: 'fulfilled', value: 'c' }, + { status: 'fulfilled', value: 'd' }, + ]) + }) + + it('离视口近的先发:视口顶部往下的按远近,往上的排在往下一屏之后;视口挪了按挪到的排', async () => { + const { started, answers, request, settle } = requests() + let top = 100 + const queue = new LanguageRequests(() => 1, () => top, 1) + for (const line of [0, 500, 95, 130, 101]) + void queue.run(1, line, request(line), '') + const next = async (): Promise => { + answers.get(started.at(-1)!)?.resolve('') + await settle() + } + await next() + await next() + expect(started).toEqual([0, 101, 130]) + top = 480 + await next() + await next() + expect(started).toEqual([0, 101, 130, 500, 95]) + }) + + it('文档换了版本:排着的旧版本请求不再发,交回 dropped;在途的照常答完', async () => { + const { started, answers, request, settle } = requests() + let version = 1 + const queue = new LanguageRequests(() => version, () => 0, 1) + const first = queue.run(1, 1, request(1), 'dropped') + const stale = queue.run(1, 2, request(2), 'dropped') + version = 2 + const fresh = queue.run(2, 3, request(3), 'dropped') + expect(await stale).toBe('dropped') + answers.get(1)?.resolve('a') + expect(await first).toBe('a') + await settle() + expect(started).toEqual([1, 3]) + answers.get(3)?.resolve('c') + expect(await fresh).toBe('c') + }) + + it('视口内:视口顶部往下一屏', () => { + const queue = new LanguageRequests(() => 1, () => 100) + expect([99, 100, 150, 400].map(line => queue.visible(line))).toEqual([false, true, true, false]) + }) +}) diff --git a/vscode/src/codeReader/languageRequests.ts b/vscode/src/codeReader/languageRequests.ts new file mode 100644 index 0000000..71f0869 --- /dev/null +++ b/vscode/src/codeReader/languageRequests.ts @@ -0,0 +1,96 @@ +/** + * 同时在途的最多几个。语言服务一次只处理一个请求(Pylance、tsserver 都是;VS Code 自带的 TypeScript 扩展也在自己这边排队, + * 一次只往 tsserver 发一个),多发出去的只是在它那儿排着,还把用户自己的悬停、跳到定义挤到后面;留几个在途够填上来回的空当。 + */ +const LIMIT = 4 + +/** 页面只报视口顶部那块的起始行,一屏按这么多行算。 */ +const SCREEN_LINES = 60 + +/** 排着的一个请求。 */ +interface Waiting { + version: number + line: number + start: () => void + drop: () => void +} + +/** + * 文档发出去之后在后台问语言扩展的请求(中文名、库规则查的定义、调用的种类,连同没答时隔一会儿的重试)排队发: + * 同时在途的最多 limit 个;每空出一个位子挑排着的里面离视口最近的(视口顶部往下的按远近,往上的排在往下一屏之后); + * 文档换了版本,排着的旧版本请求不再发,按没答交回去(问的那边见版本变了会丢掉)。用户自己点的(悬停、跳到定义)不排。这里不碰 VS Code。 + */ +export class LanguageRequests { + private waiting: Waiting[] = [] + private running = 0 + + constructor( + /** 文档眼下的版本 */ + private readonly version: () => number, + /** 页面视口顶部那块的起始行;页面还没报时没有,按文件开头算 */ + private readonly visibleLine: () => number | undefined, + private readonly limit = LIMIT, + ) {} + + /** 排一个 version 这一版文档、问第 line 行的请求;没发就作废的交回 dropped。 */ + run(version: number, line: number, request: () => Promise, dropped: T): Promise { + return new Promise((resolve, reject) => { + this.waiting.push({ + version, + line, + start: () => { + request().then(resolve, reject).finally(() => { + this.running-- + this.next() + }) + }, + drop: () => resolve(dropped), + }) + this.next() + }) + } + + /** 第 line 行在不在视口里:视口顶部往下一屏。 */ + visible(line: number): boolean { + const top = this.visibleLine() ?? 0 + return line >= top && line < top + SCREEN_LINES + } + + /** 第 line 行离视口多远:视口顶部往下的按行数,往上的排在往下一屏之后。 */ + distance(line: number): number { + const top = this.visibleLine() ?? 0 + return line >= top ? line - top : top - line + SCREEN_LINES + } + + /** 预览关了:排着的都作废。 */ + dispose(): void { + const waiting = this.waiting + this.waiting = [] + for (const item of waiting) + item.drop() + } + + private next(): void { + const version = this.version() + const stale = this.waiting.filter(item => item.version !== version) + if (stale.length > 0) { + this.waiting = this.waiting.filter(item => item.version === version) + for (const item of stale) + item.drop() + } + while (this.running < this.limit && this.waiting.length > 0) { + let nearest = 0 + let nearestDistance = this.distance(this.waiting[0].line) + for (let i = 1; i < this.waiting.length; i++) { + const distance = this.distance(this.waiting[i].line) + if (distance < nearestDistance) { + nearest = i + nearestDistance = distance + } + } + const [item] = this.waiting.splice(nearest, 1) + this.running++ + item.start() + } + } +} diff --git a/vscode/src/codeReader/nameResolution.test.ts b/vscode/src/codeReader/nameResolution.test.ts index a14f1b5..27b685e 100644 --- a/vscode/src/codeReader/nameResolution.test.ts +++ b/vscode/src/codeReader/nameResolution.test.ts @@ -71,9 +71,11 @@ function fakeSources(source: string, options: { definitions?: Record indexed?: Record asked?: string[] + visible?: (offset: number) => boolean } = {}): NameSources { return { uri: 'file:///w/routes.py', + visible: options.visible ?? (() => true), occurrences: async offset => options.highlights?.[offset] ?? [], definitions: async (offset) => { options.asked?.push(`definition ${offset}`) @@ -156,6 +158,30 @@ describe('中文名落到哪些名字上', () => { expect(asked.filter(line => line.startsWith('definition')).sort()).toEqual([`definition ${admin}`, `definition ${role}`].sort()) }) + it('别的文件定义的:页面看得见的先问,问完先交一次,再问其余的', async () => { + const { blocks } = await read(ROUTES) + const admin = at(ROUTES, 'ADMIN') + const role = at(ROUTES, 'role') + const models = 'file:///w/models.py' + const asked: string[] = [] + const sources = fakeSources(ROUTES, { + asked, + visible: offset => offset === admin, + indexed: { + ADMIN: [{ location: { uri: models, start: { line: 6, character: 4 }, end: { line: 6, character: 9 } }, text: '管理员' }], + role: [{ location: { uri: models, start: { line: 11, character: 4 }, end: { line: 11, character: 8 } }, text: '角色' }], + }, + definitions: { + [admin]: [{ uri: models, line: 6, character: 4 }], + [role]: [{ uri: models, line: 11, character: 4 }], + }, + }) + const reports: [number[], boolean][] = [] + await resolveNames([], blocks, sources, (names, final) => reports.push([names.map(({ offset }) => offset), final])) + expect(asked.filter(line => line.startsWith('definition'))).toEqual([`definition ${admin}`, `definition ${role}`]) + expect(reports).toEqual([[[], false], [[admin], false], [[admin, role], true]]) + }) + it('语言扩展还没起来:定义处自己都查不到出现,或要问的定义一个都没答,算没答完(过一会儿再查)', async () => { const { blocks, displayNames } = await read(ROUTES) expect(await resolveNames(displayNames, blocks, fakeSources(ROUTES), () => {})).toBe(false) @@ -212,6 +238,26 @@ describe('中文名跟着文档版本走', () => { expect(names.known(3)).toEqual([name(10), name(20)]) }) + it('有上一版挪过来的:查到一步的把挪过来的都盖住了(不会有名字闪没)就先交,少了的照旧等查完', async () => { + const delivered: string[] = [] + const names = new DocumentNames((version, list) => delivered.push(`${version}:${list.map(item => item.offset).join(',')}`), []) + names.resolve(1, async (report) => { + report([name(10), name(20)], true) + return true + }) + await Promise.resolve() + names.edited(2, [{ offset: 0, length: 0, text: 'x' }]) + names.resolve(2, async (report) => { + report([name(11)], false) + report([name(11), name(21, '乙')], false) + report([name(11), name(21), name(40)], false) + report([name(11), name(21), name(40), name(50)], true) + return true + }) + await Promise.resolve() + expect(delivered).toEqual(['1:10,20', '2:11,21,40', '2:11,21,40,50']) + }) + it('没答完隔一会儿再查,按给的间隔、最多那么多次;答完了就不再查', async () => { const scheduled: number[] = [] const pending: (() => void)[] = [] diff --git a/vscode/src/codeReader/nameResolution.ts b/vscode/src/codeReader/nameResolution.ts index 952787a..4e86f9c 100644 --- a/vscode/src/codeReader/nameResolution.ts +++ b/vscode/src/codeReader/nameResolution.ts @@ -76,12 +76,15 @@ export interface NameSources { definitions: (offset: number) => Promise /** 工作区里定义的、原名叫 name 的中文名 */ indexed: (name: string) => Promise + /** offset 处页面眼下看不看得见:别的文件定义的,看得见的先问、问完先交一次 */ + visible: (offset: number) => boolean } /** * 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final): * 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition; - * 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算。 + * 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算; + * 页面看得见的先问,问完先交一次,再问其余的。 * 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。 */ export async function resolveNames( @@ -109,23 +112,34 @@ export async function resolveNames( if (!shown.has(offset)) byName.set(original, [...(byName.get(original) ?? []), offset]) } - let asked = 0 - let answered = 0 + const asks: { offset: number, original: string, elsewhere: IndexedName[] }[] = [] await Promise.all([...byName].map(async ([original, offsets]) => { const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri) - if (elsewhere.length === 0) - return - await Promise.all(offsets.map(async (offset) => { - asked++ - const found = await sources.definitions(offset) - if (found.length > 0) - answered++ - const entry = elsewhere.find(({ location }) => found.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character)) - if (entry) - shown.set(offset, { offset, length: original.length, name: entry.text }) - })) + for (const offset of elsewhere.length > 0 ? offsets : []) + asks.push({ offset, original, elsewhere }) })) - if (asked > 0 && answered === 0) + let answered = 0 + const ask = (list: typeof asks): Promise => Promise.all(list.map(async ({ offset, original, elsewhere }) => { + const found = await sources.definitions(offset) + if (found.length > 0) + answered++ + const entry = elsewhere.find(({ location }) => found.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character)) + if (entry) + shown.set(offset, { offset, length: original.length, name: entry.text }) + })) + const near: typeof asks = [] + const far: typeof asks = [] + for (const item of asks) + (sources.visible(item.offset) ? near : far).push(item) + if (near.length > 0 && far.length > 0) { + await ask(near) + report([...shown.values()], false) + await ask(far) + } + else { + await ask(asks) + } + if (asks.length > 0 && answered === 0) settled = false report([...shown.values()], true) return settled @@ -184,6 +198,13 @@ export function shiftNames(names: readonly ShownName[], changes: readonly TextCh return current } +/** next 把 current 的每一个都盖住了(一模一样的一项),还多出几个。 */ +function adds(next: readonly ShownName[], current: readonly ShownName[]): boolean { + const keyOf = ({ offset, length, name, definition }: ShownName): string => JSON.stringify([offset, length, name, definition]) + const keys = new Set(next.map(keyOf)) + return next.length > current.length && current.every(name => keys.has(keyOf(name))) +} + /** 语言扩展还没起来时隔多久再查(毫秒),查到最后一次还没答就算了 */ export const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000] @@ -219,7 +240,8 @@ export class DocumentNames { /** * 发出去了 version 这份文档:查它的中文名;同一个版本只查一次(页面重新加载时照旧用查到的)。 - * 手上还没有这个版本的中文名时查到一步交一步(先交本文件的),有(上一版挪过来的)就等查完一起换,免得少了的那些闪一下。 + * 查到一步交一步(先交本文件的,再交看得见的);比眼下显示着的(上一版挪过来的、上一步交过的)少了哪个、或者没多出什么的那一步不交, + * 等查完一起换,免得少了的那些闪一下。 * 查的过程中文档又改了的,查到的丢掉。 */ resolve(version: number, run: (report: (names: ShownName[], final: boolean) => void) => Promise): void { @@ -232,7 +254,7 @@ export class DocumentNames { } this.resolving = version const report = (names: ShownName[], final: boolean): void => { - if (this.resolving !== version || (!final && this.names.length > 0)) + if (this.resolving !== version || (!final && !adds(names, this.names))) return this.names = names this.deliver(version, names)