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 => uri === OWN ? [symbol('load', 0, 80)] : [symbol('RuntimeError', 100, 200)]) const definition = vi.fn(async (offset: number): Promise => 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([['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) }) })