150 lines
7.2 KiB
TypeScript
150 lines
7.2 KiB
TypeScript
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)
|
||
})
|
||
})
|