✨ feat(vscode): 文件和文件夹在预览顶部问
侧栏「问 AI」打开代码阅读预览。文件夹是没有代码块的一页,源码文件在整页顶部聊,代码块下面的问答不动。历史按工作区文件夹和相对路径记在窗口状态里,不写进按块存的记录。
This commit is contained in:
1 parent
2be635d1e5
commit
2c10acf9c4
19 files changed
+610
-459
No files matched your search
@@ -25,6 +25,7 @@ import { locate } from '../folderStore'
|
||||
import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols'
|
||||
import { DocumentNames, resolveNames } from '../nameResolution'
|
||||
import { askClaude, askFailure } from '../qa/claudeAsk'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { definitionAction } from './definitionAction'
|
||||
@@ -33,6 +34,7 @@ import { DefinitionSources } from './definitionSources'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
|
||||
import { occurrencesOf } from './occurrences'
|
||||
import { pathPreviewOf } from './pathPreview'
|
||||
import { findRepoSymbol, isImageFile, isRepoImagePath, repoRootOf, resolveRepoEntry } from './repoPath'
|
||||
|
||||
export const PREVIEW_VIEW_TYPE = 'superpowers.codeReader.preview'
|
||||
@@ -52,6 +54,8 @@ export interface DiffBases {
|
||||
*/
|
||||
export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
private readonly previews = new Set<Preview>()
|
||||
/** 侧栏「问 AI」打开的预览,解析时把光标放进顶部输入框;已经开着的由 openAsking 自己聚焦 */
|
||||
private readonly pathFocus = new Set<string>()
|
||||
/** 按文档 uri:等着交给马上要打开的新预览的位置(从文本编辑器切过来时它顶部那一行,或跳到的定义) */
|
||||
private readonly reveals = new Map<string, Reveal>()
|
||||
/** 哪个预览眼下能用键盘在高亮的各处出现之间跳,跟着它设快捷键 when 子句用的上下文键 */
|
||||
@@ -74,13 +78,32 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
readonly diffBases: DiffBases,
|
||||
) {}
|
||||
|
||||
/** 侧栏「问 AI」打开预览,并把光标放进顶部输入框。已经开着的就揭到前面再聚焦。 */
|
||||
async openAsking(uri: Uri): Promise<void> {
|
||||
const key = uri.toString()
|
||||
this.pathFocus.add(key)
|
||||
try {
|
||||
await commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, { preview: true })
|
||||
}
|
||||
finally {
|
||||
// 新开的预览在解析时已经拿走标记;还在的是本来就开着的那页,这里聚焦
|
||||
if (this.pathFocus.delete(key)) {
|
||||
for (const preview of this.previews) {
|
||||
if (preview.uri.toString() === key)
|
||||
preview.focusPathAsk()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
|
||||
// 问答带的图片(工作区文件夹的 .spx/code-qa/)也要能读,历史里才画得出缩略图
|
||||
const imageRoot = this.qa.imageRoot(document.uri)
|
||||
const imageRoot = this.qa.imageRoot(pathPreviewOf(document.uri)?.uri ?? document.uri)
|
||||
panel.webview.options = { enableScripts: true, localResourceRoots: [webviewUiRoot(this.extensionUri), ...(imageRoot ? [imageRoot] : [])] }
|
||||
panel.webview.html = webviewHtml(panel.webview, this.extensionUri, 'code-reader.html')
|
||||
const key = document.uri.toString()
|
||||
const preview = new Preview(document, panel, this, this.registry, this.runtime, this.filter, this.reveals.get(key))
|
||||
const focusPath = this.pathFocus.delete(key)
|
||||
const preview = new Preview(document, panel, this, this.registry, this.runtime, this.filter, this.reveals.get(key), focusPath)
|
||||
this.reveals.delete(key)
|
||||
this.previews.add(preview)
|
||||
panel.onDidDispose(() => {
|
||||
@@ -140,6 +163,8 @@ type RewriteOutcome
|
||||
class Preview {
|
||||
/** webview 发过 ready 之后才发消息;标签页切走时页面被销毁,切回来重新加载、再发 ready,这期间也不发 */
|
||||
private ready = false
|
||||
/** 侧栏「问 AI」打开的:页面准备好后把光标放进顶部输入框 */
|
||||
private pendingPathFocus = false
|
||||
/** 解析是异步的:只发最后一次请求的结果,慢的旧结果不能盖掉新的 */
|
||||
private latest = 0
|
||||
private timer: NodeJS.Timeout | undefined
|
||||
@@ -179,7 +204,9 @@ class Preview {
|
||||
* 之后页面因标签页切走重新加载,按 webview 自己记的位置恢复
|
||||
*/
|
||||
private pendingReveal: Reveal | undefined,
|
||||
focusPath: boolean,
|
||||
) {
|
||||
this.pendingPathFocus = focusPath
|
||||
this.definitions = new DefinitionSources({
|
||||
rule: async reference => ruleDefinitions(await owner.resolveReference(reference)),
|
||||
language: async (offset) => {
|
||||
@@ -227,7 +254,7 @@ class Preview {
|
||||
this.post({ type: 'filter', ...state })
|
||||
}),
|
||||
owner.qa.onDidChange((uris) => {
|
||||
if (uris.some(uri => uri.toString() === document.uri.toString()))
|
||||
if (uris.some(uri => uri.toString() === this.subject().uri.toString()))
|
||||
this.postQa()
|
||||
}),
|
||||
owner.qa.profiles.subscribe((choice) => {
|
||||
@@ -235,7 +262,7 @@ class Preview {
|
||||
this.post({ type: 'askProfiles', ...choice })
|
||||
}),
|
||||
owner.qa.onDidAnswer((event) => {
|
||||
if (event.owner !== document.uri.toString() || !this.ready)
|
||||
if (event.owner !== this.subject().uri.toString() || !this.ready)
|
||||
return
|
||||
if (event.kind === 'update') {
|
||||
const { askId, text, activity } = event.ask
|
||||
@@ -272,6 +299,23 @@ class Preview {
|
||||
if (!this.ready)
|
||||
return
|
||||
const request = ++this.latest
|
||||
if (pathPreviewOf(this.document.uri)) {
|
||||
this.sent = undefined
|
||||
const reveal = this.pendingReveal
|
||||
this.pendingReveal = undefined
|
||||
this.post({
|
||||
type: 'document',
|
||||
version: this.document.version,
|
||||
languageId: '',
|
||||
blocks: [],
|
||||
attributes: [],
|
||||
translators: [],
|
||||
translatorId: '',
|
||||
names: [],
|
||||
...(reveal && { reveal }),
|
||||
})
|
||||
return
|
||||
}
|
||||
const { languageId, version } = this.document
|
||||
const source = this.document.getText()
|
||||
const session = this.lookups.session()
|
||||
@@ -405,8 +449,25 @@ class Preview {
|
||||
}
|
||||
|
||||
private postQa(): void {
|
||||
if (this.ready)
|
||||
this.post(this.owner.qa.view(this.document.uri, file => this.panel.webview.asWebviewUri(file).toString()))
|
||||
if (!this.ready)
|
||||
return
|
||||
const subject = this.subject()
|
||||
this.post(this.owner.qa.view(subject.uri, file => this.panel.webview.asWebviewUri(file).toString(), subject.kind))
|
||||
}
|
||||
|
||||
/** 这一页问的是哪个文件或文件夹。路径页还原成真实路径,源码预览就是文档自己。 */
|
||||
private subject(): { uri: Uri, kind: 'file' | 'folder' } {
|
||||
return pathPreviewOf(this.document.uri) ?? { uri: this.document.uri, kind: 'file' }
|
||||
}
|
||||
|
||||
/** 把光标放进顶部输入框;页面还没准备好就等 ready。 */
|
||||
focusPathAsk(): void {
|
||||
if (!this.ready) {
|
||||
this.pendingPathFocus = true
|
||||
return
|
||||
}
|
||||
this.pendingPathFocus = false
|
||||
this.post({ type: 'focusPathAsk' })
|
||||
}
|
||||
|
||||
/** profile 列表每次现读(同看板),读完页面还在才发。 */
|
||||
@@ -421,13 +482,22 @@ class Preview {
|
||||
* 找不到这块(代码刚改过、还没重新发)或问不了时,直接回一条失败。
|
||||
*/
|
||||
private async ask(askId: string, anchor: string, question: string, images: ClaudeImage[]): Promise<void> {
|
||||
const at = locate(this.document.uri)
|
||||
const subject = at && this.sent && { path: at.key, languageId: this.document.languageId, blocks: this.sent.blocks, source: this.sent.source, anchor }
|
||||
const prompt = subject && askPrompt(subject, question)
|
||||
const followUp = subject && followUpPrompt(subject, question)
|
||||
const subject = this.subject()
|
||||
if (anchor === PATH_ASK_ANCHOR) {
|
||||
const refused = await this.owner.qa.askPath(subject.uri, subject.kind, { askId, question, images })
|
||||
if (refused !== undefined)
|
||||
this.post({ type: 'answerEnd', askId, status: 'failed', error: refused })
|
||||
else
|
||||
this.postQa()
|
||||
return
|
||||
}
|
||||
const at = locate(subject.uri)
|
||||
const block = at && this.sent && { path: at.key, languageId: this.document.languageId, blocks: this.sent.blocks, source: this.sent.source, anchor }
|
||||
const prompt = block && askPrompt(block, question)
|
||||
const followUp = block && followUpPrompt(block, question)
|
||||
const refused = prompt === undefined || followUp === undefined
|
||||
? '这块代码刚改过,找不到了。等预览刷新后重新点「问 AI」再问。'
|
||||
: await this.owner.qa.ask(this.document.uri, { askId, anchor, question, prompt, followUp, images })
|
||||
: await this.owner.qa.ask(subject.uri, { askId, anchor, question, prompt, followUp, images })
|
||||
if (refused !== undefined) {
|
||||
this.post({ type: 'answerEnd', askId, status: 'failed', error: refused })
|
||||
return
|
||||
@@ -449,6 +519,8 @@ class Preview {
|
||||
this.postQa()
|
||||
void this.postProfiles()
|
||||
await this.push()
|
||||
if (this.pendingPathFocus)
|
||||
this.focusPathAsk()
|
||||
return
|
||||
case 'ask':
|
||||
await this.ask(msg.askId, msg.anchor, msg.question, msg.images)
|
||||
@@ -460,7 +532,7 @@ class Preview {
|
||||
this.owner.qa.stop(msg.askId)
|
||||
return
|
||||
case 'deleteQa':
|
||||
await this.owner.qa.delete(this.document.uri, msg.anchor, msg.entryId)
|
||||
await this.owner.qa.delete(this.subject().uri, msg.anchor, msg.entryId)
|
||||
return
|
||||
case 'setFilter':
|
||||
await this.filter.set(msg.excluded)
|
||||
|
||||
Reference in new issue
Block a user