✨ 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,65 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { RuleReference } from '../contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { celeryRule } from '../rules/celery'
|
||||
import { readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
import { DefinitionSources, referencesIn } from './definitionSources'
|
||||
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const SNAPSHOT = readFixture('celeryApp.py')
|
||||
|
||||
function views(source: string): Promise<BlockView[]> {
|
||||
return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, source, py, pythonZhTranslator, { rules: [celeryRule], report: () => {} }))
|
||||
}
|
||||
|
||||
/** 记下问了哪边:规则(引用的键)还是语言扩展(下标)。 */
|
||||
function recorder(): { asked: string[], sources: DefinitionSources<string> } {
|
||||
const asked: string[] = []
|
||||
const sources = new DefinitionSources<string>({
|
||||
rule: async (reference: RuleReference) => {
|
||||
asked.push(`规则 ${reference.rule} ${reference.key}`)
|
||||
return ['tasks.py']
|
||||
},
|
||||
language: async (offset: number) => {
|
||||
asked.push(`语言扩展 ${offset}`)
|
||||
return []
|
||||
},
|
||||
})
|
||||
return { asked, sources }
|
||||
}
|
||||
|
||||
describe('名字的定义去哪儿查', () => {
|
||||
it('视图数据里规则标了引用的名字:下标 → 引用(任务表里的任务名)', async () => {
|
||||
const references = referencesIn(await views(SNAPSHOT))
|
||||
expect(references.size).toBe(12)
|
||||
const offset = SNAPSHOT.indexOf('commerce.retry_failed_fulfillments",\n "schedule"')
|
||||
expect(references.get(offset)).toEqual({ rule: 'celery', key: 'commerce.retry_failed_fulfillments' })
|
||||
})
|
||||
|
||||
it('规则标了引用的问规则,其余(同一张卡片里的应用名、别的块里的名字)问语言扩展', async () => {
|
||||
const blocks = await views(SNAPSHOT)
|
||||
const { asked, sources } = recorder()
|
||||
sources.remember(3, blocks)
|
||||
const task = SNAPSHOT.indexOf('commerce.retry_failed_fulfillments",\n "schedule"')
|
||||
const app = SNAPSHOT.indexOf('celery_app.conf.beat_schedule')
|
||||
|
||||
expect(await sources.locate(3, task)).toEqual(['tasks.py'])
|
||||
expect(await sources.locate(3, app)).toEqual([])
|
||||
expect(asked).toEqual([`规则 celery commerce.retry_failed_fulfillments`, `语言扩展 ${app}`])
|
||||
})
|
||||
|
||||
it('引用只对记下它的那个文档版本有效;没记过文档时都问语言扩展', async () => {
|
||||
const blocks = await views(SNAPSHOT)
|
||||
const task = SNAPSHOT.indexOf('commerce.retry_failed_fulfillments",\n "schedule"')
|
||||
const { asked, sources } = recorder()
|
||||
await sources.locate(1, task)
|
||||
sources.remember(3, blocks)
|
||||
await sources.locate(4, task)
|
||||
sources.remember(4, [])
|
||||
await sources.locate(4, task)
|
||||
expect(asked).toEqual([`语言扩展 ${task}`, `语言扩展 ${task}`, `语言扩展 ${task}`])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { RuleReference } from '../contract'
|
||||
import { referencedSegments } from '../libraryRules'
|
||||
|
||||
/** 视图数据里库规则标了引用的名字:源码下标 → 引用。引用只出在规则认领的块的属性表里。 */
|
||||
export function referencesIn(blocks: readonly BlockView[]): Map<number, RuleReference> {
|
||||
const found = new Map<number, RuleReference>()
|
||||
const visit = (list: readonly BlockView[]): void => {
|
||||
for (const block of list) {
|
||||
if ('properties' in block) {
|
||||
for (const { offset, reference } of referencedSegments(block.properties)) {
|
||||
if (offset !== undefined && reference)
|
||||
found.set(offset, reference)
|
||||
}
|
||||
}
|
||||
visit(block.children)
|
||||
}
|
||||
}
|
||||
visit(blocks)
|
||||
return found
|
||||
}
|
||||
|
||||
/** 两处查定义的地方:库规则(查它自己标的引用)和语言扩展(其余的名字,和普通编辑器同一套)。 */
|
||||
export interface DefinitionProviders<L> {
|
||||
rule: (reference: RuleReference) => Promise<L[]>
|
||||
language: (offset: number) => Promise<L[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览里一个名字的定义去哪儿查:发出去的文档里库规则标了引用的问规则,其余问语言扩展。
|
||||
* webview 只报名字的源码下标,⌘ / Ctrl 悬停、点击都走这一条路,链接样子、跳转、挑选、闪都一样。
|
||||
* 引用按最近发出去的那份文档记,只对它的版本有效。
|
||||
*/
|
||||
export class DefinitionSources<L> {
|
||||
private version: number | undefined
|
||||
private references = new Map<number, RuleReference>()
|
||||
|
||||
constructor(private readonly providers: DefinitionProviders<L>) {}
|
||||
|
||||
/** 发出去了一份文档:记下它里面的引用。 */
|
||||
remember(version: number, blocks: readonly BlockView[]): void {
|
||||
this.version = version
|
||||
this.references = referencesIn(blocks)
|
||||
}
|
||||
|
||||
/** version 这份文档里 offset 处名字的各处定义。 */
|
||||
locate(version: number, offset: number): Promise<L[]> {
|
||||
const reference = version === this.version ? this.references.get(offset) : undefined
|
||||
return reference ? this.providers.rule(reference) : this.providers.language(offset)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,44 @@
|
||||
import type { Location, LocationLink, Position, Range, Uri } from 'vscode'
|
||||
import type { Location, LocationLink, Position } from 'vscode'
|
||||
import type { RuleLocation } from '../contract'
|
||||
import type { DefinitionTarget } from './definitionAction'
|
||||
import { commands, window, workspace } from 'vscode'
|
||||
import { commands, Range, Uri, window, workspace } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
|
||||
/** 查到的一处定义:文件和定义处那个名字的范围。 */
|
||||
export interface FoundLocation { uri: Uri, range: Range }
|
||||
|
||||
/** VS Code 查到的一处定义,加上打开目标文件才知道的信息。 */
|
||||
export interface Definition extends DefinitionTarget {
|
||||
location: { uri: Uri, range: Range }
|
||||
location: FoundLocation
|
||||
/** 定义处在目标文件里的下标,预览靠它找到是哪块、哪一行属性 */
|
||||
offset: number
|
||||
/** 定义所在那一行的源码,列在挑选框里 */
|
||||
lineText: string
|
||||
}
|
||||
|
||||
/** 交给 VS Code 注册的定义提供者(Pylance、TypeScript 语言服务……),和普通编辑器里 ⌘ + 点击查的是同一套。 */
|
||||
async function provided(uri: Uri, position: Position): Promise<(Location | LocationLink)[]> {
|
||||
/** uri 里 position 处名字的定义:交给 VS Code 注册的定义提供者(Pylance、TypeScript 语言服务……),和普通编辑器里 ⌘ + 点击查的是同一套。 */
|
||||
export async function languageDefinitions(uri: Uri, position: Position): Promise<FoundLocation[]> {
|
||||
let provided: (Location | LocationLink)[]
|
||||
try {
|
||||
return await commands.executeCommand<(Location | LocationLink)[] | undefined>('vscode.executeDefinitionProvider', uri, position) ?? []
|
||||
provided = await commands.executeCommand<(Location | LocationLink)[] | undefined>('vscode.executeDefinitionProvider', uri, position) ?? []
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `查定义失败 ${uri.toString()}:${position.line + 1}:${err instanceof Error ? err.message : String(err)}` })
|
||||
return []
|
||||
}
|
||||
return provided.map(item => 'targetUri' in item
|
||||
? { uri: item.targetUri, range: item.targetSelectionRange ?? item.targetRange }
|
||||
: { uri: item.uri, range: item.range })
|
||||
}
|
||||
|
||||
export async function hasDefinition(uri: Uri, position: Position): Promise<boolean> {
|
||||
return (await provided(uri, position)).length > 0
|
||||
/** 库规则查到的定义(RuleLocation)换成 VS Code 的位置。 */
|
||||
export function ruleDefinitions(locations: readonly RuleLocation[]): FoundLocation[] {
|
||||
return locations.map(({ uri, start, end }) => ({ uri: Uri.parse(uri), range: new Range(start.line, start.character, end.line, end.character) }))
|
||||
}
|
||||
|
||||
/** uri 里 position 处名字的所有定义;目标打不开成文本的(例如 Python 命名空间包指向的目录)略过。 */
|
||||
export async function findDefinitions(uri: Uri, position: Position, supports: (languageId: string) => boolean): Promise<Definition[]> {
|
||||
const found = await Promise.all((await provided(uri, position)).map(async (item): Promise<Definition[]> => {
|
||||
const location = 'targetUri' in item
|
||||
? { uri: item.targetUri, range: item.targetSelectionRange ?? item.targetRange }
|
||||
: { uri: item.uri, range: item.range }
|
||||
/** 查到的各处定义打开目标文件补上行文字、下标;目标打不开成文本的(例如 Python 命名空间包指向的目录)略过。 */
|
||||
export async function findDefinitions(locations: readonly FoundLocation[], supports: (languageId: string) => boolean): Promise<Definition[]> {
|
||||
const found = await Promise.all(locations.map(async (location): Promise<Definition[]> => {
|
||||
const { start } = location.range
|
||||
try {
|
||||
const document = await workspace.openTextDocument(location.uri)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
|
||||
import type { RuleLocation, RuleReference } from '../contract'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
import type { Definition } from './definitions'
|
||||
import type { Definition, FoundLocation } from './definitions'
|
||||
import type { ExtensionToWebview, Reveal, WebviewToExtension } from './messages'
|
||||
import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { webviewHtml, webviewUiRoot } from '../../webview/html'
|
||||
import { blockReplacement } from '../editText'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { findDefinitions, hasDefinition, pickDefinition } from './definitions'
|
||||
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { occurrencesOf } from './occurrences'
|
||||
|
||||
@@ -32,6 +34,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
private readonly registry: CodeReaderRegistry,
|
||||
private readonly runtime: TreeSitterRuntime,
|
||||
private readonly filter: BlockFilter,
|
||||
/** 库规则标的引用去哪儿查定义(交给标它的规则) */
|
||||
readonly resolveReference: (reference: RuleReference) => Promise<RuleLocation[]>,
|
||||
) {}
|
||||
|
||||
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
|
||||
@@ -90,6 +94,8 @@ class Preview {
|
||||
private latest = 0
|
||||
private timer: NodeJS.Timeout | undefined
|
||||
private readonly disposables: Disposable[]
|
||||
/** 名字的定义去哪儿查:库规则标了引用的问规则,其余问语言扩展 */
|
||||
private readonly definitions: DefinitionSources<FoundLocation>
|
||||
/** webview 报来的视口顶部那块的起始行 */
|
||||
visibleLine: number | undefined
|
||||
|
||||
@@ -106,6 +112,10 @@ class Preview {
|
||||
*/
|
||||
private pendingReveal: Reveal | undefined,
|
||||
) {
|
||||
this.definitions = new DefinitionSources({
|
||||
rule: async reference => ruleDefinitions(await owner.resolveReference(reference)),
|
||||
language: offset => languageDefinitions(document.uri, document.positionAt(offset)),
|
||||
})
|
||||
this.disposables = [
|
||||
panel.webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)),
|
||||
panel.onDidChangeViewState(() => {
|
||||
@@ -146,6 +156,7 @@ class Preview {
|
||||
})
|
||||
if (request !== this.latest || !this.ready)
|
||||
return
|
||||
this.definitions.remember(message.version, message.blocks)
|
||||
const reveal = this.pendingReveal
|
||||
this.pendingReveal = undefined
|
||||
this.post(reveal === undefined ? message : { ...message, reveal })
|
||||
@@ -221,7 +232,7 @@ class Preview {
|
||||
const { document } = this
|
||||
if (version !== document.version)
|
||||
return
|
||||
const found = await hasDefinition(document.uri, document.positionAt(offset))
|
||||
const found = (await this.definitions.locate(version, offset)).length > 0
|
||||
if (version === document.version && this.ready)
|
||||
this.post({ type: 'definitionProbed', id, found })
|
||||
}
|
||||
@@ -233,7 +244,7 @@ class Preview {
|
||||
return
|
||||
const current = document.uri.toString()
|
||||
const supports = (languageId: string): boolean => this.registry.languageFor(languageId) !== undefined
|
||||
let action = definitionAction(await findDefinitions(document.uri, document.positionAt(offset), supports), current)
|
||||
let action = definitionAction(await findDefinitions(await this.definitions.locate(version, offset), supports), current)
|
||||
if (action.kind === 'pick') {
|
||||
const picked = await pickDefinition(action.targets)
|
||||
action = definitionAction(picked ? [picked] : [], current)
|
||||
|
||||
Reference in new issue
Block a user