✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下
This commit is contained in:
1 parent
badc722640
commit
fb53bb727c
34 files changed
+2726
-43
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { RuleLocation, RuleReference } from '../contract'
|
||||
import type { CodeQa } from '../qa/codeQa'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
@@ -9,6 +11,8 @@ import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { webviewHtml, webviewUiRoot } from '../../webview/html'
|
||||
import { blockReplacement } from '../editText'
|
||||
import { locate } from '../folderStore'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
@@ -36,6 +40,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
private readonly filter: BlockFilter,
|
||||
/** 库规则标的引用去哪儿查定义(交给标它的规则) */
|
||||
readonly resolveReference: (reference: RuleReference) => Promise<RuleLocation[]>,
|
||||
/** 问 AI:各预览共用,同一份文件开在几处时问答、正在答的问题都一样 */
|
||||
readonly qa: CodeQa,
|
||||
) {}
|
||||
|
||||
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
|
||||
@@ -98,6 +104,8 @@ class Preview {
|
||||
private readonly definitions: DefinitionSources<FoundLocation>
|
||||
/** webview 报来的视口顶部那块的起始行 */
|
||||
visibleLine: number | undefined
|
||||
/** 最近一次发出去的块树和它的源码:问 AI 时按块的记号在里面找那块代码 */
|
||||
private sent: { blocks: BlockView[], source: string } | undefined
|
||||
|
||||
constructor(
|
||||
private readonly document: TextDocument,
|
||||
@@ -131,6 +139,28 @@ class Preview {
|
||||
if (this.ready)
|
||||
this.post({ type: 'filter', excluded })
|
||||
}),
|
||||
owner.qa.onDidChange((uris) => {
|
||||
if (uris.some(uri => uri.toString() === document.uri.toString()))
|
||||
this.postQa()
|
||||
}),
|
||||
owner.qa.onDidAnswer((event) => {
|
||||
if (event.owner !== document.uri.toString() || !this.ready)
|
||||
return
|
||||
if (event.kind === 'update') {
|
||||
const { askId, text, activity } = event.ask
|
||||
this.post({ type: 'answer', askId, text, ...(activity !== undefined && { activity }) })
|
||||
return
|
||||
}
|
||||
const { end, entryId } = event
|
||||
this.post({
|
||||
type: 'answerEnd',
|
||||
askId: event.ask.askId,
|
||||
status: end.status,
|
||||
...(end.status === 'failed' && { error: end.error }),
|
||||
...(end.status === 'done' && { text: end.answer.text, entryId, sessionId: end.answer.sessionId }),
|
||||
})
|
||||
this.postQa()
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -151,12 +181,15 @@ class Preview {
|
||||
if (!this.ready)
|
||||
return
|
||||
const request = ++this.latest
|
||||
const message: ExtensionToWebview = await documentMessage(this.document, this.registry, this.runtime, (text) => {
|
||||
const { languageId, version } = this.document
|
||||
const source = this.document.getText()
|
||||
const message = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
|
||||
})
|
||||
if (request !== this.latest || !this.ready)
|
||||
return
|
||||
this.definitions.remember(message.version, message.blocks)
|
||||
this.sent = { blocks: message.blocks, source }
|
||||
const reveal = this.pendingReveal
|
||||
this.pendingReveal = undefined
|
||||
this.post(reveal === undefined ? message : { ...message, reveal })
|
||||
@@ -180,6 +213,30 @@ class Preview {
|
||||
void this.panel.webview.postMessage(message)
|
||||
}
|
||||
|
||||
private postQa(): void {
|
||||
if (this.ready)
|
||||
this.post(this.owner.qa.view(this.document.uri))
|
||||
}
|
||||
|
||||
/**
|
||||
* 问 anchor 那块代码:按最近一次发出去的块树找到它、带上代码上下文交给 CodeQa;resume 是追问时上一次的会话 id。
|
||||
* 找不到这块(代码刚改过、还没重新发)或问不了时,直接回一条失败。
|
||||
*/
|
||||
private ask(askId: string, anchor: string, question: string, resume: string | undefined): void {
|
||||
const at = locate(this.document.uri)
|
||||
const subject = at && this.sent && { path: at.key, languageId: this.document.languageId, ...this.sent, anchor }
|
||||
const prompt = subject && askPrompt(subject, question)
|
||||
const followUp = subject && resume ? followUpPrompt(subject, question) : undefined
|
||||
const refused = prompt === undefined
|
||||
? '这块代码刚改过,找不到了。等预览刷新后重新点「问 AI」再问。'
|
||||
: this.owner.qa.ask(this.document.uri, { askId, anchor, question, prompt, ...(resume && followUp && { followUp: { sessionId: resume, prompt: followUp } }) })
|
||||
if (refused !== undefined) {
|
||||
this.post({ type: 'answerEnd', askId, status: 'failed', error: refused })
|
||||
return
|
||||
}
|
||||
this.postQa()
|
||||
}
|
||||
|
||||
private schedulePush(): void {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => void this.push(), REFRESH_DELAY_MS)
|
||||
@@ -191,8 +248,18 @@ class Preview {
|
||||
this.ready = true
|
||||
// 先于文档发:第一份文档按筛选画,不会先整份露出来再藏
|
||||
this.post({ type: 'filter', excluded: this.filter.excluded })
|
||||
this.postQa()
|
||||
await this.push()
|
||||
return
|
||||
case 'ask':
|
||||
this.ask(msg.askId, msg.anchor, msg.question, msg.resume)
|
||||
return
|
||||
case 'stopAsk':
|
||||
this.owner.qa.stop(msg.askId)
|
||||
return
|
||||
case 'deleteQa':
|
||||
await this.owner.qa.delete(this.document.uri, msg.anchor, msg.entryId)
|
||||
return
|
||||
case 'setFilter':
|
||||
await this.filter.set(msg.excluded)
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user