✨ feat(vscode): 注释里的仓库路径链接能点开并跳到符号

This commit is contained in:
cruldra committed 2026-10-03 03:22:20 +08:00
1 parent 5528a405cc
commit c4113748e1
13 files changed
+662 -18

No files matched your search

+109 -12
View File
@@ -1,4 +1,4 @@
import type { CustomTextEditorProvider, Disposable, Event, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
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 { DiffBase } from '../diff/diffBase'
@@ -13,7 +13,7 @@ import type { BlockFilter } from './blockFilter'
import type { Definition, FoundLocation } from './definitions'
import type { ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
import { dirname } from 'node:path'
import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
import { commands, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode'
import { logger } from '../../logging/logger'
import { webviewHtml, webviewUiRoot } from '../../webview/html'
import { DocumentCallees } from '../calleeKinds'
@@ -32,6 +32,7 @@ import { DefinitionSources } from './definitionSources'
import { documentMessage } from './documentMessage'
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
import { occurrencesOf } from './occurrences'
import { findRepoSymbol, repoRootOf, resolveRepoEntry } from './repoPath'
export const PREVIEW_VIEW_TYPE = 'superpowers.codeReader.preview'
@@ -491,6 +492,12 @@ class Preview {
case 'findOccurrences':
await this.findOccurrences(msg.id, msg.version, msg.offset)
return
case 'openRepoLink':
await this.openRepoLink(msg.path, msg.symbol)
return
case 'checkRepoLinks':
await this.checkRepoLinks(msg.id, msg.paths)
return
case 'occurrenceNavigation':
this.owner.navigation.update(this, msg.available)
}
@@ -561,6 +568,84 @@ 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> {
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)
if (this.ready)
this.post({ type: 'repoLinksChecked', id, missing })
}
/**
* 打开注释里的仓库链接:目录在资源管理器里定位,文件用编辑器打开。
* 带了符号就按文档符号的名字跳过去;文件或符号没有就不打开。
*/
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)
try {
if (target.isDirectory) {
if (symbol !== undefined) {
this.markRepoLinkBroken(rel, symbol)
return
}
await commands.executeCommand('revealInExplorer', uri)
this.markRepoLinkOpened(rel)
return
}
if (symbol === undefined) {
await this.showRepoFile(uri)
this.markRepoLinkOpened(rel)
return
}
const found = await this.locateRepoSymbol(uri, symbol)
if (!found) {
this.markRepoLinkBroken(rel, symbol)
return
}
await this.showRepoFile(uri, found.selectionRange)
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}`)
}
}
/** 用文档符号按名字找。先把文件读进来(不显示),没有这个名字就没有,不打开到文件顶上。 */
private async locateRepoSymbol(uri: Uri, symbol: string): Promise<DocumentSymbol | undefined> {
const doc = await workspace.openTextDocument(uri)
const symbols = await commands.executeCommand<DocumentSymbol[] | undefined>('vscode.executeDocumentSymbolProvider', doc.uri)
return Array.isArray(symbols) ? findRepoSymbol(symbols, symbol) : undefined
}
/** 在当前这个预览所在的编辑器组打开文件,并把预览标签固定住,免得被它替掉。 */
private async showRepoFile(uri: Uri, selection?: Range): Promise<void> {
if (this.panel.active && !workspace.getConfiguration('workbench.editor').get<boolean>('enablePreviewFromCodeNavigation', false))
await commands.executeCommand('workbench.action.keepEditor')
await window.showTextDocument(uri, { viewColumn: this.panel.viewColumn, ...(selection && { selection }) })
}
private markRepoLinkBroken(path: string, symbol?: string): void {
if (this.ready)
this.post({ type: 'repoLinkBroken', path, ...(symbol !== undefined && { symbol }) })
}
private markRepoLinkOpened(path: string, symbol?: string): void {
if (this.ready)
this.post({ type: 'repoLinkOpened', path, ...(symbol !== undefined && { symbol }) })
}
/** 用写注释的技能重写这一段,只把正文套回原来的定界符,再走块编辑那条写回。 */
private async rewriteComment(anchor: string): Promise<void> {
if (this.rewriting.has(anchor) || this.rewriteAbort.signal.aborted)
@@ -592,27 +677,39 @@ class Preview {
return { status: 'failed', error: message }
}
const found = this.sent && findPlaced(this.sent.blocks, anchor)
if (!found || found.block.kind !== 'comment')
if (!found)
return failed('这块注释刚改过,找不到了。等预览刷新后再重写。')
const { block, siblings, index } = found
const startLine = block.commentStartLine
const endLine = block.commentEndLine
if (startLine === undefined || endLine === undefined)
return failed('这块没有能重写的注释。')
const { document } = this
if (block.startLine < 0 || block.endLine < block.startLine || block.endLine >= document.lineCount)
if (startLine < 0 || endLine < startLine || endLine >= document.lineCount)
return failed('这块注释的位置已经变了,没有写回。')
const raw = Array.from({ length: block.endLine - block.startLine + 1 }, (_, i) => document.lineAt(block.startLine + i).text)
const linesOf = (from: number, to: number): string => Array.from({ length: to - from + 1 }, (_, i) => document.lineAt(from + i).text).join('\n')
const raw = linesOf(startLine, endLine).split('\n')
const snapshot = raw.join('\n')
const comment = commentSource(raw)
const source = document.getText()
const attached = startLine !== block.startLine || endLine !== block.endLine
? [
...(startLine > block.startLine ? [linesOf(block.startLine, startLine - 1)] : []),
...(endLine < block.endLine ? [linesOf(endLine + 1, block.endLine)] : []),
].filter(part => part !== '').join('\n')
: undefined
const next = siblings[index + 1]
const following = next && next.startLine >= 0 && next.endLine >= next.startLine && next.endLine < document.lineCount
? Array.from({ length: next.endLine - next.startLine + 1 }, (_, i) => document.lineAt(next.startLine + i).text).join('\n')
const following = attached === undefined && next && next.startLine >= 0 && next.endLine >= next.startLine && next.endLine < document.lineCount
? linesOf(next.startLine, next.endLine)
: undefined
const prompt = commentRewritePrompt({
comment,
source,
startLine: block.startLine,
endLine: block.endLine,
startLine,
endLine,
earlierKinds: siblings.slice(0, index).map(item => item.kind),
...(following !== undefined && { following }),
...(attached !== undefined && attached !== '' && { attached }),
})
if (!prompt)
return failed('这段注释的定界符认不出来,没有改文件。')
@@ -666,9 +763,9 @@ class Preview {
const body = modelCommentBody(answerText)
if (!body)
return failed('模型没有给出注释正文,没有改文件。')
if (block.endLine >= document.lineCount)
if (endLine >= document.lineCount)
return failed('这段注释在重写期间变了,没有写回。')
const current = Array.from({ length: block.endLine - block.startLine + 1 }, (_, i) => document.lineAt(block.startLine + i).text).join('\n')
const current = linesOf(startLine, endLine)
if (current !== snapshot)
return failed('这段注释在重写期间变了,没有写回。')
const filled = refillComment(comment, body)
@@ -676,7 +773,7 @@ class Preview {
return failed('这段注释的定界符认不出来,没有改文件。')
if (signal.aborted)
return { status: 'stopped' }
const wrote = await this.applyEdit(block.startLine, block.endLine, filled)
const wrote = await this.applyEdit(startLine, endLine, filled)
if (wrote === 'written' || wrote === 'unchanged')
return { status: 'done' }
if (wrote === 'rejected')