⚡ perf(vscode): 预览发文档后在后台问语言扩展的请求排队发:同时在途最多 4 个、离视口近的先问、文档改了排着的作废,看得见的中文名先交
This commit is contained in:
1 parent
5a0c5490ea
commit
12ad047b4d
7 files changed
+335
-45
No files matched your search
@@ -46,7 +46,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
||||
},
|
||||
}
|
||||
const posted: { type: string, counts?: Record<string, number> }[] = []
|
||||
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<void> {
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
@@ -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<void> {
|
||||
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<Pick<CallTreeBlock, 'defines'>>
|
||||
|
||||
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])
|
||||
|
||||
@@ -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<T>(version: number, offset: number, request: () => Promise<T>, dropped: T): Promise<T> {
|
||||
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()
|
||||
|
||||
@@ -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<number, { resolve: (value: string) => void, reject: (err: Error) => void }>()
|
||||
const request = (line: number) => (): Promise<string> => {
|
||||
started.push(line)
|
||||
return new Promise((resolve, reject) => answers.set(line, { resolve, reject }))
|
||||
}
|
||||
const settle = async (): Promise<void> => {
|
||||
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<void> => {
|
||||
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])
|
||||
})
|
||||
})
|
||||
@@ -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<T>(version: number, line: number, request: () => Promise<T>, dropped: T): Promise<T> {
|
||||
return new Promise<T>((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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,9 +71,11 @@ function fakeSources(source: string, options: {
|
||||
definitions?: Record<number, DefinitionStart[]>
|
||||
indexed?: Record<string, IndexedName[]>
|
||||
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)[] = []
|
||||
|
||||
@@ -76,12 +76,15 @@ export interface NameSources {
|
||||
definitions: (offset: number) => Promise<DefinitionStart[]>
|
||||
/** 工作区里定义的、原名叫 name 的中文名 */
|
||||
indexed: (name: string) => Promise<IndexedName[]>
|
||||
/** 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<unknown> => 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<boolean>): 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)
|
||||
|
||||
Reference in new issue
Block a user