✨ feat(vscode): 代码阅读加 Celery 库规则,定时任务表里的任务名能跳到任务函数

This commit is contained in:
cruldra committed 2026-09-26 19:41:07 +08:00
1 parent 905d7845b5
commit 68104e2cba
22 files changed
+2228 -139

No files matched your search

@@ -0,0 +1,133 @@
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<string, string>): { 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)
})
})