✨ feat(vscode): 代码阅读注释里不带路径的引用 [Name] 能点,本文件定义的优先、再找导入的
This commit is contained in:
1 parent
0958506681
commit
300d8a07b9
16 files changed
+600
-124
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import type { DisplayName, LocalSymbol, RuleContext, SourceDocument } from '../contract'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { ImportedName } from '../symbolLinks'
|
||||
import type { DocumentMessage } from './messages'
|
||||
import { fileAttributes, mergeAttributeNames } from '../attributes'
|
||||
import { readBlockViews } from '../blockView'
|
||||
@@ -19,11 +20,15 @@ export interface DocumentSnapshot {
|
||||
entryNames?: readonly string[]
|
||||
}
|
||||
|
||||
/** 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)。 */
|
||||
/**
|
||||
* 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)、
|
||||
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)。
|
||||
*/
|
||||
export interface DocumentRead {
|
||||
message: DocumentMessage
|
||||
displayNames: DisplayName[]
|
||||
symbols: LocalSymbol[]
|
||||
imports: ImportedName[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,10 +60,10 @@ export async function documentMessage(
|
||||
}
|
||||
const adapter = registry.languageFor(languageId)
|
||||
if (!adapter)
|
||||
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [] }
|
||||
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [] }
|
||||
try {
|
||||
const rules = registry.rulesFor(languageId)
|
||||
const { blocks, displayNames, symbols } = await readBlockViews(runtime, adapter, translator, languageId, source, {
|
||||
const { blocks, displayNames, symbols, imports } = await readBlockViews(runtime, adapter, translator, languageId, source, {
|
||||
...(document.fileName && { fileName: document.fileName }),
|
||||
rules: { rules, report, lookup },
|
||||
calleeKind,
|
||||
@@ -73,9 +78,9 @@ export async function documentMessage(
|
||||
},
|
||||
})
|
||||
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
|
||||
return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols }
|
||||
return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols, imports }
|
||||
}
|
||||
catch (err) {
|
||||
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [] }
|
||||
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [], imports: [] }
|
||||
}
|
||||
}
|
||||
@@ -101,9 +101,9 @@ export interface QaMessage {
|
||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
|
||||
* diff:这份文件相比对比基准改了什么(侧栏开着「未提交的更改」或审查工单时才有,见 DiffBase):每发一份文档之后算好了补发,
|
||||
* 换了筛选、基准挪了(提交后 HEAD 动了)再发;null 是不比了(回到全部文件)
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了
|
||||
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
|
||||
*/
|
||||
export type ExtensionToWebview
|
||||
@@ -121,9 +121,9 @@ export type ExtensionToWebview
|
||||
| { type: 'rewriteProgress', anchor: string, question: string, text: string, activity?: string }
|
||||
| { type: 'rewriteEnd', anchor: string, status: 'done' | 'failed' | 'stopped', error?: string }
|
||||
| { type: 'diff', diff: DocumentDiff | null }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[] }
|
||||
| { type: 'repoLinkBroken', path: string, symbol?: string }
|
||||
| { type: 'repoLinkOpened', path: string, symbol?: string }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: string[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string }
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string }
|
||||
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
|
||||
| { type: 'focusPathAsk' }
|
||||
|
||||
@@ -145,8 +145,9 @@ export type ExtensionToWebview
|
||||
* selectAskProfile:「问 AI」换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后发 askProfiles 给所有预览
|
||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发;
|
||||
* 扩展端按它设 when 子句用的上下文键
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录
|
||||
* checkRepoLinks:预览里这批仓库路径,问哪些文件或目录没有。id 标出是哪一次问
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按这份文件找 symbol(本文件定义的,再是导入的,见 symbolLinks.ts)
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(symbols)问哪些找不到。id 标出是哪一次问
|
||||
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
|
||||
*/
|
||||
export type WebviewToExtension
|
||||
@@ -166,6 +167,6 @@ export type WebviewToExtension
|
||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||
| { type: 'selectAskProfile', id: string }
|
||||
| { type: 'occurrenceNavigation', available: boolean }
|
||||
| { type: 'openRepoLink', path: string, symbol?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[] }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||
| { type: 'checkRepoImages', id: number, paths: string[] }
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CustomTextEditorProvider, Disposable, DocumentSymbol, Event, TextDocument, TextDocumentShowOptions, WebviewPanel } from 'vscode'
|
||||
import type { ClaudeImage } from '../../cc/spawnClaude'
|
||||
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
|
||||
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, SourceRange, WorkspaceIndex } from '../contract'
|
||||
import type { DiffBase } from '../diff/diffBase'
|
||||
import type { OldDocument, SentDocument } from '../diff/previewDiff'
|
||||
import type { WorkspaceEntries } from '../entries/workspaceEntries'
|
||||
@@ -9,6 +9,7 @@ import type { CodeQa } from '../qa/codeQa'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { LookupSources } from '../ruleLookups'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { IndexedEntry } from '../workspaceIndex'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
import type { Definition, FoundLocation } from './definitions'
|
||||
@@ -29,6 +30,7 @@ import { askClaude, askFailure } from '../qa/claudeAsk'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { resolveReference } from '../symbolLinks'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
@@ -188,6 +190,8 @@ class Preview {
|
||||
private sent: SentDocument | undefined
|
||||
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
|
||||
private symbols: { version: number, list: LocalSymbol[] } | undefined
|
||||
/** 最近一次发出去的文档里各条导入引进来的名字:注释里不带路径的引用按它找导入的定义 */
|
||||
private imports: ImportedName[] = []
|
||||
/** 这份文档各版本显示成中文名的名字 */
|
||||
private readonly names: DocumentNames
|
||||
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
|
||||
@@ -330,7 +334,7 @@ class Preview {
|
||||
const source = this.document.getText()
|
||||
const session = this.lookups.session()
|
||||
const callees = this.callees.session()
|
||||
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
|
||||
const { message, displayNames, symbols, imports } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
|
||||
}, session.lookup, callees.calleeKind)
|
||||
if (request !== this.latest || !this.ready)
|
||||
@@ -339,6 +343,7 @@ class Preview {
|
||||
// 解析失败的文档没有块:问 AI 找不到块,也不拿它比差异
|
||||
this.sent = message.error === undefined ? { version: message.version, blocks: message.blocks, source } : undefined
|
||||
this.symbols = { version, list: symbols }
|
||||
this.imports = imports
|
||||
const reveal = this.pendingReveal
|
||||
this.pendingReveal = undefined
|
||||
this.post({ ...message, names: this.names.known(version), ...(reveal && { reveal }) })
|
||||
@@ -579,7 +584,7 @@ class Preview {
|
||||
await this.openRepoLink(msg.path, msg.symbol)
|
||||
return
|
||||
case 'checkRepoLinks':
|
||||
await this.checkRepoLinks(msg.id, msg.paths)
|
||||
await this.checkRepoLinks(msg.id, msg.paths, msg.symbols)
|
||||
return
|
||||
case 'checkRepoImages':
|
||||
await this.checkRepoImages(msg.id, msg.paths)
|
||||
@@ -654,16 +659,25 @@ class Preview {
|
||||
await this.owner.openAt(uri, reveal, () => commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, options))
|
||||
}
|
||||
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在。 */
|
||||
private async checkRepoLinks(id: number, paths: string[]): Promise<void> {
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,哪些找不到。 */
|
||||
private async checkRepoLinks(id: number, paths: string[], symbols: string[]): Promise<void> {
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const missing = (await Promise.all(paths.map(async (rel) => {
|
||||
if (root === undefined || await resolveRepoEntry(root, rel) === undefined)
|
||||
return rel
|
||||
return undefined
|
||||
}))).filter(rel => rel !== undefined)
|
||||
const asked = new Map<string, Promise<SymbolNode[]>>()
|
||||
const symbolsOf = (uri: string): Promise<SymbolNode[]> => {
|
||||
const known = asked.get(uri) ?? this.symbolNodes(uri)
|
||||
asked.set(uri, known)
|
||||
return known
|
||||
}
|
||||
const [missing, unresolved] = await Promise.all([
|
||||
Promise.all(paths.map(async (rel) => {
|
||||
if (root === undefined || await resolveRepoEntry(root, rel) === undefined)
|
||||
return rel
|
||||
return undefined
|
||||
})),
|
||||
Promise.all(symbols.map(async symbol => await this.resolveSymbolLink(symbol, symbolsOf) ? undefined : symbol)),
|
||||
])
|
||||
if (this.ready)
|
||||
this.post({ type: 'repoLinksChecked', id, missing })
|
||||
this.post({ type: 'repoLinksChecked', id, missing: missing.filter(rel => rel !== undefined), unresolved: unresolved.filter(symbol => symbol !== undefined) })
|
||||
}
|
||||
|
||||
/** 仓库根放进这个预览能读的本地目录,图片地址才能加载。真实位置和 git 给的路径不一样时两个都放。 */
|
||||
@@ -707,18 +721,29 @@ class Preview {
|
||||
|
||||
/**
|
||||
* 打开注释里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
|
||||
* 带了符号就按文档符号的名字跳过去;文件或符号没有就不打开。
|
||||
* 带了符号就按文档符号的名字跳过去;文件或符号没有就不打开。没有路径的是不带路径的引用,按这份文件找(见 resolveSymbolLink)。
|
||||
*/
|
||||
private async openRepoLink(rel: string, symbol?: string): Promise<void> {
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const target = root === undefined ? undefined : await resolveRepoEntry(root, rel)
|
||||
if (!target) {
|
||||
this.markRepoLinkBroken(rel, symbol)
|
||||
return
|
||||
}
|
||||
const uri = Uri.file(target.fsPath)
|
||||
private async openRepoLink(rel: string | undefined, symbol?: string): Promise<void> {
|
||||
const link = rel ?? `[${symbol}]`
|
||||
try {
|
||||
if (target.isDirectory) {
|
||||
if (rel === undefined) {
|
||||
const target = symbol === undefined ? undefined : await this.resolveSymbolLink(symbol)
|
||||
if (!target) {
|
||||
this.markRepoLinkBroken(undefined, symbol)
|
||||
return
|
||||
}
|
||||
await this.showLinkTarget(target)
|
||||
this.markRepoLinkOpened(undefined, symbol)
|
||||
return
|
||||
}
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const entry = root === undefined ? undefined : await resolveRepoEntry(root, rel)
|
||||
if (!entry) {
|
||||
this.markRepoLinkBroken(rel, symbol)
|
||||
return
|
||||
}
|
||||
const uri = Uri.file(entry.fsPath)
|
||||
if (entry.isDirectory) {
|
||||
if (symbol !== undefined) {
|
||||
this.markRepoLinkBroken(rel, symbol)
|
||||
return
|
||||
@@ -738,21 +763,69 @@ class Preview {
|
||||
this.markRepoLinkBroken(rel, symbol)
|
||||
return
|
||||
}
|
||||
await this.showRepoFile(uri, found.selectionRange)
|
||||
await this.showLinkTarget(found)
|
||||
this.markRepoLinkOpened(rel, symbol)
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能打开仓库链接 ${rel}:${message}` })
|
||||
void window.showErrorMessage(`代码阅读:没能打开 ${rel}`)
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能打开仓库链接 ${link}:${message}` })
|
||||
void window.showErrorMessage(`代码阅读:没能打开 ${link}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 用文档符号按名字找。先把文件读进来(不显示),没有这个名字就没有,不打开到文件顶上。 */
|
||||
private async locateRepoSymbol(uri: Uri, symbol: string): Promise<DocumentSymbol | undefined> {
|
||||
const doc = await workspace.openTextDocument(uri)
|
||||
/** 用文档符号按名字找(`Agent`、`Agent.owner_id`);没有这个名字就没有,不打开到文件顶上。 */
|
||||
private async locateRepoSymbol(uri: Uri, symbol: string): Promise<LinkTarget | undefined> {
|
||||
const found = findRepoSymbol(await this.symbolNodes(uri.toString()), symbol)
|
||||
return found && { uri: uri.toString(), selection: found.selection }
|
||||
}
|
||||
|
||||
/** 一个文件的文档符号(vscode.executeDocumentSymbolProvider):先把文件读进来(不显示),范围换成那个文件里的下标。 */
|
||||
private async symbolNodes(uri: string): Promise<SymbolNode[]> {
|
||||
const doc = await workspace.openTextDocument(Uri.parse(uri))
|
||||
const symbols = await commands.executeCommand<DocumentSymbol[] | undefined>('vscode.executeDocumentSymbolProvider', doc.uri)
|
||||
return Array.isArray(symbols) ? findRepoSymbol(symbols, symbol) : undefined
|
||||
const span = (range: Range): SourceRange => ({ startIndex: doc.offsetAt(range.start), endIndex: doc.offsetAt(range.end) })
|
||||
const node = (symbol: DocumentSymbol): SymbolNode => ({
|
||||
name: symbol.name,
|
||||
range: span(symbol.range),
|
||||
selection: span(symbol.selectionRange),
|
||||
children: (symbol.children ?? []).map(node),
|
||||
})
|
||||
return Array.isArray(symbols) ? symbols.map(node) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:本文件定义的优先,再是导入进来的(见 symbolLinks.ts)。
|
||||
* 文档符号同仓库链接找符号那一套(symbols,一批引用一起问时共用一份),导入的名字问语言扩展它的定义(同跳到定义),定义几处取第一处。
|
||||
*/
|
||||
private async resolveSymbolLink(symbol: string, symbols: (uri: string) => Promise<SymbolNode[]> = uri => this.symbolNodes(uri)): Promise<LinkTarget | undefined> {
|
||||
const { document } = this
|
||||
try {
|
||||
return await resolveReference(symbol, this.imports, {
|
||||
uri: document.uri.toString(),
|
||||
symbols,
|
||||
definition: async (offset) => {
|
||||
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (!found)
|
||||
return undefined
|
||||
const doc = await workspace.openTextDocument(found.uri)
|
||||
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `没能找注释里的引用 [${symbol}]:${err instanceof Error ? err.message : String(err)}` })
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳到链接指的符号:在这份文件里就在本预览里滚过去、闪一下;别的文件在预览所在的编辑器组打开、选中符号的名字。 */
|
||||
private async showLinkTarget({ uri, selection }: LinkTarget): Promise<void> {
|
||||
if (uri === this.document.uri.toString()) {
|
||||
this.reveal({ line: this.document.positionAt(selection.startIndex).line, definition: selection.startIndex })
|
||||
return
|
||||
}
|
||||
const doc = await workspace.openTextDocument(Uri.parse(uri))
|
||||
await this.showRepoFile(doc.uri, new Range(doc.positionAt(selection.startIndex), doc.positionAt(selection.endIndex)))
|
||||
}
|
||||
|
||||
/** 在预览所在的编辑器组打开文件,并把预览标签固定住,免得被它替掉。图片用默认打开方式,才看得到图。 */
|
||||
@@ -766,14 +839,14 @@ class Preview {
|
||||
await window.showTextDocument(uri, { viewColumn: this.panel.viewColumn, ...(selection && { selection }) })
|
||||
}
|
||||
|
||||
private markRepoLinkBroken(path: string, symbol?: string): void {
|
||||
private markRepoLinkBroken(path: string | undefined, symbol?: string): void {
|
||||
if (this.ready)
|
||||
this.post({ type: 'repoLinkBroken', path, ...(symbol !== undefined && { symbol }) })
|
||||
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }) })
|
||||
}
|
||||
|
||||
private markRepoLinkOpened(path: string, symbol?: string): void {
|
||||
private markRepoLinkOpened(path: string | undefined, symbol?: string): void {
|
||||
if (this.ready)
|
||||
this.post({ type: 'repoLinkOpened', path, ...(symbol !== undefined && { symbol }) })
|
||||
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }) })
|
||||
}
|
||||
|
||||
/** 用写注释的技能重写这一段,只把正文套回原来的定界符,再走块编辑那条写回。 */
|
||||
|
||||
Reference in new issue
Block a user