✨ feat(vscode): 重写注释时在卡片下展示模型过程
This commit is contained in:
1 parent
08fb04e3fd
commit
5e8e7b65ee
15 files changed
+369
-83
No files matched your search
@@ -89,6 +89,8 @@ export interface QaMessage {
|
||||
* askProfiles:「问 AI」输入框旁边的 profile 下拉:选项和选中的那项;ready 后发一次,任何一个预览改了都发给所有预览
|
||||
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
||||
* answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停
|
||||
* rewriteProgress:anchor 这段注释正在按规范重写。question 是发给模型的要求,text 是到现在为止的全文,activity 是正在读什么
|
||||
* rewriteEnd:这段重写结束了。done 是正文已经写进文件;failed 带给人看的原因,文件没改;stopped 是被叫停,文件没改
|
||||
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现,由这两条命令发给活动的预览
|
||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
|
||||
* diff:这份文件相比对比基准改了什么(侧栏开着「未提交的更改」或审查工单时才有,见 DiffBase):每发一份文档之后算好了补发,
|
||||
@@ -106,7 +108,8 @@ export type ExtensionToWebview
|
||||
| ({ type: 'askProfiles' } & ProfileChoice)
|
||||
| { type: 'answer', askId: string, text: string, activity?: string }
|
||||
| { type: 'answerEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string }
|
||||
| { type: 'commentRewritten', anchor: string }
|
||||
| { 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 }
|
||||
|
||||
/**
|
||||
@@ -120,6 +123,8 @@ export type ExtensionToWebview
|
||||
* setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览
|
||||
* ask:问 anchor 那块代码;askId 由 webview 起,回答的消息带回去;扩展端接着这块最近一次问答的会话问,用眼下选的 profile;
|
||||
* images 是贴进输入框的图片(没贴是空的),随问题发给 Claude Code,答成了跟这条一起存
|
||||
* rewriteComment:按注释规范重写 anchor 这段注释。过程用 rewriteProgress / rewriteEnd 发回页面,不写入问答
|
||||
* stopRewrite:叫停 anchor 这段正在进行的重写,不写文件
|
||||
* stopAsk:叫停 askId 那个问题,这次不存
|
||||
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
|
||||
* selectAskProfile:「问 AI」换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后发 askProfiles 给所有预览
|
||||
@@ -132,6 +137,7 @@ export type WebviewToExtension
|
||||
| { type: 'setOnlyChanges', on: boolean }
|
||||
| { type: 'edit', startLine: number, endLine: number, text: string }
|
||||
| { type: 'rewriteComment', anchor: string }
|
||||
| { type: 'stopRewrite', anchor: string }
|
||||
| { type: 'selectTranslator', id: string }
|
||||
| { type: 'visibleLine', line: number }
|
||||
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
||||
|
||||
@@ -129,6 +129,12 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/** 一段注释重写的结果:`done` 已经写进文件,`failed` 没写并带给人看的原因,`stopped` 是被叫停、没写。 */
|
||||
type RewriteOutcome
|
||||
= | { status: 'done' }
|
||||
| { status: 'stopped' }
|
||||
| { status: 'failed', error: string }
|
||||
|
||||
class Preview {
|
||||
/** webview 发过 ready 之后才发消息;标签页切走时页面被销毁,切回来重新加载、再发 ready,这期间也不发 */
|
||||
private ready = false
|
||||
@@ -144,6 +150,8 @@ class Preview {
|
||||
private readonly rewriting = new Set<string>()
|
||||
/** 预览关掉时停掉还在跑的重写 */
|
||||
private readonly rewriteAbort = new AbortController()
|
||||
/** 每段正在重写的注释各自的停止开关;预览关掉时 [rewriteAbort] 会把它们都停掉 */
|
||||
private readonly rewriteStops = new Map<string, AbortController>()
|
||||
/** 最近一次发出去的文档(版本、块树、源码):问 AI 时按块的记号在里面找那块代码,对比基准变了时拿它重新比 */
|
||||
private sent: SentDocument | undefined
|
||||
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
|
||||
@@ -464,6 +472,9 @@ class Preview {
|
||||
case 'rewriteComment':
|
||||
await this.rewriteComment(msg.anchor)
|
||||
return
|
||||
case 'stopRewrite':
|
||||
this.rewriteStops.get(msg.anchor)?.abort()
|
||||
return
|
||||
case 'selectTranslator':
|
||||
await this.registry.selectTranslator(this.languageId, msg.id)
|
||||
this.owner.refresh(this.languageId)
|
||||
@@ -555,12 +566,19 @@ class Preview {
|
||||
if (this.rewriting.has(anchor) || this.rewriteAbort.signal.aborted)
|
||||
return
|
||||
this.rewriting.add(anchor)
|
||||
const stop = new AbortController()
|
||||
const onDispose = (): void => stop.abort()
|
||||
this.rewriteAbort.signal.addEventListener('abort', onDispose)
|
||||
this.rewriteStops.set(anchor, stop)
|
||||
try {
|
||||
await this.rewriteCommentNow(anchor)
|
||||
const outcome = await this.rewriteCommentNow(anchor, stop.signal)
|
||||
if (!this.rewriteAbort.signal.aborted)
|
||||
this.post({ type: 'rewriteEnd', anchor, status: outcome.status, ...(outcome.status === 'failed' && { error: outcome.error }) })
|
||||
}
|
||||
finally {
|
||||
this.rewriteAbort.signal.removeEventListener('abort', onDispose)
|
||||
this.rewriteStops.delete(anchor)
|
||||
this.rewriting.delete(anchor)
|
||||
this.post({ type: 'commentRewritten', anchor })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,18 +586,18 @@ class Preview {
|
||||
void window.showErrorMessage(`代码阅读:${message}`)
|
||||
}
|
||||
|
||||
private async rewriteCommentNow(anchor: string): Promise<void> {
|
||||
const found = this.sent && findPlaced(this.sent.blocks, anchor)
|
||||
if (!found || found.block.kind !== 'comment') {
|
||||
this.failRewrite('这块注释刚改过,找不到了。等预览刷新后再重写。')
|
||||
return
|
||||
private async rewriteCommentNow(anchor: string, signal: AbortSignal): Promise<RewriteOutcome> {
|
||||
const failed = (message: string): RewriteOutcome => {
|
||||
this.failRewrite(message)
|
||||
return { status: 'failed', error: message }
|
||||
}
|
||||
const found = this.sent && findPlaced(this.sent.blocks, anchor)
|
||||
if (!found || found.block.kind !== 'comment')
|
||||
return failed('这块注释刚改过,找不到了。等预览刷新后再重写。')
|
||||
const { block, siblings, index } = found
|
||||
const { document } = this
|
||||
if (block.startLine < 0 || block.endLine < block.startLine || block.endLine >= document.lineCount) {
|
||||
this.failRewrite('这块注释的位置已经变了,没有写回。')
|
||||
return
|
||||
}
|
||||
if (block.startLine < 0 || block.endLine < block.startLine || block.endLine >= document.lineCount)
|
||||
return failed('这块注释的位置已经变了,没有写回。')
|
||||
const raw = Array.from({ length: block.endLine - block.startLine + 1 }, (_, i) => document.lineAt(block.startLine + i).text)
|
||||
const snapshot = raw.join('\n')
|
||||
const comment = commentSource(raw)
|
||||
@@ -596,65 +614,92 @@ class Preview {
|
||||
earlierKinds: siblings.slice(0, index).map(item => item.kind),
|
||||
...(following !== undefined && { following }),
|
||||
})
|
||||
if (!prompt) {
|
||||
this.failRewrite('这段注释的定界符认不出来,没有改文件。')
|
||||
return
|
||||
}
|
||||
if (!prompt)
|
||||
return failed('这段注释的定界符认不出来,没有改文件。')
|
||||
const at = locate(document.uri)
|
||||
let text = ''
|
||||
let activity: string | undefined
|
||||
const publish = (): void => {
|
||||
this.post({
|
||||
type: 'rewriteProgress',
|
||||
anchor,
|
||||
question: prompt,
|
||||
text,
|
||||
...(activity !== undefined && { activity }),
|
||||
})
|
||||
}
|
||||
publish()
|
||||
const profile = await this.owner.qa.profiles.current()
|
||||
let text: string
|
||||
if (signal.aborted)
|
||||
return { status: 'stopped' }
|
||||
let answerText: string
|
||||
try {
|
||||
const answer = await askClaude({
|
||||
prompt,
|
||||
images: [],
|
||||
...(profile && { profilePath: profile.path }),
|
||||
cwd: at?.folder.uri.fsPath ?? dirname(document.uri.fsPath),
|
||||
signal: this.rewriteAbort.signal,
|
||||
onEvent: () => {},
|
||||
signal,
|
||||
onEvent: (event) => {
|
||||
if (event.kind === 'text') {
|
||||
text = event.text
|
||||
activity = undefined
|
||||
}
|
||||
else if (event.kind === 'activity') {
|
||||
activity = event.label
|
||||
}
|
||||
else {
|
||||
return
|
||||
}
|
||||
publish()
|
||||
},
|
||||
})
|
||||
text = answer.text
|
||||
answerText = answer.text
|
||||
}
|
||||
catch (err) {
|
||||
if (!this.rewriteAbort.signal.aborted)
|
||||
this.failRewrite(askFailure(err))
|
||||
return
|
||||
}
|
||||
if (this.rewriteAbort.signal.aborted)
|
||||
return
|
||||
const body = modelCommentBody(text)
|
||||
if (!body) {
|
||||
this.failRewrite('模型没有给出注释正文,没有改文件。')
|
||||
return
|
||||
}
|
||||
if (block.endLine >= document.lineCount) {
|
||||
this.failRewrite('这段注释在重写期间变了,没有写回。')
|
||||
return
|
||||
if (signal.aborted)
|
||||
return { status: 'stopped' }
|
||||
return failed(askFailure(err))
|
||||
}
|
||||
if (signal.aborted)
|
||||
return { status: 'stopped' }
|
||||
const body = modelCommentBody(answerText)
|
||||
if (!body)
|
||||
return failed('模型没有给出注释正文,没有改文件。')
|
||||
if (block.endLine >= document.lineCount)
|
||||
return failed('这段注释在重写期间变了,没有写回。')
|
||||
const current = Array.from({ length: block.endLine - block.startLine + 1 }, (_, i) => document.lineAt(block.startLine + i).text).join('\n')
|
||||
if (current !== snapshot) {
|
||||
this.failRewrite('这段注释在重写期间变了,没有写回。')
|
||||
return
|
||||
}
|
||||
if (current !== snapshot)
|
||||
return failed('这段注释在重写期间变了,没有写回。')
|
||||
const filled = refillComment(comment, body)
|
||||
if (!filled) {
|
||||
this.failRewrite('这段注释的定界符认不出来,没有改文件。')
|
||||
return
|
||||
}
|
||||
await this.applyEdit(block.startLine, block.endLine, filled)
|
||||
if (!filled)
|
||||
return failed('这段注释的定界符认不出来,没有改文件。')
|
||||
if (signal.aborted)
|
||||
return { status: 'stopped' }
|
||||
const wrote = await this.applyEdit(block.startLine, block.endLine, filled)
|
||||
if (wrote === 'written' || wrote === 'unchanged')
|
||||
return { status: 'done' }
|
||||
if (wrote === 'rejected')
|
||||
return { status: 'failed', error: `没能把修改写回 ${workspace.asRelativePath(document.uri)}` }
|
||||
return failed('这段注释在重写期间变了,没有写回。')
|
||||
}
|
||||
|
||||
private async applyEdit(startLine: number, endLine: number, text: string): Promise<void> {
|
||||
/** 把这几行换成 [text]。`written` 写进去了,`unchanged` 跟文件里一样不用写,`missing` 行号已经不在,`rejected` 编辑器没接受。 */
|
||||
private async applyEdit(startLine: number, endLine: number, text: string): Promise<'written' | 'unchanged' | 'missing' | 'rejected'> {
|
||||
const { document } = this
|
||||
const replacement = blockReplacement(line => document.lineAt(line).text, document.lineCount, startLine, endLine, text)
|
||||
if (!replacement) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `块内修改的行号 ${startLine}-${endLine} 已不在 ${document.uri.fsPath} 里,没有写回` })
|
||||
return
|
||||
return 'missing'
|
||||
}
|
||||
if (replacement.unchanged)
|
||||
return
|
||||
return 'unchanged'
|
||||
const edit = new WorkspaceEdit()
|
||||
edit.replace(document.uri, new Range(replacement.startLine, 0, replacement.endLine, replacement.endCharacter), replacement.text)
|
||||
if (!await workspace.applyEdit(edit))
|
||||
if (!await workspace.applyEdit(edit)) {
|
||||
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
|
||||
return 'rejected'
|
||||
}
|
||||
return 'written'
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user