✨ feat(vscode): 代码阅读的注释挂到它讲的代码块上成「注释」一行,重写注释改用技能一句话加结构化结果

- 普通注释不再单独成卡片:同一层挂到后面最近的块(隔空行也算),后面没有挂到前面那块,整层没代码挂到父块;只有文件开头隔着空行的注释、模块 docstring、//!、@file 单独成「文件开头的说明」
- 各语言(Python、TypeScript、Rust、Shell、Dockerfile)建好树后统一调 attachComments;块的编辑范围连着挂上的注释,差异、跳转、钉住卡片都按它
- 宿主统一给每张卡片(语言卡片、翻译插件的属性表、库规则卡片、一行译文的语句卡片)插「注释」一行,排在名称、说明后面
- TypeScript 的 JSDoc 也能当常量、变量的说明;sqlx、React 不再单独处理前面的注释
- 「重写注释」每块都有:提示只发 /ecc:writing-code-comments 路径:起-止,模型只读、按 --json-schema 交 body,没交结构化正文就失败不写;扩展换掉块前面的注释或在块上面新写一段
This commit is contained in:
cruldra committed 2026-10-08 07:51:21 +08:00
1 parent 335cf9ca9f
commit 582ac4c987
49 files changed
+1730 -757

No files matched your search

+74 -109
View File
@@ -1,5 +1,6 @@
import type { CustomTextEditorProvider, Disposable, DocumentSymbol, Event, TextDocument, TextDocumentShowOptions, WebviewPanel } from 'vscode'
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { CommentEdit, RewriteOutcome, WriteResult } from '../commentRewrite'
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, SourceRange, WorkspaceIndex } from '../contract'
import type { DiffBase } from '../diff/diffBase'
import type { OldDocument, SentDocument } from '../diff/previewDiff'
@@ -16,18 +17,18 @@ import type { FoundLocation } from './definitions'
import type { DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
import type { TargetSpot } from './openTarget'
import { realpath } from 'node:fs/promises'
import { dirname } from 'node:path'
import { commands, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode'
import { dirname, relative, sep } from 'node:path'
import { commands, EndOfLine, Position, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode'
import { logger } from '../../logging/logger'
import { webviewHtml, webviewUiRoot } from '../../webview/html'
import { DocumentCallees } from '../calleeKinds'
import { commentRewritePrompt, commentSource, findPlaced, modelCommentBody, refillComment } from '../commentRewrite'
import { findBlock, rewriteBlockComment } from '../commentRewrite'
import { PreviewDiff } from '../diff/previewDiff'
import { blockReplacement } from '../editText'
import { locate } from '../folderStore'
import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols'
import { DocumentNames, resolveNames } from '../nameResolution'
import { askClaude, askFailure } from '../qa/claudeAsk'
import { askCommentBody, askFailure } from '../qa/claudeAsk'
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
import { askPrompt, followUpPrompt } from '../qa/prompt'
import { DocumentLookups } from '../ruleLookups'
@@ -164,12 +165,6 @@ 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
@@ -962,113 +957,83 @@ class Preview {
this.failRewrite(message)
return { status: 'failed', error: message }
}
const found = this.sent && findPlaced(this.sent.blocks, anchor)
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 block = this.sent && findBlock(this.sent.blocks, anchor)
if (!block)
return failed('这块代码刚改过,找不到了。等预览刷新后再重写。')
const target = block.commentTarget
if (!target)
return failed('这块不能写注释。')
const { document } = this
if (startLine < 0 || endLine < startLine || endLine >= document.lineCount)
return failed('这块注释的位置已经变了,没有写回。')
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 = 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,
endLine,
earlierKinds: siblings.slice(0, index).map(item => item.kind),
...(following !== undefined && { following }),
...(attached !== undefined && attached !== '' && { attached }),
})
if (!prompt)
return failed('这段注释的定界符认不出来,没有改文件。')
if (document.isDirty)
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 cwd = at?.folder.uri.fsPath ?? dirname(document.uri.fsPath)
const profile = await this.owner.qa.profiles.current()
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,
onEvent: (event) => {
if (event.kind === 'text') {
text = event.text
activity = undefined
}
else if (event.kind === 'activity') {
activity = event.label
}
else {
return
}
publish()
},
})
answerText = answer.text
let question = ''
let text = ''
let activity: string | undefined
const publish = (): void => {
this.post({ type: 'rewriteProgress', anchor, question, text, ...(activity !== undefined && { activity }) })
}
catch (err) {
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 (endLine >= document.lineCount)
return failed('这段注释在重写期间变了,没有写回。')
const current = linesOf(startLine, endLine)
if (current !== snapshot)
return failed('这段注释在重写期间变了,没有写回。')
const filled = refillComment(comment, body)
if (!filled)
return failed('这段注释的定界符认不出来,没有改文件。')
if (signal.aborted)
return { status: 'stopped' }
const wrote = await this.applyEdit(startLine, endLine, filled)
if (wrote === 'written' || wrote === 'unchanged')
return { status: 'done' }
if (wrote === 'rejected')
return { status: 'failed', error: `没能把修改写回 ${workspace.asRelativePath(document.uri)}` }
return failed('这段注释在重写期间变了,没有写回。')
const outcome = await rewriteBlockComment({
target,
startLine: block.startLine,
endLine: block.endLine,
path: relative(cwd, document.uri.fsPath).split(sep).join('/'),
lineComment: this.registry.languageFor(this.languageId)?.lineComment,
}, {
lineAt: line => document.lineAt(line).text,
lineCount: () => document.lineCount,
ask: (prompt) => {
question = prompt
publish()
return askCommentBody({
prompt,
...(profile && { profilePath: profile.path }),
cwd,
signal,
onEvent: (event) => {
if (event.kind === 'text') {
text = event.text
activity = undefined
}
else {
activity = event.label
}
publish()
},
})
},
failure: askFailure,
write: edit => this.applyCommentEdit(edit),
signal,
})
if (outcome.status === 'failed')
this.failRewrite(outcome.error)
return outcome
}
/** 把这几行换成 [text]。`written` 写进去了,`unchanged` 跟文件里一样不用写,`missing` 行号已经不在,`rejected` 编辑器没接受。 */
private async applyEdit(startLine: number, endLine: number, text: string): Promise<'written' | 'unchanged' | 'missing' | 'rejected'> {
/** 写回重写好的注释:换掉原来那几行(同块编辑),或在块上面插进新写的几行。 */
private async applyCommentEdit(edit: CommentEdit): Promise<WriteResult> {
if (edit.kind === 'replace')
return this.applyEdit(edit.startLine, edit.endLine, edit.text)
const { document } = this
if (edit.line >= document.lineCount)
return 'missing'
const eol = document.eol === EndOfLine.CRLF ? '\r\n' : '\n'
const change = new WorkspaceEdit()
change.insert(document.uri, new Position(edit.line, 0), edit.text.split('\n').join(eol) + eol)
if (!await workspace.applyEdit(change)) {
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
return 'rejected'
}
return 'written'
}
/** 把这几行换成 [text](见 WriteResult)。 */
private async applyEdit(startLine: number, endLine: number, text: string): Promise<WriteResult> {
const { document } = this
const replacement = blockReplacement(line => document.lineAt(line).text, document.lineCount, startLine, endLine, text)
if (!replacement) {