⚡ perf(vscode): 预览发文档后在后台问语言扩展的请求排队发:同时在途最多 4 个、离视口近的先问、文档改了排着的作废,看得见的中文名先交
This commit is contained in:
1 parent
5a0c5490ea
commit
12ad047b4d
7 files changed
+329
-39
No files matched your search
@@ -46,7 +46,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: { type: string, counts?: Record<string, number> }[] = []
|
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()
|
session.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
expect(posted).toEqual([{ type: 'callTreeCounts', version: 3, counts: { fn: 1, inner: 1 } }])
|
expect(posted).toEqual([{ type: 'callTreeCounts', version: 3, counts: { fn: 1, inner: 1 } }])
|
||||||
@@ -71,7 +71,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
version = 2
|
version = 2
|
||||||
session.documentSent()
|
session.documentSent()
|
||||||
@@ -91,7 +91,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
load: async () => ({ symbol: mask, edges: [edge] }),
|
load: async () => ({ symbol: mask, edges: [edge] }),
|
||||||
}
|
}
|
||||||
const posted: { type: string, version?: number, anchor?: string, trees?: { name: string }[] }[] = []
|
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')
|
session.open(3, 'fn')
|
||||||
expect(posted).toEqual([])
|
expect(posted).toEqual([])
|
||||||
session.documentSent()
|
session.documentSent()
|
||||||
@@ -124,7 +124,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
expect(posted).toEqual([{ type: 'callTreeCounts', version: 5, counts: {} }])
|
expect(posted).toEqual([{ type: 'callTreeCounts', version: 5, counts: {} }])
|
||||||
@@ -158,7 +158,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
load: async () => ({ symbol: symbol('nobody'), edges: [] }),
|
load: async () => ({ symbol: symbol('nobody'), edges: [] }),
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
posted.length = 0
|
posted.length = 0
|
||||||
@@ -187,7 +187,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
session.open(7, 'fn')
|
session.open(7, 'fn')
|
||||||
version = 8
|
version = 8
|
||||||
@@ -210,7 +210,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
expect(asked).toEqual([endpoint])
|
expect(asked).toEqual([endpoint])
|
||||||
@@ -231,7 +231,7 @@ describe('调用树棵数在文档发出去之后算', () => {
|
|||||||
load: async () => ({ symbol: route, edges: callers, contractMiss: { files: ['web/src/lib/api/schema.d.ts'] } }),
|
load: async () => ({ symbol: route, edges: callers, contractMiss: { files: ['web/src/lib/api/schema.d.ts'] } }),
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
expect(posted).toEqual([{ type: 'callTreeCounts', version: 10, counts: {} }])
|
expect(posted).toEqual([{ type: 'callTreeCounts', version: 10, counts: {} }])
|
||||||
@@ -277,7 +277,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
expect(asked).toEqual(['fn', 'alone', 'unknown'])
|
expect(asked).toEqual(['fn', 'alone', 'unknown'])
|
||||||
@@ -314,7 +314,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
expect(posted).toEqual([])
|
expect(posted).toEqual([])
|
||||||
@@ -346,7 +346,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
posted.length = 0
|
posted.length = 0
|
||||||
@@ -372,7 +372,7 @@ describe('同一版文档又发一遍(页面重新加载、按新规则重发
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const posted: unknown[] = []
|
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.documentSent()
|
||||||
await viFlush()
|
await viFlush()
|
||||||
source.blocks = [route('/api/v1/agents'), block('fn', 'function', 1)]
|
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> {
|
async function viFlush(): Promise<void> {
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export class CallTreeSession {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly post: (message: ExtensionToWebview) => void,
|
private readonly post: (message: ExtensionToWebview) => void,
|
||||||
private readonly source: () => CallTreeSource | undefined,
|
private readonly source: () => CallTreeSource | undefined,
|
||||||
|
/** 名字起点在 offset 的块离页面视口多远:后台先数近的 */
|
||||||
|
private readonly distance: (offset: number) => number,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** 文档发出去了:还是手上这一版就接着用,换了一版就从头数。 */
|
/** 文档发出去了:还是手上这一版就接着用,换了一版就从头数。 */
|
||||||
@@ -104,7 +106,10 @@ export class CallTreeSession {
|
|||||||
void this.scan(version)
|
void this.scan(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一块一块问还没问过的,每问完一块按眼下发出去的块树再找下一块(数的时候文档可能又按同一版重发了);问了才在数完时发棵数。 */
|
/**
|
||||||
|
* 一块一块问还没问过的,先问离视口近的;每问完一块按眼下发出去的块树、眼下的视口再挑下一块(数的时候文档可能又按同一版重发了、页面可能滚了)。
|
||||||
|
* 问了才在数完时发棵数。
|
||||||
|
*/
|
||||||
private async scan(version: number): Promise<void> {
|
private async scan(version: number): Promise<void> {
|
||||||
this.scanning = version
|
this.scanning = version
|
||||||
let asked = false
|
let asked = false
|
||||||
@@ -113,7 +118,11 @@ export class CallTreeSession {
|
|||||||
const src = this.source()
|
const src = this.source()
|
||||||
if (!src || this.stale(version))
|
if (!src || this.stale(version))
|
||||||
return
|
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)
|
if (!next)
|
||||||
break
|
break
|
||||||
asked = true
|
asked = true
|
||||||
@@ -199,11 +208,11 @@ export class CallTreeSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 宿主标了能看调用树(callTree)、有名字起点的块,含嵌在别的块里面的。 */
|
/** 宿主标了能看调用树(callTree)、有名字起点的块,含嵌在别的块里面的。 */
|
||||||
function callBlocks(blocks: readonly CallTreeBlock[]): CallTreeBlock[] {
|
function callBlocks(blocks: readonly CallTreeBlock[]): NamedCallBlock[] {
|
||||||
const found: CallTreeBlock[] = []
|
const found: NamedCallBlock[] = []
|
||||||
const walk = (list: readonly CallTreeBlock[]) => {
|
const walk = (list: readonly CallTreeBlock[]) => {
|
||||||
for (const block of list) {
|
for (const block of list) {
|
||||||
if (block.callTree && block.defines)
|
if (isNamedCallBlock(block))
|
||||||
found.push(block)
|
found.push(block)
|
||||||
if (block.children?.length)
|
if (block.children?.length)
|
||||||
walk(block.children)
|
walk(block.children)
|
||||||
@@ -213,6 +222,13 @@ function callBlocks(blocks: readonly CallTreeBlock[]): CallTreeBlock[] {
|
|||||||
return found
|
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 {
|
function targetOf(block: CallTreeBlock): string {
|
||||||
return JSON.stringify([block.defines?.startIndex, block.callTree?.endpoint])
|
return JSON.stringify([block.defines?.startIndex, block.callTree?.endpoint])
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { DefinitionKinds } from '../definitionKinds'
|
|||||||
import { PreviewDiff } from '../diff/previewDiff'
|
import { PreviewDiff } from '../diff/previewDiff'
|
||||||
import { blockReplacement } from '../editText'
|
import { blockReplacement } from '../editText'
|
||||||
import { locate } from '../folderStore'
|
import { locate } from '../folderStore'
|
||||||
|
import { LanguageRequests } from '../languageRequests'
|
||||||
import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols'
|
import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols'
|
||||||
import { DocumentNames, resolveNames } from '../nameResolution'
|
import { DocumentNames, resolveNames } from '../nameResolution'
|
||||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||||
@@ -208,7 +209,9 @@ class Preview {
|
|||||||
visibleLine: number | undefined
|
visibleLine: number | undefined
|
||||||
/** 最近一次发出去的文档(版本、块树、源码):问 AI 时按块的记号在里面找那块代码,对比基准变了时拿它重新比 */
|
/** 最近一次发出去的文档(版本、块树、源码):问 AI 时按块的记号在里面找那块代码,对比基准变了时拿它重新比 */
|
||||||
private sent: SentDocument | undefined
|
private sent: SentDocument | undefined
|
||||||
/** 能看调用树的块(函数、类、接口卡片)落在几棵调用树里。文档发出去之后再数,不挡住第一次预览;同一版文档数过的页面重新加载后接着用 */
|
/** 发文档后在后台问语言扩展的请求(中文名、库规则查的定义、调用的种类)排队发:在途的有上限,离视口近的先发,文档改了排着的作废 */
|
||||||
|
private readonly requests = new LanguageRequests(() => this.document.version, () => this.visibleLine)
|
||||||
|
/** 能看调用树的块(函数、类、接口卡片)落在几棵调用树里。文档发出去之后再数,不挡住第一次预览,离视口近的先数;同一版文档数过的页面重新加载后接着用 */
|
||||||
private readonly callTrees = new CallTreeSession(
|
private readonly callTrees = new CallTreeSession(
|
||||||
message => this.post(message),
|
message => this.post(message),
|
||||||
() => {
|
() => {
|
||||||
@@ -220,6 +223,7 @@ class Preview {
|
|||||||
load: (block, cancelled) => loadBlockCalls(this.document, block, this.owner.indexed, cancelled),
|
load: (block, cancelled) => loadBlockCalls(this.document, block, this.owner.indexed, cancelled),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
offset => this.requests.distance(this.document.positionAt(offset).line),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
|
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
|
||||||
@@ -430,9 +434,9 @@ class Preview {
|
|||||||
void this.diff.update(this.sent)
|
void this.diff.update(this.sent)
|
||||||
// 文档在解析期间又改了的,等下一次发文档再查
|
// 文档在解析期间又改了的,等下一次发文档再查
|
||||||
if (version === this.document.version) {
|
if (version === this.document.version) {
|
||||||
this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(), report))
|
this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(version), report))
|
||||||
this.lookups.resolve(version, session.requests(), this.lookupSources())
|
this.lookups.resolve(version, session.requests(), this.lookupSources(version))
|
||||||
this.callees.resolve(version, callees.requests(), offset => this.calleeKind(offset))
|
this.callees.resolve(version, callees.requests(), offset => this.queued(version, offset, () => this.calleeKind(offset), undefined))
|
||||||
this.callTrees.documentSent()
|
this.callTrees.documentSent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,25 +505,31 @@ class Preview {
|
|||||||
.map(({ uri, range }) => ({ uri: uri.toString(), line: range.start.line, character: range.start.character }))
|
.map(({ uri, range }) => ({ uri: uri.toString(), line: range.start.line, character: range.start.character }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 找中文名落在哪些名字上时问的:这份文档的语言扩展、工作区里的中文名索引。 */
|
/** 找 version 这一版的中文名落在哪些名字上时问的:这份文档的语言扩展(排队问)、工作区里的中文名索引、页面的视口。 */
|
||||||
private nameSources(): NameSources {
|
private nameSources(version: number): NameSources {
|
||||||
const { document } = this
|
const { document } = this
|
||||||
return {
|
return {
|
||||||
uri: document.uri.toString(),
|
uri: document.uri.toString(),
|
||||||
occurrences: async offset => (await this.occurrences(offset)).map(({ start, end }) => ({ start, end })),
|
occurrences: offset => this.queued(version, offset, async () => (await this.occurrences(offset)).map(({ start, end }) => ({ start, end })), []),
|
||||||
definitions: offset => this.definitionStarts(offset),
|
definitions: offset => this.queued(version, offset, () => this.definitionStarts(offset), []),
|
||||||
indexed: name => this.owner.indexedNames(document.languageId, name),
|
indexed: name => this.owner.indexedNames(document.languageId, name),
|
||||||
|
visible: offset => this.requests.visible(document.positionAt(offset).line),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查库规则问的名字时问的:规则的工作区索引、这份文档的语言扩展。 */
|
/** 查 version 这一版库规则问的名字时问的:规则的工作区索引、这份文档的语言扩展(排队问)。 */
|
||||||
private lookupSources(): LookupSources {
|
private lookupSources(version: number): LookupSources {
|
||||||
return {
|
return {
|
||||||
find: (index, key) => this.owner.indexed(index, key),
|
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 就算了(还没有高亮)。 */
|
/** 跳到高亮着的名字的下一处 / 上一处出现;页面还没 ready 就算了(还没有高亮)。 */
|
||||||
moveOccurrence(step: 1 | -1): void {
|
moveOccurrence(step: 1 | -1): void {
|
||||||
if (this.ready)
|
if (this.ready)
|
||||||
@@ -552,6 +562,7 @@ class Preview {
|
|||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
this.callTrees.dispose()
|
this.callTrees.dispose()
|
||||||
|
this.requests.dispose()
|
||||||
clearTimeout(this.timer)
|
clearTimeout(this.timer)
|
||||||
this.names.dispose()
|
this.names.dispose()
|
||||||
this.lookups.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[]>
|
definitions?: Record<number, DefinitionStart[]>
|
||||||
indexed?: Record<string, IndexedName[]>
|
indexed?: Record<string, IndexedName[]>
|
||||||
asked?: string[]
|
asked?: string[]
|
||||||
|
visible?: (offset: number) => boolean
|
||||||
} = {}): NameSources {
|
} = {}): NameSources {
|
||||||
return {
|
return {
|
||||||
uri: 'file:///w/routes.py',
|
uri: 'file:///w/routes.py',
|
||||||
|
visible: options.visible ?? (() => true),
|
||||||
occurrences: async offset => options.highlights?.[offset] ?? [],
|
occurrences: async offset => options.highlights?.[offset] ?? [],
|
||||||
definitions: async (offset) => {
|
definitions: async (offset) => {
|
||||||
options.asked?.push(`definition ${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())
|
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 () => {
|
it('语言扩展还没起来:定义处自己都查不到出现,或要问的定义一个都没答,算没答完(过一会儿再查)', async () => {
|
||||||
const { blocks, displayNames } = await read(ROUTES)
|
const { blocks, displayNames } = await read(ROUTES)
|
||||||
expect(await resolveNames(displayNames, blocks, fakeSources(ROUTES), () => {})).toBe(false)
|
expect(await resolveNames(displayNames, blocks, fakeSources(ROUTES), () => {})).toBe(false)
|
||||||
@@ -212,6 +238,26 @@ describe('中文名跟着文档版本走', () => {
|
|||||||
expect(names.known(3)).toEqual([name(10), name(20)])
|
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 () => {
|
it('没答完隔一会儿再查,按给的间隔、最多那么多次;答完了就不再查', async () => {
|
||||||
const scheduled: number[] = []
|
const scheduled: number[] = []
|
||||||
const pending: (() => void)[] = []
|
const pending: (() => void)[] = []
|
||||||
|
|||||||
@@ -76,12 +76,15 @@ export interface NameSources {
|
|||||||
definitions: (offset: number) => Promise<DefinitionStart[]>
|
definitions: (offset: number) => Promise<DefinitionStart[]>
|
||||||
/** 工作区里定义的、原名叫 name 的中文名 */
|
/** 工作区里定义的、原名叫 name 的中文名 */
|
||||||
indexed: (name: string) => Promise<IndexedName[]>
|
indexed: (name: string) => Promise<IndexedName[]>
|
||||||
|
/** offset 处页面眼下看不看得见:别的文件定义的,看得见的先问、问完先交一次 */
|
||||||
|
visible: (offset: number) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final):
|
* 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final):
|
||||||
* 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition;
|
* 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition;
|
||||||
* 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算。
|
* 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算;
|
||||||
|
* 页面看得见的先问,问完先交一次,再问其余的。
|
||||||
* 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。
|
* 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。
|
||||||
*/
|
*/
|
||||||
export async function resolveNames(
|
export async function resolveNames(
|
||||||
@@ -109,14 +112,14 @@ export async function resolveNames(
|
|||||||
if (!shown.has(offset))
|
if (!shown.has(offset))
|
||||||
byName.set(original, [...(byName.get(original) ?? []), offset])
|
byName.set(original, [...(byName.get(original) ?? []), offset])
|
||||||
}
|
}
|
||||||
let asked = 0
|
const asks: { offset: number, original: string, elsewhere: IndexedName[] }[] = []
|
||||||
let answered = 0
|
|
||||||
await Promise.all([...byName].map(async ([original, offsets]) => {
|
await Promise.all([...byName].map(async ([original, offsets]) => {
|
||||||
const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri)
|
const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri)
|
||||||
if (elsewhere.length === 0)
|
for (const offset of elsewhere.length > 0 ? offsets : [])
|
||||||
return
|
asks.push({ offset, original, elsewhere })
|
||||||
await Promise.all(offsets.map(async (offset) => {
|
}))
|
||||||
asked++
|
let answered = 0
|
||||||
|
const ask = (list: typeof asks): Promise<unknown> => Promise.all(list.map(async ({ offset, original, elsewhere }) => {
|
||||||
const found = await sources.definitions(offset)
|
const found = await sources.definitions(offset)
|
||||||
if (found.length > 0)
|
if (found.length > 0)
|
||||||
answered++
|
answered++
|
||||||
@@ -124,8 +127,19 @@ export async function resolveNames(
|
|||||||
if (entry)
|
if (entry)
|
||||||
shown.set(offset, { offset, length: original.length, name: entry.text })
|
shown.set(offset, { offset, length: original.length, name: entry.text })
|
||||||
}))
|
}))
|
||||||
}))
|
const near: typeof asks = []
|
||||||
if (asked > 0 && answered === 0)
|
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
|
settled = false
|
||||||
report([...shown.values()], true)
|
report([...shown.values()], true)
|
||||||
return settled
|
return settled
|
||||||
@@ -184,6 +198,13 @@ export function shiftNames(names: readonly ShownName[], changes: readonly TextCh
|
|||||||
return current
|
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]
|
export const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000]
|
||||||
|
|
||||||
@@ -219,7 +240,8 @@ export class DocumentNames {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 发出去了 version 这份文档:查它的中文名;同一个版本只查一次(页面重新加载时照旧用查到的)。
|
* 发出去了 version 这份文档:查它的中文名;同一个版本只查一次(页面重新加载时照旧用查到的)。
|
||||||
* 手上还没有这个版本的中文名时查到一步交一步(先交本文件的),有(上一版挪过来的)就等查完一起换,免得少了的那些闪一下。
|
* 查到一步交一步(先交本文件的,再交看得见的);比眼下显示着的(上一版挪过来的、上一步交过的)少了哪个、或者没多出什么的那一步不交,
|
||||||
|
* 等查完一起换,免得少了的那些闪一下。
|
||||||
* 查的过程中文档又改了的,查到的丢掉。
|
* 查的过程中文档又改了的,查到的丢掉。
|
||||||
*/
|
*/
|
||||||
resolve(version: number, run: (report: (names: ShownName[], final: boolean) => void) => Promise<boolean>): void {
|
resolve(version: number, run: (report: (names: ShownName[], final: boolean) => void) => Promise<boolean>): void {
|
||||||
@@ -232,7 +254,7 @@ export class DocumentNames {
|
|||||||
}
|
}
|
||||||
this.resolving = version
|
this.resolving = version
|
||||||
const report = (names: ShownName[], final: boolean): void => {
|
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
|
return
|
||||||
this.names = names
|
this.names = names
|
||||||
this.deliver(version, names)
|
this.deliver(version, names)
|
||||||
|
|||||||
Reference in new issue
Block a user