import type { IndexedFile, IndexEntry, Node, WorkspaceIndex } from './contract' import { describe, expect, it } from 'vitest' import { createPythonAdapter } from './languages/python' import { MemoryIndexHost, WASM_DIR } from './testSupport' import { WorkspaceIndexes } from './workspaceIndex' const py = createPythonAdapter(WASM_DIR) /** 测试用的索引:每个函数一项,键是「包路径:函数名」,文件所在目录有 __init__.py 时包路径是目录,否则是空的。 */ function functionIndex(seen: string[] = []): WorkspaceIndex { return { id: 'test.functions', languageId: 'python', include: '**/*.py', mentions: source => source.includes('def '), entries: (root: Node, file: IndexedFile): IndexEntry[] => { seen.push(file.path) const directory = file.path.split('/').slice(0, -1).join('/') const prefix = file.exists(`${directory}/./__init__.py`) ? directory : '' return root.descendantsOfType('function_definition').flatMap((fn) => { const name = fn?.childForFieldName('name') return name ? [{ key: `${prefix}:${name.text}`, name: { startIndex: name.startIndex, endIndex: name.endIndex } }] : [] }) }, } } function setup(files: Record): { host: MemoryIndexHost, indexes: WorkspaceIndexes } { const host = new MemoryIndexHost(files, py) return { host, indexes: new WorkspaceIndexes(host) } } describe('工作区索引', () => { it('第一次查才建;查到的是名字的行列(列按 UTF-16 算),照文件、行排好', async () => { const { host, indexes } = setup({ 'b.py': '# 中文注释\ndef load():\n pass\n', 'a.py': 'def load():\n pass\n\n\ndef load():\n pass\n', 'notes.txt': 'def load():', }) expect(host.lists).toBe(0) expect(await indexes.lookup(functionIndex(), ':load')).toEqual([ { uri: 'file:///w/a.py', start: { line: 0, character: 4 }, end: { line: 0, character: 8 } }, { uri: 'file:///w/a.py', start: { line: 4, character: 4 }, end: { line: 4, character: 8 } }, { uri: 'file:///w/b.py', start: { line: 1, character: 4 }, end: { line: 1, character: 8 } }, ]) expect(await indexes.lookup(functionIndex(), ':missing')).toEqual([]) expect(host.lists).toBe(1) }) it('预筛不过的文件不解析', async () => { const seen: string[] = [] const { indexes } = setup({ 'a.py': 'def a(): pass\n', 'b.py': 'x = 1\n' }) await indexes.lookup(functionIndex(seen), ':a') expect(seen).toEqual(['a.py']) }) it('文件改了只重看那一份;新建的加进来;删掉的目录连里面的文件一起去掉', async () => { const { host, indexes } = setup({ 'a.py': 'def a(): pass\n', 'pkg/b.py': 'def b(): pass\n', 'pkg/deep/c.py': 'def c(): pass\n' }) const index = functionIndex() await indexes.lookup(index, ':a') host.reads.length = 0 host.emit('change', 'a.py', 'def renamed(): pass\n') expect(await indexes.lookup(index, ':a')).toEqual([]) expect(await indexes.lookup(index, ':renamed')).toHaveLength(1) expect(host.reads).toEqual(['a.py']) host.emit('create', 'd.py', '\n\ndef d(): pass\n') expect((await indexes.lookup(index, ':d'))[0].start).toEqual({ line: 2, character: 4 }) host.emit('delete', 'pkg') expect(await indexes.lookup(index, ':b')).toEqual([]) expect(await indexes.lookup(index, ':c')).toEqual([]) expect(host.reads).toEqual(['a.py', 'd.py']) }) it('新建、删掉别的文件问过在不在的文件(__init__.py)时,问过它的文件跟着重看', async () => { const { host, indexes } = setup({ 'pkg/mod.py': 'def run(): pass\n', 'other.py': 'def other(): pass\n' }) const index = functionIndex() expect(await indexes.lookup(index, ':run')).toHaveLength(1) host.reads.length = 0 host.emit('create', 'pkg/__init__.py', '') expect(await indexes.lookup(index, 'pkg:run')).toHaveLength(1) expect(host.reads.sort()).toEqual(['pkg/__init__.py', 'pkg/mod.py']) host.emit('delete', 'pkg/__init__.py') expect(await indexes.lookup(index, ':run')).toHaveLength(1) }) it('建索引期间的改动建好后补上', async () => { const { host, indexes } = setup({ 'a.py': 'def a(): pass\n' }) const index = functionIndex() const first = indexes.lookup(index, ':a') host.emit('change', 'a.py', 'def b(): pass\n') await first expect(await indexes.lookup(index, ':b')).toHaveLength(1) expect(await indexes.lookup(index, ':a')).toEqual([]) }) it('读不了、规则出错的文件记下来,别的照常', async () => { const { host, indexes } = setup({ 'a.py': 'def a(): pass\n', 'bad.py': 'def boom(): pass\n' }) host.files.delete('bad.py') host.files.set('b.py', 'def b(): pass\n') const index = functionIndex() const broken: WorkspaceIndex = { ...index, id: 'test.broken', entries: (root, file) => { if (file.path === 'a.py') throw new Error('认不了') return index.entries(root, file) }, } // bad.py 在找到的文件里,读的时候已经没了 host.list = async () => ['a.py', 'bad.py', 'b.py'].map(relative => host.file(relative)) expect(await indexes.lookup(broken, ':b')).toHaveLength(1) expect([...host.reports].sort()).toEqual([ '索引 test.broken 看 file:///w/a.py 时出错:认不了', '索引 test.broken 读不了 file:///w/bad.py:没有 bad.py', ]) }) it('同一个 id 只建一份;dispose 后不再盯着文件', async () => { const { host, indexes } = setup({ 'a.py': 'def a(): pass\n' }) await indexes.lookup(functionIndex(), ':a') await indexes.lookup(functionIndex(), ':a') expect(host.lists).toBe(1) expect(host.watching).toBe(1) indexes.dispose() expect(host.watching).toBe(0) }) })