✨ feat(vscode): 代码阅读加 Celery 库规则,定时任务表里的任务名能跳到任务函数
This commit is contained in:
1 parent
905d7845b5
commit
68104e2cba
22 files changed
+2228
-139
No files matched your search
@@ -0,0 +1,207 @@
|
||||
import type { Tree } from '@vscode/tree-sitter-wasm'
|
||||
import type { RuleLocation, RulePosition, RuleWorkspace, WorkspaceIndex } from './contract'
|
||||
import * as path from 'node:path'
|
||||
|
||||
/**
|
||||
* 库规则的工作区索引(WorkspaceIndex)的宿主部分:找文件、预筛、解析、缓存,跟着文件改动只重看改了的文件。
|
||||
* 规则只说要什么;这里不碰 VS Code,工作区的能力由 IndexHost 给(VS Code 里是 workspaceFiles.ts,测试里是内存里的文件)。
|
||||
*/
|
||||
|
||||
/** 工作区里的一份文件:uri,它所在的工作区文件夹(uri)和相对这个文件夹的路径(/ 分隔)。 */
|
||||
export interface WorkspaceFile { uri: string, folder: string, path: string }
|
||||
|
||||
/** 文件改动:改了、新建了、删了(删的可能是目录,里面的文件一起算删了)。 */
|
||||
export interface FileEvent { kind: 'change' | 'create' | 'delete', file: WorkspaceFile }
|
||||
|
||||
export interface IndexHost {
|
||||
/** include 在各工作区文件夹里找到的文件,排除的不算。 */
|
||||
list: (include: string) => Promise<WorkspaceFile[]>
|
||||
read: (file: WorkspaceFile) => Promise<string>
|
||||
/** 盯着文件改动:include 以外和被排除的文件改了、新建了不报,删除都报。 */
|
||||
watch: (include: string, listener: (event: FileEvent) => void) => { dispose: () => void }
|
||||
/** 用 languageId 的语言插件解析;返回的语法树用完要 delete。 */
|
||||
parse: (languageId: string, source: string) => Promise<Tree>
|
||||
report: (message: string) => void
|
||||
}
|
||||
|
||||
/** 一次读几份文件:建索引时不一口气把整个工作区的文件都读进内存。 */
|
||||
const READ_BATCH = 32
|
||||
|
||||
function keyOf(folder: string, relative: string): string {
|
||||
return `${folder}\n${path.posix.normalize(relative)}`
|
||||
}
|
||||
|
||||
/** 每一行开头在源码里的下标。 */
|
||||
function lineStarts(source: string): number[] {
|
||||
const starts = [0]
|
||||
for (let i = source.indexOf('\n'); i >= 0; i = source.indexOf('\n', i + 1))
|
||||
starts.push(i + 1)
|
||||
return starts
|
||||
}
|
||||
|
||||
function positionAt(starts: number[], index: number): RulePosition {
|
||||
let low = 0
|
||||
let high = starts.length - 1
|
||||
while (low < high) {
|
||||
const middle = (low + high + 1) >> 1
|
||||
if (starts[middle] <= index)
|
||||
low = middle
|
||||
else
|
||||
high = middle - 1
|
||||
}
|
||||
return { line: low, character: index - starts[low] }
|
||||
}
|
||||
|
||||
/** 一份看过的文件:认出的各项,和建索引时问过在不在的文件(它们新建、删掉时这份要重看)。 */
|
||||
interface Indexed {
|
||||
file: WorkspaceFile
|
||||
entries: { key: string, location: RuleLocation }[]
|
||||
asked: Set<string>
|
||||
}
|
||||
|
||||
function compareLocations(a: RuleLocation, b: RuleLocation): number {
|
||||
return a.uri.localeCompare(b.uri) || a.start.line - b.start.line || a.start.character - b.start.character
|
||||
}
|
||||
|
||||
/** 一个 WorkspaceIndex 建出来的索引:建好之前的查询、文件改动都排在建索引后面,一件件来。 */
|
||||
class BuiltIndex {
|
||||
/** include 找到的全部文件(按 keyOf),IndexedFile.exists 查的就是它 */
|
||||
private readonly files = new Map<string, WorkspaceFile>()
|
||||
/** 预筛过了、解析过的文件 */
|
||||
private readonly indexed = new Map<string, Indexed>()
|
||||
private readonly watcher: { dispose: () => void }
|
||||
private queue: Promise<void>
|
||||
|
||||
constructor(private readonly index: WorkspaceIndex, private readonly host: IndexHost) {
|
||||
// 先盯着再找文件:找文件期间的改动排在建好之后补上
|
||||
this.watcher = host.watch(index.include, event => this.enqueue(() => this.apply(event)))
|
||||
this.queue = this.guard(() => this.build())
|
||||
}
|
||||
|
||||
async lookup(key: string): Promise<RuleLocation[]> {
|
||||
await this.queue
|
||||
return [...this.indexed.values()]
|
||||
.flatMap(indexed => indexed.entries.filter(entry => entry.key === key).map(entry => entry.location))
|
||||
.sort(compareLocations)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.watcher.dispose()
|
||||
}
|
||||
|
||||
private enqueue(task: () => Promise<void>): void {
|
||||
this.queue = this.queue.then(() => this.guard(task))
|
||||
}
|
||||
|
||||
private async guard(task: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await task()
|
||||
}
|
||||
catch (err) {
|
||||
this.host.report(`索引 ${this.index.id} 更新失败:${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async build(): Promise<void> {
|
||||
const files = await this.host.list(this.index.include)
|
||||
for (const file of files)
|
||||
this.files.set(keyOf(file.folder, file.path), file)
|
||||
for (let i = 0; i < files.length; i += READ_BATCH)
|
||||
await Promise.all(files.slice(i, i + READ_BATCH).map(file => this.visit(file)))
|
||||
}
|
||||
|
||||
/** 重看一份文件:读、预筛、解析、交给规则认。 */
|
||||
private async visit(file: WorkspaceFile): Promise<void> {
|
||||
const key = keyOf(file.folder, file.path)
|
||||
this.indexed.delete(key)
|
||||
let source: string
|
||||
let tree: Tree
|
||||
try {
|
||||
source = await this.host.read(file)
|
||||
if (!this.index.mentions(source))
|
||||
return
|
||||
tree = await this.host.parse(this.index.languageId, source)
|
||||
}
|
||||
catch (err) {
|
||||
this.host.report(`索引 ${this.index.id} 读不了 ${file.uri}:${err instanceof Error ? err.message : String(err)}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const asked = new Set<string>()
|
||||
const exists = (relative: string): boolean => {
|
||||
const other = keyOf(file.folder, relative)
|
||||
asked.add(other)
|
||||
return this.files.has(other)
|
||||
}
|
||||
const starts = lineStarts(source)
|
||||
const entries = this.index.entries(tree.rootNode, { path: file.path, source, exists }).map(({ key: entryKey, name }) => ({
|
||||
key: entryKey,
|
||||
location: { uri: file.uri, start: positionAt(starts, name.startIndex), end: positionAt(starts, name.endIndex) },
|
||||
}))
|
||||
this.indexed.set(key, { file, entries, asked })
|
||||
}
|
||||
catch (err) {
|
||||
this.host.report(`索引 ${this.index.id} 看 ${file.uri} 时出错:${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private async apply({ kind, file }: FileEvent): Promise<void> {
|
||||
const key = keyOf(file.folder, file.path)
|
||||
if (kind === 'change') {
|
||||
if (this.files.has(key))
|
||||
await this.visit(file)
|
||||
return
|
||||
}
|
||||
const touched = kind === 'create' ? this.add(file) : this.remove(file)
|
||||
// 问过这些文件在不在的(Python 包路径看 __init__.py)跟着重看
|
||||
const dependents = [...this.indexed.values()]
|
||||
.filter(indexed => touched.some(other => indexed.asked.has(other)))
|
||||
.map(indexed => indexed.file)
|
||||
.filter(dependent => keyOf(dependent.folder, dependent.path) !== key)
|
||||
await Promise.all([...(kind === 'create' ? [file] : []), ...dependents].map(target => this.visit(target)))
|
||||
}
|
||||
|
||||
private add(file: WorkspaceFile): string[] {
|
||||
const key = keyOf(file.folder, file.path)
|
||||
this.files.set(key, file)
|
||||
return [key]
|
||||
}
|
||||
|
||||
/** 删掉文件,或目录连同里面的文件。 */
|
||||
private remove(file: WorkspaceFile): string[] {
|
||||
const prefix = `${path.posix.normalize(file.path)}/`
|
||||
const gone = [...this.files.entries()]
|
||||
.filter(([, known]) => known.folder === file.folder && (known.path === file.path || known.path.startsWith(prefix)))
|
||||
.map(([key]) => key)
|
||||
for (const key of gone) {
|
||||
this.files.delete(key)
|
||||
this.indexed.delete(key)
|
||||
}
|
||||
return gone
|
||||
}
|
||||
}
|
||||
|
||||
/** 各库规则的工作区索引:第一次查某个索引时才建,同一个 id 只建一份。 */
|
||||
export class WorkspaceIndexes implements RuleWorkspace {
|
||||
private readonly built = new Map<string, BuiltIndex>()
|
||||
|
||||
constructor(private readonly host: IndexHost) {}
|
||||
|
||||
lookup(index: WorkspaceIndex, key: string): Promise<RuleLocation[]> {
|
||||
let built = this.built.get(index.id)
|
||||
if (!built) {
|
||||
built = new BuiltIndex(index, this.host)
|
||||
this.built.set(index.id, built)
|
||||
}
|
||||
return built.lookup(key)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const built of this.built.values())
|
||||
built.dispose()
|
||||
this.built.clear()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user