⚡ perf(vscode): 代码阅读预览注释里的引用解析过就记着:标坏链接、悬停、点击同一个引用只问一次 Pylance,同时问的等同一个结果,文档版本或块树、导入、用到的名字换了才作废,指到的别的文件改过再问,找不到和语言自带的不留;悬停看别的文件里的定义时读成的文档各预览共用,同一版不再重新解析,留最近 8 份,注册的插件变了全部重读

This commit is contained in:
cruldra committed 2026-10-14 19:57:20 +11:00
1 parent 3983032356
commit cfde0fedf5
9 files changed
+595 -70

No files matched your search

@@ -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)
})
})