⚡ perf(vscode): 代码阅读预览注释里的引用解析过就记着:标坏链接、悬停、点击同一个引用只问一次 Pylance,同时问的等同一个结果,文档版本或块树、导入、用到的名字换了才作废,指到的别的文件改过再问,找不到和语言自带的不留;悬停看别的文件里的定义时读成的文档各预览共用,同一版不再重新解析,留最近 8 份,注册的插件变了全部重读
This commit is contained in:
1 parent
3983032356
commit
cfde0fedf5
9 files changed
+595
-70
No files matched your search
@@ -2,6 +2,7 @@ import type { NamedKind } from './blockView'
|
||||
import type { BlockKind, LanguageAdapter } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { kindAt, readNamedKinds } from './blockView'
|
||||
import { RecentPromises } from './recentPromises'
|
||||
|
||||
/** 定义所在的那份文件(TextDocument 的子集):uri、版本、语言、内容。 */
|
||||
export interface KindSource {
|
||||
@@ -20,17 +21,19 @@ const LIMIT = 64
|
||||
* 最近用过的留 limit 份。这里不碰 VS Code。
|
||||
*/
|
||||
export class DefinitionKinds {
|
||||
private readonly files = new Map<string, { version: number, kinds: Promise<NamedKind[] | undefined> }>()
|
||||
private readonly files: RecentPromises<NamedKind[] | undefined>
|
||||
|
||||
constructor(
|
||||
private readonly languageFor: (languageId: string) => LanguageAdapter | undefined,
|
||||
private readonly runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
private readonly limit = LIMIT,
|
||||
) {}
|
||||
limit = LIMIT,
|
||||
) {
|
||||
this.files = new RecentPromises(limit)
|
||||
}
|
||||
|
||||
/** uri 这份文件 version 这一版各块的名字和种类(预览发文档时读出的)。 */
|
||||
remember(uri: string, version: number, kinds: NamedKind[]): void {
|
||||
this.keep(uri, version, Promise.resolve(kinds))
|
||||
this.files.set(uri, version, Promise.resolve(kinds))
|
||||
}
|
||||
|
||||
/** file 里 offset 处的名字定义的是哪种块;没有语言插件、不在哪块的名字上时没有。 */
|
||||
@@ -40,32 +43,12 @@ export class DefinitionKinds {
|
||||
}
|
||||
|
||||
private kindsOf(file: KindSource): Promise<NamedKind[] | undefined> {
|
||||
const known = this.files.get(file.uri)
|
||||
if (known?.version === file.version) {
|
||||
this.keep(file.uri, known.version, known.kinds)
|
||||
return known.kinds
|
||||
}
|
||||
const known = this.files.get(file.uri, file.version)
|
||||
if (known)
|
||||
return known
|
||||
const adapter = this.languageFor(file.languageId)
|
||||
if (!adapter)
|
||||
return Promise.resolve(undefined)
|
||||
const kinds = readNamedKinds(this.runtime, adapter, file.languageId, file.getText())
|
||||
this.keep(file.uri, file.version, kinds)
|
||||
// 解析失败的不记,下次重来
|
||||
kinds.catch(() => {
|
||||
if (this.files.get(file.uri)?.kinds === kinds)
|
||||
this.files.delete(file.uri)
|
||||
})
|
||||
return kinds
|
||||
}
|
||||
|
||||
/** 记下(或用到了)一份:挪到最近用过的那头,多了丢最久没用的。 */
|
||||
private keep(uri: string, version: number, kinds: Promise<NamedKind[] | undefined>): void {
|
||||
this.files.delete(uri)
|
||||
this.files.set(uri, { version, kinds })
|
||||
for (const oldest of this.files.keys()) {
|
||||
if (this.files.size <= this.limit)
|
||||
break
|
||||
this.files.delete(oldest)
|
||||
}
|
||||
return this.files.set(file.uri, file.version, readNamedKinds(this.runtime, adapter, file.languageId, file.getText()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { Memento } from 'vscode'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentSnapshot } from './documentMessage'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { CodeReaderRegistry } from '../registry'
|
||||
import { fastapiRule } from '../rules/fastapi'
|
||||
import { runtime, WASM_DIR } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
import { DocumentReads } from './documentReads'
|
||||
|
||||
const SOURCE = 'class UserService:\n def update_user(self):\n pass\n'
|
||||
|
||||
/** 数着解析了几次的解析运行时。 */
|
||||
function counting(): { runtime: Pick<TreeSitterRuntime, 'parse'>, parses: () => number } {
|
||||
let parses = 0
|
||||
return {
|
||||
runtime: {
|
||||
parse: (...args) => {
|
||||
parses++
|
||||
return runtime.parse(...args)
|
||||
},
|
||||
},
|
||||
parses: () => parses,
|
||||
}
|
||||
}
|
||||
|
||||
function registryWithPython(): CodeReaderRegistry {
|
||||
const store: Record<string, unknown> = {}
|
||||
const registry = new CodeReaderRegistry({
|
||||
get: <T>(key: string): T | undefined => store[key] as T | undefined,
|
||||
update: async (key: string, value: unknown) => {
|
||||
store[key] = value
|
||||
},
|
||||
} as Pick<Memento, 'get' | 'update'>)
|
||||
registry.registerLanguage(createPythonAdapter(WASM_DIR))
|
||||
registry.registerTranslator(pythonZhTranslator)
|
||||
return registry
|
||||
}
|
||||
|
||||
function file(version: number, entryNames: string[] = []): DocumentSnapshot {
|
||||
return { languageId: 'python', version, getText: () => SOURCE, fileName: '/app/service.py', entryNames }
|
||||
}
|
||||
|
||||
describe('悬停看别的文件里的定义时读成的文档(DocumentReads)', () => {
|
||||
it('同一份文件同一版再看不再解析,几处同时看也只解析一次;文件改了(版本变了)、入口名字变了再解析', async () => {
|
||||
const { runtime: parser, parses } = counting()
|
||||
const reads = new DocumentReads(registryWithPython(), parser)
|
||||
const [first, second] = [reads.read('file:///app/service.py', file(1)), reads.read('file:///app/service.py', file(1))]
|
||||
expect(second).toBe(first)
|
||||
const { message } = await first
|
||||
expect(message.blocks.map(block => block.anchor)).toContain('class UserService')
|
||||
const once = parses()
|
||||
expect(once).toBeGreaterThan(0)
|
||||
|
||||
expect(await reads.read('file:///app/service.py', file(1))).toBe(await first)
|
||||
expect(parses()).toBe(once)
|
||||
|
||||
await reads.read('file:///app/service.py', file(2))
|
||||
expect(parses()).toBe(once * 2)
|
||||
await reads.read('file:///app/service.py', file(2, ['UserService']))
|
||||
expect(parses()).toBe(once * 3)
|
||||
})
|
||||
|
||||
it('注册的插件、规则变了,或换了翻译插件:重新解析', async () => {
|
||||
const { runtime: parser, parses } = counting()
|
||||
const registry = registryWithPython()
|
||||
const reads = new DocumentReads(registry, parser)
|
||||
await reads.read('file:///app/service.py', file(1))
|
||||
const once = parses()
|
||||
|
||||
registry.registerLibraryRule(fastapiRule)
|
||||
await reads.read('file:///app/service.py', file(1))
|
||||
expect(parses()).toBe(once * 2)
|
||||
await reads.read('file:///app/service.py', file(1))
|
||||
expect(parses()).toBe(once * 2)
|
||||
|
||||
registry.registerTranslator({ ...pythonZhTranslator, id: 'python-other' })
|
||||
await reads.read('file:///app/service.py', file(1))
|
||||
expect(parses()).toBe(once * 3)
|
||||
await registry.selectTranslator('python', 'python-other')
|
||||
await reads.read('file:///app/service.py', file(1))
|
||||
expect(parses()).toBe(once * 4)
|
||||
})
|
||||
|
||||
it('最多记 limit 份文件,最久没看过的先丢', async () => {
|
||||
const { runtime: parser, parses } = counting()
|
||||
const reads = new DocumentReads(registryWithPython(), parser, 2)
|
||||
await reads.read('file:///a.py', file(1))
|
||||
const once = parses()
|
||||
await reads.read('file:///b.py', file(1))
|
||||
await reads.read('file:///a.py', file(1))
|
||||
await reads.read('file:///c.py', file(1))
|
||||
expect(parses()).toBe(once * 3)
|
||||
await reads.read('file:///a.py', file(1))
|
||||
expect(parses()).toBe(once * 3)
|
||||
await reads.read('file:///b.py', file(1))
|
||||
expect(parses()).toBe(once * 4)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentRead, DocumentSnapshot } from './documentMessage'
|
||||
import { RecentPromises } from '../recentPromises'
|
||||
import { documentMessage } from './documentMessage'
|
||||
|
||||
/** 默认记几份文件 */
|
||||
const LIMIT = 8
|
||||
|
||||
/** 悬停看定义用到的那部分:读成的文档,和里面起了中文名的定义。 */
|
||||
export type PeekRead = Pick<DocumentRead, 'message' | 'displayNames'>
|
||||
|
||||
/**
|
||||
* 悬停看别的文件里的定义时,那份文件读成的文档(见 [documentMessage]),各预览共用,免得每次悬停都把整份文件重新解析一遍。
|
||||
*
|
||||
* 同一份文件的同一版、同样的翻译插件和入口名字再看时用记着的,几处同时看也只解析一次;注册的语言插件、翻译插件、规则变了全部重读。
|
||||
* 只留最近看过的 `limit` 份文件,大文件不一直占着内存。这里不碰 VS Code。
|
||||
*/
|
||||
export class DocumentReads {
|
||||
private readonly files: RecentPromises<PeekRead>
|
||||
|
||||
constructor(
|
||||
private readonly registry: Pick<CodeReaderRegistry, 'onDidChange' | 'languageFor' | 'translatorsFor' | 'selectedTranslator' | 'rulesFor' | 'commandRulesFor'>,
|
||||
private readonly runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
limit = LIMIT,
|
||||
) {
|
||||
this.files = new RecentPromises(limit)
|
||||
registry.onDidChange(() => this.files.clear())
|
||||
}
|
||||
|
||||
/** `uri` 这份文件按 `document` 读成的文档。 */
|
||||
read(uri: string, document: DocumentSnapshot): Promise<PeekRead> {
|
||||
const { languageId, version, fileName, entryNames } = document
|
||||
const stamp = JSON.stringify([version, languageId, fileName, this.registry.selectedTranslator(languageId)?.id, entryNames ?? []])
|
||||
return this.files.get(uri, stamp) ?? this.files.set(uri, stamp, this.parse(document))
|
||||
}
|
||||
|
||||
private async parse(document: DocumentSnapshot): Promise<PeekRead> {
|
||||
// 那个文件里规则出的错由它自己的预览记
|
||||
const { message, displayNames } = await documentMessage(document, this.registry, this.runtime, () => {})
|
||||
return { message, displayNames }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import type { SymbolNode } from '../symbolLinks'
|
||||
import type { CodeColors } from '../theme/codeColors'
|
||||
import type { ExtensionToWebview, WebviewToExtension } from './messages'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { commands, window, workspace } from 'vscode'
|
||||
import { commands, Range, Uri, window, workspace } from 'vscode'
|
||||
import { languageDefinitions } from './definitions'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { documentSymbolNodes } from './documentSymbols'
|
||||
import { FIND_CONTEXT } from './findCommands'
|
||||
@@ -22,6 +23,8 @@ vi.mock('vscode', () => ({
|
||||
getConfiguration: () => ({ get: (_key: string, fallback: unknown) => fallback }),
|
||||
openTextDocument: vi.fn(),
|
||||
applyEdit: vi.fn(async () => true),
|
||||
textDocuments: [],
|
||||
asRelativePath: (uri: { fsPath: string }) => uri.fsPath,
|
||||
},
|
||||
/** 记下换了哪儿、换成什么 */
|
||||
WorkspaceEdit: class {
|
||||
@@ -33,7 +36,10 @@ vi.mock('vscode', () => ({
|
||||
get start() { return { line: this.startLine, character: this.startCharacter } }
|
||||
get end() { return { line: this.endLine, character: this.endCharacter } }
|
||||
},
|
||||
Uri: { file: (fsPath: string) => ({ fsPath, scheme: 'file', toString: () => `file://${fsPath}` }) },
|
||||
Uri: {
|
||||
file: (fsPath: string) => ({ fsPath, scheme: 'file', toString: () => `file://${fsPath}` }),
|
||||
parse: (value: string) => ({ fsPath: value.replace('file://', ''), scheme: 'file', toString: () => value }),
|
||||
},
|
||||
EndOfLine: { LF: 1, CRLF: 2 },
|
||||
}))
|
||||
vi.mock('../../logging/logger', () => ({ logger: { add: vi.fn() } }))
|
||||
@@ -55,6 +61,11 @@ vi.mock('./documentMessage', async importOriginal => ({
|
||||
}))
|
||||
/** 文档符号由各个测试给 */
|
||||
vi.mock('./documentSymbols', () => ({ documentSymbolNodes: vi.fn(async () => []) }))
|
||||
/** 语言扩展答的定义由各个测试给,默认没答 */
|
||||
vi.mock('./definitions', async importOriginal => ({
|
||||
...await importOriginal<typeof import('./definitions')>(),
|
||||
languageDefinitions: vi.fn(async () => []),
|
||||
}))
|
||||
/** 仓库在 /workspace,里面只有这几个文件 */
|
||||
vi.mock('./repoPath', async importOriginal => ({
|
||||
...await importOriginal<typeof import('./repoPath')>(),
|
||||
@@ -74,7 +85,7 @@ function preview(rewrite: (uri: unknown, request: RewriteRequest) => Promise<str
|
||||
const qa = { imageRoot: () => undefined, onDidChange: subscribe, onDidAnswer: subscribe, onDidRewrite: subscribe, profiles: { ask: { subscribe }, rewrite: { subscribe } }, rewrite: vi.fn(rewrite) }
|
||||
const provider = new PreviewEditorProvider(...[
|
||||
{},
|
||||
{},
|
||||
{ onDidChange: subscribe },
|
||||
{},
|
||||
{ subscribe },
|
||||
{ subscribe },
|
||||
@@ -211,7 +222,7 @@ function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }, co
|
||||
}
|
||||
const provider = new PreviewEditorProvider(...[
|
||||
{},
|
||||
{ selectedTranslator: () => undefined, languageFor },
|
||||
{ selectedTranslator: () => undefined, languageFor, onDidChange: subscribe },
|
||||
{},
|
||||
{ subscribe, current: { excluded: {}, onlyChanges: false } },
|
||||
colors,
|
||||
@@ -587,6 +598,50 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkBuiltin', symbol: 'Error', block: HELPER })
|
||||
expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'repoLinkBroken' }))
|
||||
})
|
||||
|
||||
it('检查过的引用再悬停、再点:文档符号、定义都只问一次;悬停两次,定义所在的别的文件也只读一次', async () => {
|
||||
const BUILTINS = '/typeshed/builtins.pyi'
|
||||
const builtins = { uri: Uri.file(BUILTINS), version: 1, languageId: 'python', fileName: BUILTINS, getText: () => 'class RuntimeError: ...', positionAt: () => ({ line: 0, character: 6 }), offsetAt: () => 6 }
|
||||
vi.mocked(documentMessage).mockImplementationOnce(async input => ({
|
||||
message: { type: 'document', version: input.version, languageId: input.languageId, blocks: [declared(HELPER, 950)], source: '', colors: { roles: [], runs: [] }, attributes: [], translators: [], translatorId: '', names: [] },
|
||||
displayNames: [],
|
||||
symbols: [],
|
||||
imports: [{ name: 'RuntimeError', offset: 7, range: { startIndex: 0, endIndex: 20 } }],
|
||||
kinds: [],
|
||||
bindings: [],
|
||||
}) as never)
|
||||
vi.mocked(documentSymbolNodes).mockReset().mockResolvedValue([symbol('helper', 950, 990)])
|
||||
vi.mocked(languageDefinitions).mockClear().mockResolvedValue([{ uri: builtins.uri, range: new Range(0, 6, 0, 18) }] as never)
|
||||
const { provider, document } = opening()
|
||||
Object.assign(document, { positionAt: (offset: number) => ({ line: 0, character: offset }) })
|
||||
Object.assign(workspace, { textDocuments: [document, builtins] })
|
||||
vi.mocked(workspace.openTextDocument).mockImplementation((async (uri: { toString: () => string }) => uri.toString() === builtins.uri.toString() ? builtins : document) as never)
|
||||
try {
|
||||
const page = tab()
|
||||
provider.resolveCustomTextEditor(document as never, page.panel)
|
||||
await page.send({ type: 'ready' })
|
||||
await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document' })))
|
||||
vi.mocked(documentMessage).mockClear()
|
||||
|
||||
const reference = { symbol: 'RuntimeError', block: HELPER }
|
||||
await page.send({ type: 'checkRepoLinks', id: 1, paths: [], references: [reference] })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [], builtin: [] })
|
||||
await page.send({ type: 'peekLink', id: 2, ...reference })
|
||||
await page.send({ type: 'peekLink', id: 3, ...reference })
|
||||
await page.send({ type: 'openRepoLink', ...reference })
|
||||
|
||||
expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'repoLinkOpened', ...reference }))
|
||||
expect(commands.executeCommand).toHaveBeenCalledWith('vscode.open', expect.objectContaining({ fsPath: BUILTINS }), expect.anything())
|
||||
expect(documentSymbolNodes).toHaveBeenCalledTimes(1)
|
||||
expect(languageDefinitions).toHaveBeenCalledTimes(1)
|
||||
expect(documentMessage).toHaveBeenCalledTimes(1)
|
||||
expect(documentMessage).toHaveBeenCalledWith(expect.objectContaining({ fileName: BUILTINS }), expect.anything(), expect.anything(), expect.any(Function))
|
||||
}
|
||||
finally {
|
||||
Object.assign(workspace, { textDocuments: [] })
|
||||
vi.mocked(workspace.openTextDocument).mockReset()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('点了带几行的仓库链接', () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { CodeQa, TopRequest } from '../qa/codeQa'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { LookupSources } from '../ruleLookups'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { ImportedName, LinkTarget, ReferenceTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { ImportedName, LinkTarget, ReferenceTarget } from '../symbolLinks'
|
||||
import type { ThemeColors } from '../theme/codeColors'
|
||||
import type { IndexFind } from '../workspaceIndex'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
@@ -37,18 +37,20 @@ import { NOT_REWRITABLE } from '../qa/codeQa'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { outsideUses, referenceScope, resolveReference } from '../symbolLinks'
|
||||
import { outsideUses } from '../symbolLinks'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { definitionSiteNames, namesOnCard, pagePeekBlock, peekBlock } from './definitionPeek'
|
||||
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
import { documentMessage, pageDocument } from './documentMessage'
|
||||
import { DocumentReads } from './documentReads'
|
||||
import { documentSymbolNodes } from './documentSymbols'
|
||||
import { FIND_CONTEXT } from './findCommands'
|
||||
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
|
||||
import { occurrencesOf } from './occurrences'
|
||||
import { languageOf, linesSpot, openingFor, revealOf } from './openTarget'
|
||||
import { PATH_PREVIEW_SCHEME, topSubjectOf } from './pathPreview'
|
||||
import { ReferenceTargets } from './referenceTargets'
|
||||
import { findRepoSymbol, isRepoImagePath, repoRootOf, resolveRepoEntry } from './repoPath'
|
||||
|
||||
export const PREVIEW_VIEW_TYPE = 'superpowers.codeReader.preview'
|
||||
@@ -85,6 +87,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
readonly finding = new NavigationContext<Preview>(value => void commands.executeCommand('setContext', FIND_CONTEXT, value))
|
||||
/** 调用的名字的定义是哪种块:各预览共用,定义所在的文件同一版只解析一次 */
|
||||
readonly definitionKinds: DefinitionKinds
|
||||
/** 悬停看别的文件里的定义时那份文件读成的文档:各预览共用,同一版不再重新解析 */
|
||||
readonly documentReads: DocumentReads
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: Uri,
|
||||
@@ -111,6 +115,7 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
readonly askFeature: (subject: Uri, request: TopRequest) => Promise<string | undefined>,
|
||||
) {
|
||||
this.definitionKinds = new DefinitionKinds(languageId => registry.languageFor(languageId), runtime)
|
||||
this.documentReads = new DocumentReads(registry, runtime)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,6 +246,11 @@ class Preview {
|
||||
offset => this.requests.distance(this.document.positionAt(offset).line),
|
||||
)
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用解析过的指到哪(见 [ReferenceTargets])。文档符号问这种语言的扩展(同仓库链接找符号),导入的、代码里用到的名字问语言扩展它的定义
|
||||
* (同跳到定义,几处取第一处),哪些名字是语言自带的问语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))
|
||||
*/
|
||||
private readonly references: ReferenceTargets
|
||||
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
|
||||
private symbols: { version: number, list: LocalSymbol[] } | undefined
|
||||
/** 最近一次发出去的文档里各条导入引进来的名字:注释里不带路径的引用按它找导入的定义 */
|
||||
@@ -283,6 +293,19 @@ class Preview {
|
||||
return found.length > 0 ? found : this.localDefinitions(offset)
|
||||
},
|
||||
})
|
||||
this.references = new ReferenceTargets({
|
||||
uri: document.uri.toString(),
|
||||
symbols: documentSymbolNodes,
|
||||
definition: async (offset) => {
|
||||
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (!found)
|
||||
return undefined
|
||||
const doc = await workspace.openTextDocument(found.uri)
|
||||
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
|
||||
},
|
||||
builtin: name => registry.languageFor(document.languageId)?.builtins?.has(name) ?? false,
|
||||
versionOf: uri => workspace.textDocuments.find(doc => doc.uri.toString() === uri)?.version,
|
||||
})
|
||||
this.names = new DocumentNames((version, names) => {
|
||||
if (version === document.version)
|
||||
this.post({ type: 'displayNames', version, names })
|
||||
@@ -870,8 +893,8 @@ class Preview {
|
||||
}
|
||||
|
||||
/**
|
||||
* uri 里 offset 处的定义的卡片:盖住它的那块(peekBlock)。同一份文件用最近发出去的那份文档的块和中文名;
|
||||
* 别的文件打开它、按它的语言排视图数据(同它自己的预览:翻译插件、库规则、项目入口;规则查名字的定义、调用的种类不等),
|
||||
* `uri` 里 `offset` 处的定义的卡片:盖住它的那块([peekBlock])。同一份文件用最近发出去的那份文档的块和中文名;
|
||||
* 别的文件按它的语言读成文档(翻译插件、库规则、项目入口同它自己的预览,规则查名字的定义、调用的种类不等),读过的各预览共用(见 [DocumentReads]),
|
||||
* 中文名只认起了中文名的定义本身。语言没有代码阅读、解析失败、没有块盖住定义处时没有。
|
||||
*/
|
||||
private async peekAt(uri: Uri, offset: number): Promise<DefinitionPeek | undefined> {
|
||||
@@ -886,12 +909,9 @@ class Preview {
|
||||
const block = peekBlock(sent.blocks, offset, line)
|
||||
return block && { path, line, block: pagePeekBlock(block, sent.source), names: namesOnCard(block, this.names.known(target.version)) }
|
||||
}
|
||||
const { message, displayNames } = await documentMessage(
|
||||
const { message, displayNames } = await this.owner.documentReads.read(
|
||||
uri.toString(),
|
||||
{ languageId: target.languageId, version: target.version, getText: () => target.getText(), fileName: target.fileName, entryNames: this.owner.entries.namesOf(uri) },
|
||||
this.registry,
|
||||
this.runtime,
|
||||
// 那个文件里规则出的错由它自己的预览记
|
||||
() => {},
|
||||
)
|
||||
const block = message.error === undefined ? peekBlock(message.blocks, offset, line) : undefined
|
||||
return block && { path, line, block: pagePeekBlock(block, message.source), names: namesOnCard(block, definitionSiteNames(displayNames)) }
|
||||
@@ -933,19 +953,13 @@ class Preview {
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到、哪些是语言自带的名字(页面当普通文字)。 */
|
||||
private async checkRepoLinks(id: number, paths: string[], references: SymbolReference[]): Promise<void> {
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const asked = new Map<string, Promise<SymbolNode[]>>()
|
||||
const symbolsOf = (uri: string): Promise<SymbolNode[]> => {
|
||||
const known = asked.get(uri) ?? documentSymbolNodes(uri)
|
||||
asked.set(uri, known)
|
||||
return known
|
||||
}
|
||||
const [missing, resolved] = await Promise.all([
|
||||
Promise.all(paths.map(async (rel) => {
|
||||
if (root === undefined || await resolveRepoEntry(root, rel) === undefined)
|
||||
return rel
|
||||
return undefined
|
||||
})),
|
||||
Promise.all(references.map(async reference => ({ reference, target: await this.resolveSymbolLink(reference, symbolsOf) }))),
|
||||
Promise.all(references.map(async reference => ({ reference, target: await this.resolveSymbolLink(reference) }))),
|
||||
])
|
||||
this.post({
|
||||
type: 'repoLinksChecked',
|
||||
@@ -1062,32 +1076,18 @@ class Preview {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:从它写在的那块(`block`,在眼下这份块树里找)的声明起一层层往外找本文件定义的,
|
||||
* 再是导入进来的、代码里用到的语言自带的名字,语言自带又问不到定义的是 `'builtin'`(见 [resolveReference])。检查坏链接、点击、悬停看定义卡片都走这里。
|
||||
* 文档符号同仓库链接找符号那一套(`symbols`,一批引用一起问时共用一份),导入的、代码里用到的名字问语言扩展它的定义(同跳到定义),定义几处取第一处;
|
||||
* 哪些名字是语言自带的问这门语言的语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))。
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪,检查坏链接、点击、悬停看定义卡片都走这里;解析过的记在 [ReferenceTargets] 里,不再问语言扩展。
|
||||
*
|
||||
* 从它写在的那块(在最近发出去的块树里找)的声明起一层层往外找本文件定义的,再找导入的、代码里用到的语言自带的名字,
|
||||
* 语言自带又问不到定义的是 `'builtin'`(见 [解析引用](vscode/src/codeReader/symbolLinks.ts#resolveReference))。出错时记一笔,当找不到。
|
||||
*/
|
||||
private async resolveSymbolLink({ symbol, block }: SymbolReference, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<ReferenceTarget> {
|
||||
const { document, sent, uses } = this
|
||||
const at = block === undefined || !sent ? undefined : referenceScope(sent.blocks, block)
|
||||
const builtins = this.registry.languageFor(document.languageId)?.builtins
|
||||
private async resolveSymbolLink(reference: SymbolReference): Promise<ReferenceTarget> {
|
||||
const { document, sent, imports, uses } = this
|
||||
try {
|
||||
return await resolveReference(symbol, at, this.imports, {
|
||||
uri: document.uri.toString(),
|
||||
symbols,
|
||||
definition: async (offset) => {
|
||||
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (!found)
|
||||
return undefined
|
||||
const doc = await workspace.openTextDocument(found.uri)
|
||||
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
|
||||
},
|
||||
uses: name => uses.get(name) ?? [],
|
||||
builtin: name => builtins?.has(name) ?? false,
|
||||
})
|
||||
return await this.references.resolve({ version: document.version, sent, imports, uses }, reference)
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能找注释里的引用 [${symbol}]:${err instanceof Error ? err.message : String(err)}` })
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能找注释里的引用 [${reference.symbol}]:${err instanceof Error ? err.message : String(err)}` })
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { SourceRange } from '../contract'
|
||||
import type { LinkTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { ReferenceInputs, TargetSources } from './referenceTargets'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ReferenceTargets } from './referenceTargets'
|
||||
|
||||
const OWN = 'file:///app/gate.py'
|
||||
const BUILTINS = 'file:///typeshed/builtins.pyi'
|
||||
const RUNTIME_ERROR: LinkTarget = { uri: BUILTINS, selection: { startIndex: 100, endIndex: 112 } }
|
||||
|
||||
function symbol(name: string, start: number, end: number): SymbolNode {
|
||||
return { name, range: { startIndex: start, endIndex: end }, selection: { startIndex: start, endIndex: start + name.length }, children: [] }
|
||||
}
|
||||
|
||||
/** 本文件定义了 `load`,代码里在 30 处用到 `RuntimeError`、在 60 处用到 `str`,问 30 处的定义落到 builtins.pyi;各文件眼下都是第 1 版 */
|
||||
function sources() {
|
||||
const versions = new Map([[OWN, 1], [BUILTINS, 1]])
|
||||
const symbols = vi.fn(async (uri: string): Promise<SymbolNode[]> => uri === OWN ? [symbol('load', 0, 80)] : [symbol('RuntimeError', 100, 200)])
|
||||
const definition = vi.fn(async (offset: number): Promise<LinkTarget | undefined> => offset === 30 ? RUNTIME_ERROR : undefined)
|
||||
const given: TargetSources = {
|
||||
uri: OWN,
|
||||
symbols,
|
||||
definition,
|
||||
builtin: name => name === 'str',
|
||||
versionOf: uri => versions.get(uri),
|
||||
}
|
||||
return { given, symbols, definition, versions }
|
||||
}
|
||||
|
||||
function inputs(version = 1, uses = new Map<string, SourceRange[]>([['RuntimeError', [{ startIndex: 30, endIndex: 42 }]], ['str', [{ startIndex: 60, endIndex: 63 }]]])): ReferenceInputs {
|
||||
return { version, sent: undefined, imports: [], uses }
|
||||
}
|
||||
|
||||
describe('注释里引用的解析结果(ReferenceTargets)', () => {
|
||||
it('同一份文档里同一个引用检查过,再悬停、再点:文档符号、定义都只问一次', async () => {
|
||||
const { given, symbols, definition } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
const checked = await Promise.all([targets.resolve(now, { symbol: 'RuntimeError', block: 'function load' }), targets.resolve(now, { symbol: 'load' })])
|
||||
expect(checked).toEqual([RUNTIME_ERROR, { uri: OWN, selection: { startIndex: 0, endIndex: 4 } }])
|
||||
expect(await targets.resolve(now, { symbol: 'RuntimeError', block: 'function load' })).toEqual(RUNTIME_ERROR)
|
||||
expect(await targets.resolve(now, { symbol: 'RuntimeError', block: 'function load' })).toEqual(RUNTIME_ERROR)
|
||||
expect(symbols).toHaveBeenCalledTimes(1)
|
||||
expect(definition).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('同时问同一个引用:等同一个 Promise,只问一次', async () => {
|
||||
const { given, definition } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
const first = targets.resolve(now, { symbol: 'RuntimeError' })
|
||||
const second = targets.resolve(now, { symbol: 'RuntimeError' })
|
||||
expect(second).toBe(first)
|
||||
await first
|
||||
expect(definition).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('同一个名字写在不同块的注释里分开记', async () => {
|
||||
const { given, definition } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
await targets.resolve(now, { symbol: 'RuntimeError', block: 'function load' })
|
||||
await targets.resolve(now, { symbol: 'RuntimeError' })
|
||||
expect(definition).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('文档换了版本,或块树、导入、用到的名字换了一份:重新问', async () => {
|
||||
const { given, symbols, definition } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const reference = { symbol: 'RuntimeError' }
|
||||
const first = inputs(1)
|
||||
await targets.resolve(first, reference)
|
||||
await targets.resolve(inputs(2), reference)
|
||||
expect(definition).toHaveBeenCalledTimes(2)
|
||||
expect(symbols).toHaveBeenCalledTimes(2)
|
||||
|
||||
const second = inputs(2)
|
||||
await targets.resolve(second, reference)
|
||||
await targets.resolve({ ...second, imports: [] }, reference)
|
||||
await targets.resolve({ ...second, sent: { blocks: [] } }, reference)
|
||||
expect(definition).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
|
||||
it('找不到的、语言自带的不留,下次再问;语言扩展先答空、后来答上了的用后来的', async () => {
|
||||
const { given, definition } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
expect(await targets.resolve(now, { symbol: 'str' })).toBe('builtin')
|
||||
expect(await targets.resolve(now, { symbol: 'str' })).toBe('builtin')
|
||||
expect(definition.mock.calls.filter(([offset]) => offset === 60)).toHaveLength(2)
|
||||
|
||||
definition.mockResolvedValueOnce(undefined)
|
||||
expect(await targets.resolve(now, { symbol: 'RuntimeError' })).toBeUndefined()
|
||||
expect(await targets.resolve(now, { symbol: 'RuntimeError' })).toEqual(RUNTIME_ERROR)
|
||||
})
|
||||
|
||||
it('问出错的不留:下次再问', async () => {
|
||||
const { given, definition } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
definition.mockRejectedValueOnce(new Error('Pylance 还没起来'))
|
||||
await expect(targets.resolve(now, { symbol: 'RuntimeError' })).rejects.toThrow('Pylance 还没起来')
|
||||
expect(await targets.resolve(now, { symbol: 'RuntimeError' })).toEqual(RUNTIME_ERROR)
|
||||
})
|
||||
|
||||
it('本文件的文档符号答空的不留:下次再问', async () => {
|
||||
const { given, symbols } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
symbols.mockResolvedValueOnce([])
|
||||
expect(await targets.resolve(now, { symbol: 'load' })).toBeUndefined()
|
||||
expect(await targets.resolve(now, { symbol: 'load' })).toEqual({ uri: OWN, selection: { startIndex: 0, endIndex: 4 } })
|
||||
expect(symbols).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('指到的别的文件改过(换了版本)、关掉了:重新问', async () => {
|
||||
const { given, definition, versions } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
const now = inputs()
|
||||
const reference = { symbol: 'RuntimeError' }
|
||||
await targets.resolve(now, reference)
|
||||
versions.set(BUILTINS, 2)
|
||||
await targets.resolve(now, reference)
|
||||
expect(definition).toHaveBeenCalledTimes(2)
|
||||
await targets.resolve(now, reference)
|
||||
expect(definition).toHaveBeenCalledTimes(2)
|
||||
|
||||
versions.delete(BUILTINS)
|
||||
await targets.resolve(now, reference)
|
||||
expect(definition).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('成员落在别的文件的文档符号里:那个文件改过后连文档符号一起重新问', async () => {
|
||||
const { given, symbols, versions } = sources()
|
||||
const targets = new ReferenceTargets(given)
|
||||
symbols.mockImplementation(async uri => uri === OWN
|
||||
? [symbol('load', 0, 80)]
|
||||
: [{ ...symbol('RuntimeError', 100, 200), children: [symbol('args', 150, 160)] }])
|
||||
const now = inputs()
|
||||
const reference = { symbol: 'RuntimeError.args' }
|
||||
expect(await targets.resolve(now, reference)).toEqual({ uri: BUILTINS, selection: { startIndex: 150, endIndex: 154 } })
|
||||
expect(await targets.resolve(now, reference)).toEqual({ uri: BUILTINS, selection: { startIndex: 150, endIndex: 154 } })
|
||||
expect(symbols.mock.calls.filter(([uri]) => uri === BUILTINS)).toHaveLength(1)
|
||||
|
||||
versions.set(BUILTINS, 2)
|
||||
await targets.resolve(now, reference)
|
||||
expect(symbols.mock.calls.filter(([uri]) => uri === BUILTINS)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { SourceRange } from '../contract'
|
||||
import type { ImportedName, ReferenceSources, ReferenceTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { SymbolReference } from './messages'
|
||||
import { referenceScope, resolveReference } from '../symbolLinks'
|
||||
|
||||
/**
|
||||
* 解析引用时本文件的样子:文档版本、最近一次发出去的块树(没有时只从文件顶层找)、各条导入引进来的名字、代码里用到又没定义的名字各在哪几处。
|
||||
* 哪样换了,引用从哪个声明起找、导入和用到的名字在哪都可能跟着变。
|
||||
*/
|
||||
export interface ReferenceInputs {
|
||||
version: number
|
||||
sent: { blocks: BlockView[] } | undefined
|
||||
imports: readonly ImportedName[]
|
||||
uses: ReadonlyMap<string, readonly SourceRange[]>
|
||||
}
|
||||
|
||||
/** 解析引用要问的(见 [ReferenceSources],代码里用到的名字由 [ReferenceInputs] 给),和眼下打开着的一个文件是第几版(没打开时没有)。 */
|
||||
export interface TargetSources extends Omit<ReferenceSources, 'uses'> {
|
||||
versionOf: (uri: string) => number | undefined
|
||||
}
|
||||
|
||||
/** 记着的一份:问的 Promise,问完留下来的再记着结果落在哪个文件、那个文件当时第几版。 */
|
||||
interface Kept<T> {
|
||||
value: Promise<T>
|
||||
file?: { uri: string, version: number }
|
||||
}
|
||||
|
||||
/** 按同一份 [ReferenceInputs] 记着的一轮:各引用指到哪,各文件的文档符号。 */
|
||||
interface Round {
|
||||
inputs: ReferenceInputs
|
||||
targets: Map<string, Kept<ReferenceTarget>>
|
||||
symbols: Map<string, Kept<readonly SymbolNode[]>>
|
||||
}
|
||||
|
||||
/** 引用的键:同一个名字写在不同块的注释里分开记,指到的可以不一样(拼法同[页面那边的键](vscode/webview-ui/src/codeReader/repoLink.ts#referenceKey))。 */
|
||||
function referenceKey({ symbol, block }: SymbolReference): string {
|
||||
return `${symbol}\u0000${block ?? ''}`
|
||||
}
|
||||
|
||||
function sameInputs(a: ReferenceInputs, b: ReferenceInputs): boolean {
|
||||
return a.version === b.version && a.sent === b.sent && a.imports === b.imports && a.uses === b.uses
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个预览里注释中不带路径的引用指到哪(见 [resolveReference]),解析过的记着:标链接、悬停、点击同一个引用只问一次语言扩展,同时问的等同一个 Promise。
|
||||
*
|
||||
* 本文件的样子([ReferenceInputs])换了,记着的全部作废。指到的文件改过、关掉了的再问一遍,免得范围指错地方。
|
||||
* 找不到的、语言自带的、出错的不留,因为语言扩展还没准备好时会暂时答空,下次要再问。各文件的文档符号也这样记着,一批引用共用一份。
|
||||
*/
|
||||
export class ReferenceTargets {
|
||||
private round: Round | undefined
|
||||
|
||||
constructor(private readonly sources: TargetSources) {}
|
||||
|
||||
/** 本文件是 `inputs` 这个样子时,`reference` 指到哪。 */
|
||||
resolve(inputs: ReferenceInputs, reference: SymbolReference): Promise<ReferenceTarget> {
|
||||
const round = this.roundOf(inputs)
|
||||
const { uri, definition, builtin } = this.sources
|
||||
const symbols = (file: string): Promise<readonly SymbolNode[]> =>
|
||||
this.share(round.symbols, file, () => this.sources.symbols(file), nodes => nodes.length > 0 ? file : undefined)
|
||||
return this.share(round.targets, referenceKey(reference), () => {
|
||||
const { symbol, block } = reference
|
||||
const at = block === undefined || !inputs.sent ? undefined : referenceScope(inputs.sent.blocks, block)
|
||||
return resolveReference(symbol, at, inputs.imports, { uri, symbols, definition, builtin, uses: name => inputs.uses.get(name) ?? [] })
|
||||
}, target => target && target !== 'builtin' ? target.uri : undefined)
|
||||
}
|
||||
|
||||
/** 按 `inputs` 记的这一轮:本文件的样子换了就开新的一轮,旧一轮里还在问的问完也不记。 */
|
||||
private roundOf(inputs: ReferenceInputs): Round {
|
||||
if (!this.round || !sameInputs(this.round.inputs, inputs))
|
||||
this.round = { inputs, targets: new Map(), symbols: new Map() }
|
||||
return this.round
|
||||
}
|
||||
|
||||
/**
|
||||
* `kept` 里 `key` 那份:记着、指到的文件也没改过就给它,没有就 `ask` 一份记上,问完 `fileOf` 说结果落在哪个文件。
|
||||
* 不说(不留的结果)、那个文件眼下没打开、问失败了的拿掉,留下的记着那个文件此刻第几版。
|
||||
*/
|
||||
private share<T>(kept: Map<string, Kept<T>>, key: string, ask: () => Promise<T>, fileOf: (value: T) => string | undefined): Promise<T> {
|
||||
const { versionOf } = this.sources
|
||||
const known = kept.get(key)
|
||||
if (known && (!known.file || versionOf(known.file.uri) === known.file.version))
|
||||
return known.value
|
||||
const entry: Kept<T> = { value: ask() }
|
||||
kept.set(key, entry)
|
||||
const drop = (): void => {
|
||||
if (kept.get(key) === entry)
|
||||
kept.delete(key)
|
||||
}
|
||||
entry.value.then((value) => {
|
||||
const uri = fileOf(value)
|
||||
const version = uri === undefined ? undefined : versionOf(uri)
|
||||
if (uri === undefined || version === undefined)
|
||||
drop()
|
||||
else
|
||||
entry.file = { uri, version }
|
||||
}, drop)
|
||||
return entry.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RecentPromises } from './recentPromises'
|
||||
|
||||
describe('最近用过的几份异步结果(RecentPromises)', () => {
|
||||
it('同一个键按同样的 stamp 再要给记着的那份,stamp 变了没有;换上新的那份后旧的不再有', async () => {
|
||||
const kept = new RecentPromises<string>(4)
|
||||
const first = kept.set('a', 1, Promise.resolve('一'))
|
||||
expect(kept.get('a', 1)).toBe(first)
|
||||
expect(kept.get('a', 2)).toBeUndefined()
|
||||
const second = kept.set('a', 2, Promise.resolve('二'))
|
||||
expect(kept.get('a', 2)).toBe(second)
|
||||
expect(kept.get('a', 1)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('算失败的拿掉,下次重算;失败前已经换上的新的那份不受影响', async () => {
|
||||
const kept = new RecentPromises<string>(4)
|
||||
const failed = kept.set('a', 1, Promise.reject(new Error('坏了')))
|
||||
await expect(failed).rejects.toThrow('坏了')
|
||||
expect(kept.get('a', 1)).toBeUndefined()
|
||||
|
||||
let fail: (err: Error) => void = () => {}
|
||||
const slow = kept.set('b', 1, new Promise<string>((_resolve, reject) => {
|
||||
fail = reject
|
||||
}))
|
||||
const fresh = kept.set('b', 2, Promise.resolve('新'))
|
||||
fail(new Error('旧的坏了'))
|
||||
await expect(slow).rejects.toThrow('旧的坏了')
|
||||
expect(kept.get('b', 2)).toBe(fresh)
|
||||
})
|
||||
|
||||
it('最多留 limit 份,拿过的算用过,最久没用的先丢;clear 全部丢掉', () => {
|
||||
const kept = new RecentPromises<string>(2)
|
||||
kept.set('a', 1, Promise.resolve('a'))
|
||||
kept.set('b', 1, Promise.resolve('b'))
|
||||
kept.get('a', 1)
|
||||
kept.set('c', 1, Promise.resolve('c'))
|
||||
expect(kept.get('b', 1)).toBeUndefined()
|
||||
expect(kept.get('a', 1)).toBeDefined()
|
||||
expect(kept.get('c', 1)).toBeDefined()
|
||||
|
||||
kept.clear()
|
||||
expect(kept.get('a', 1)).toBeUndefined()
|
||||
expect(kept.get('c', 1)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 按键记着最近用过的几份异步结果,同一个键按同样的东西(`stamp`,比如文件的版本)再要时直接给记着的那份,正在算的也给同一个 Promise,
|
||||
* 免得同一份东西反复算;`stamp` 变了就换成新算的,算失败的不留、下次重算,多了丢最久没用的。这里不碰 VS Code。
|
||||
*/
|
||||
export class RecentPromises<T> {
|
||||
private readonly entries = new Map<string, { stamp: string | number, value: Promise<T> }>()
|
||||
|
||||
constructor(private readonly limit: number) {}
|
||||
|
||||
/** `key` 记着的、按 `stamp` 算的那份,拿到的挪到最近用过的那头;没记过或记的不是按 `stamp` 算的时没有。 */
|
||||
get(key: string, stamp: string | number): Promise<T> | undefined {
|
||||
const known = this.entries.get(key)
|
||||
if (known?.stamp !== stamp)
|
||||
return undefined
|
||||
this.keep(key, known)
|
||||
return known.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 记下 `key` 按 `stamp` 算的 `value`,换掉这个键原来那份;`value` 失败时拿掉,下次重算。
|
||||
*
|
||||
* @return `value` 本身,好接着用
|
||||
*/
|
||||
set(key: string, stamp: string | number, value: Promise<T>): Promise<T> {
|
||||
const entry = { stamp, value }
|
||||
this.keep(key, entry)
|
||||
value.catch(() => {
|
||||
if (this.entries.get(key) === entry)
|
||||
this.entries.delete(key)
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
/** 全部丢掉:算它们用的东西整个变了时。 */
|
||||
clear(): void {
|
||||
this.entries.clear()
|
||||
}
|
||||
|
||||
/** 挪到最近用过的那头,多了丢最久没用的。 */
|
||||
private keep(key: string, entry: { stamp: string | number, value: Promise<T> }): void {
|
||||
this.entries.delete(key)
|
||||
this.entries.set(key, entry)
|
||||
for (const oldest of this.entries.keys()) {
|
||||
if (this.entries.size <= this.limit)
|
||||
break
|
||||
this.entries.delete(oldest)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user