✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新

This commit is contained in:
cruldra committed 2026-09-27 22:08:16 +08:00
1 parent ebf228cdc7
commit ecc415d763
30 files changed
+1033 -157

No files matched your search

+20 -1
View File
@@ -1,8 +1,10 @@
import type { Location, LocationLink, Position } from 'vscode'
import type { RuleLocation } from '../contract'
import type { BlockKind, LanguageAdapter, RuleLocation } from '../contract'
import type { TreeSitterRuntime } from '../runtime'
import type { DefinitionTarget } from './definitionAction'
import { commands, Range, Uri, window, workspace } from 'vscode'
import { logger } from '../../logging/logger'
import { readDefinitionKind } from '../blockView'
/** 查到的一处定义:文件和定义处那个名字的范围。 */
export interface FoundLocation { uri: Uri, range: Range }
@@ -36,6 +38,23 @@ export function ruleDefinitions(locations: readonly RuleLocation[]): FoundLocati
return locations.map(({ uri, start, end }) => ({ uri: Uri.parse(uri), range: new Range(start.line, start.character, end.line, end.character) }))
}
/** 定义处那个名字定义的是哪种块:打开目标文件,用它那种语言的语言插件分块认;目标打不开、语言不支持、不在哪块的名字上时没有。 */
export async function definitionKind(
location: FoundLocation,
languageFor: (languageId: string) => LanguageAdapter | undefined,
runtime: Pick<TreeSitterRuntime, 'parse'>,
): Promise<BlockKind | undefined> {
try {
const document = await workspace.openTextDocument(location.uri)
const adapter = languageFor(document.languageId)
return adapter && await readDefinitionKind(runtime, adapter, document.languageId, document.getText(), document.offsetAt(location.range.start))
}
catch (err) {
logger.add({ level: 'warn', source: 'codeReader', message: `认不出 ${location.uri.toString()} 里定义的是哪种块:${err instanceof Error ? err.message : String(err)}` })
return undefined
}
}
/** 查到的各处定义打开目标文件补上行文字、下标;目标打不开成文本的(例如 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[]> => {
@@ -118,6 +118,21 @@ describe('预览的 document 消息', () => {
expect(reports).toEqual(['库规则 fastapi 给筛选属性起名时出错,筛选栏照写 id:词表坏了'])
})
it('调用的名字定义成哪种块交给 calleeKind 问:跨几行新建对象的一行译文下面带实参表', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n name=user.name,\n )\n'
const asked: string[] = []
const { message } = await documentMessage(doc('python', source), registryWithPython(), runtime, ignore, undefined, (call) => {
asked.push(call.callee.text)
return 'class'
})
expect(asked).toEqual(['Detail'])
expect(message.blocks[0].children[0]).toMatchObject({
kind: 'return',
translation: [{ text: '返回' }, { text: ' ' }, { text: '新建' }, { text: ' ' }, { text: 'Detail', role: 'type' }, { text: ' ' }, { text: '· 2 个参数', role: 'hint' }],
table: { columns: ['参数', '值'], items: [{ cells: [[{ text: 'id' }], expect.anything()] }, { cells: [[{ text: 'name' }], expect.anything()] }] },
})
})
it('库规则抛错:不算解析失败,交给 report 记下,这块照常显示', async () => {
const broken: LibraryRule = {
id: 'broken',
@@ -1,4 +1,4 @@
import type { DisplayName, RuleContext } from '../contract'
import type { DisplayName, RuleContext, SourceDocument } from '../contract'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
import type { DocumentMessage } from './messages'
@@ -21,7 +21,8 @@ export interface DocumentRead {
/**
* 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下;
* 规则查名字的定义时问 lookup(RuleContext.lookup)。筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
* 规则查名字的定义时问 lookup(RuleContext.lookup),翻译插件问调用的名字定义成哪种块时问 calleeKind(SourceDocument.calleeKind)。
* 筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
*/
export async function documentMessage(
document: DocumentSnapshot,
@@ -29,6 +30,7 @@ export async function documentMessage(
runtime: Pick<TreeSitterRuntime, 'parse'>,
report: (message: string) => void,
lookup?: RuleContext['lookup'],
calleeKind?: SourceDocument['calleeKind'],
): Promise<DocumentRead> {
const { languageId, version } = document
const source = document.getText()
@@ -48,7 +50,7 @@ export async function documentMessage(
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [] }
try {
const rules = registry.rulesFor(languageId)
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup })
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup }, calleeKind)
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames }
}
+24 -3
View File
@@ -1,7 +1,7 @@
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { BlockView } from '../blockView'
import type { RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
import type { BlockKind, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
import type { DefinitionStart, IndexedName, NameSources } from '../nameResolution'
import type { CodeQa } from '../qa/codeQa'
import type { CodeReaderRegistry } from '../registry'
@@ -14,13 +14,14 @@ 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 { DocumentCallees } from '../calleeKinds'
import { blockReplacement } from '../editText'
import { locate } from '../folderStore'
import { DocumentNames, resolveNames } from '../nameResolution'
import { askPrompt, followUpPrompt } from '../qa/prompt'
import { DocumentLookups } from '../ruleLookups'
import { definitionAction } from './definitionAction'
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
import { DefinitionSources } from './definitionSources'
import { documentMessage } from './documentMessage'
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
@@ -131,6 +132,8 @@ class Preview {
private readonly names: DocumentNames
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
private readonly lookups: DocumentLookups
/** 调用的名字定义成哪种块(分新建还是调用):查到的变了就重发文档 */
private readonly callees: DocumentCallees
constructor(
private readonly document: TextDocument,
@@ -154,6 +157,7 @@ class Preview {
this.post({ type: 'displayNames', version, names })
})
this.lookups = new DocumentLookups(() => this.schedulePush())
this.callees = new DocumentCallees(() => this.schedulePush())
this.disposables = [
panel.webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)),
panel.onDidChangeViewState(() => {
@@ -223,9 +227,10 @@ class Preview {
const { languageId, version } = this.document
const source = this.document.getText()
const session = this.lookups.session()
const callees = this.callees.session()
const { message, displayNames } = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
}, session.lookup)
}, session.lookup, callees.calleeKind)
if (request !== this.latest || !this.ready)
return
this.definitions.remember(message.version, message.blocks)
@@ -237,9 +242,24 @@ class Preview {
if (version === this.document.version) {
this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(), report))
this.lookups.resolve(version, session.requests(), this.lookupSources())
this.callees.resolve(version, callees.requests(), offset => this.calleeKind(offset))
}
}
/** 这份文档里 offset 处名字的定义是哪种块:语言扩展查到的几处定义里第一处认得出的;语言扩展没答是 undefined。 */
private async calleeKind(offset: number): Promise<BlockKind | null | undefined> {
const { document } = this
const found = await languageDefinitions(document.uri, document.positionAt(offset))
if (found.length === 0)
return undefined
for (const location of found) {
const kind = await definitionKind(location, languageId => this.registry.languageFor(languageId), this.runtime)
if (kind)
return kind
}
return null
}
/** 这份文档里 offset 处名字的定义(语言扩展查的)落在哪儿。 */
private async definitionStarts(offset: number): Promise<DefinitionStart[]> {
const { document } = this
@@ -284,6 +304,7 @@ class Preview {
clearTimeout(this.timer)
this.names.dispose()
this.lookups.dispose()
this.callees.dispose()
for (const disposable of this.disposables)
disposable.dispose()
}