✨ 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:
1 parent
335cf9ca9f
commit
582ac4c987
49 files changed
+1730
-757
No files matched your search
@@ -1,6 +1,14 @@
|
||||
import type { BlockView, CommentTarget } from './blockView'
|
||||
import type { CommentEdit, CommentRewriteDeps, CommentRewriteJob, WriteResult } from './commentRewrite'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { COMMENT_SKILL, commentProse, commentRewritePrompt, commentSource, isModuleComment, modelCommentBody, refillComment } from './commentRewrite'
|
||||
import { commonIndent, replaceLines } from './editText'
|
||||
import { toBlockViews } from './blockView'
|
||||
import { COMMENT_SKILL, commentEdit, commentRewritePrompt, commentSource, createCommentParser, findBlock, refillComment, rewriteBlockComment } from './commentRewrite'
|
||||
import { commonIndent, replaceLines, splitLines } from './editText'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
import { WASM_DIR, withBlocks } from './testSupport'
|
||||
import { pythonZhTranslator } from './translators/pythonZh'
|
||||
import { typescriptZhTranslator } from './translators/typescriptZh'
|
||||
|
||||
/** 和预览写回同一条路:正文套回壳,再按原来的缩进替换这几行。 */
|
||||
function rewrite(file: string, start: number, end: number, body: string): string {
|
||||
@@ -103,66 +111,193 @@ describe('重写注释后套回原来的壳', () => {
|
||||
expect(refillComment('const x = 1', '新')).toBeUndefined()
|
||||
expect(refillComment('# 旧', ' ')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('给模型的正文不带定界符', () => {
|
||||
expect(commentProse('"""旧说明"""')).toBe('旧说明')
|
||||
expect(commentProse('# 旧说明\n# 第二行')).toBe('旧说明\n第二行')
|
||||
expect(commentProse('/**\n * 旧说明\n */')).toBe('旧说明')
|
||||
expect(commentProse('// 旧说明')).toBe('旧说明')
|
||||
})
|
||||
})
|
||||
|
||||
const line = (value: unknown): string => JSON.stringify(value)
|
||||
/** 模型调 StructuredOutput 交结果:工具输入逐段来,再是整条消息 */
|
||||
function structuredLines(body: string): string[] {
|
||||
const json = JSON.stringify({ body })
|
||||
return [
|
||||
line({ type: 'stream_event', event: { type: 'content_block_start', content_block: { type: 'tool_use', name: 'StructuredOutput', input: {} } } }),
|
||||
line({ type: 'stream_event', event: { type: 'content_block_delta', delta: { type: 'input_json_delta', partial_json: json.slice(0, 12) } } }),
|
||||
line({ type: 'stream_event', event: { type: 'content_block_delta', delta: { type: 'input_json_delta', partial_json: json.slice(12) } } }),
|
||||
line({ type: 'stream_event', event: { type: 'content_block_stop' } }),
|
||||
line({ type: 'assistant', message: { content: [{ type: 'tool_use', name: 'StructuredOutput', input: { body } }] } }),
|
||||
]
|
||||
}
|
||||
const resultLine = (fields: Record<string, unknown>): string => line({ type: 'result', subtype: 'success', is_error: false, ...fields })
|
||||
|
||||
describe('重写注释的提示', () => {
|
||||
it('文件顶上的三引号:上下文是去掉这段之后的文件,太长会截掉并说明', () => {
|
||||
const rest = Array.from({ length: 130 }, (_, index) => `line ${index}`).join('\n')
|
||||
const prompt = commentRewritePrompt({
|
||||
comment: '"""旧"""',
|
||||
source: `"""旧"""\n${rest}`,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
earlierKinds: [],
|
||||
it('只有技能命令和代码块在哪:路径:起-止,行号 1 起', () => {
|
||||
expect(commentRewritePrompt('src/acquisition/repository.py', 12, 20)).toBe(`${COMMENT_SKILL} src/acquisition/repository.py:13-21`)
|
||||
expect(commentRewritePrompt('a.ts', 0, 0)).toBe('/ecc:writing-code-comments a.ts:1-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('读模型交回的结构化结果', () => {
|
||||
it('正文跟着 StructuredOutput 的输入逐段长出来,读文件是正在做的事;结果以 structured_output 为准', () => {
|
||||
const parse = createCommentParser()
|
||||
const events = [
|
||||
line({ type: 'assistant', message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/w/src/a.py' } }] } }),
|
||||
...structuredLines('把 `a` 和 `b` 相加。'),
|
||||
resultLine({ result: '{"body":"把 `a` 和 `b` 相加。"}', structured_output: { body: '把 `a` 和 `b` 相加。' } }),
|
||||
].flatMap(parse)
|
||||
expect(events[0]).toEqual({ kind: 'activity', label: '读取 a.py' })
|
||||
expect(events.filter(event => event.kind === 'text').map(event => 'text' in event && event.text)).toEqual(['把 `', '把 `a` 和 `b` 相加。'])
|
||||
expect(events.at(-1)).toEqual({ kind: 'result', body: '把 `a` 和 `b` 相加。' })
|
||||
})
|
||||
|
||||
it('模型只回了一段话、structured_output 是空的:结果里没有正文;Claude Code 报错时是失败', () => {
|
||||
expect(createCommentParser()(resultLine({ result: '我改好了:把 a 和 b 相加。' }))).toEqual([{ kind: 'result' }])
|
||||
expect(createCommentParser()(resultLine({ result: '', structured_output: { body: ' ' } }))).toEqual([{ kind: 'result' }])
|
||||
expect(createCommentParser()(line({ type: 'result', is_error: true, result: '额度用完了' }))).toEqual([{ kind: 'error', message: '额度用完了' }])
|
||||
})
|
||||
})
|
||||
|
||||
/** 内存里的一份文件:按行读,写回时同预览一样换掉几行(补回缩进)或在某行上面插几行。 */
|
||||
function memoryFile(text: string): { deps: (ask: CommentRewriteDeps['ask']) => CommentRewriteDeps, text: () => string, writes: CommentEdit[] } {
|
||||
let current = text
|
||||
const writes: CommentEdit[] = []
|
||||
const lines = (): string[] => splitLines(current)
|
||||
return {
|
||||
text: () => current,
|
||||
writes,
|
||||
deps: ask => ({
|
||||
lineAt: index => lines()[index],
|
||||
lineCount: () => lines().length,
|
||||
ask,
|
||||
failure: err => `没答成:${err instanceof Error ? err.message : String(err)}`,
|
||||
write: async (edit): Promise<WriteResult> => {
|
||||
writes.push(edit)
|
||||
if (edit.kind === 'replace') {
|
||||
current = replaceLines(current, edit.startLine, edit.endLine, edit.text)
|
||||
}
|
||||
else {
|
||||
const all = lines()
|
||||
current = [...all.slice(0, edit.line), ...edit.text.split('\n'), ...all.slice(edit.line)].join('\n')
|
||||
}
|
||||
return 'written'
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/** 用 Python 语言插件、中文直译把源码读成视图数据,按块的第一行(1 起,含讲它的注释)找到那块。 */
|
||||
async function pythonBlock(source: string, line: number): Promise<BlockView> {
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator))
|
||||
const all = (list: BlockView[]): BlockView[] => list.flatMap(view => [view, ...all(view.children)])
|
||||
const found = all(views).find(view => view.startLine + 1 === line)
|
||||
if (!found)
|
||||
throw new Error(`第 ${line} 行没有块`)
|
||||
return found
|
||||
}
|
||||
|
||||
function job(block: BlockView, path = 'src/a.py', lineComment = '#'): CommentRewriteJob {
|
||||
if (!block.commentTarget)
|
||||
throw new Error('这块不能写注释')
|
||||
return { target: block.commentTarget, startLine: block.startLine, endLine: block.endLine, path, lineComment }
|
||||
}
|
||||
|
||||
const SOURCE = [
|
||||
'import os',
|
||||
'',
|
||||
'# 旧的说明',
|
||||
'',
|
||||
'# 第二段',
|
||||
'class Repo:',
|
||||
' def find(self):',
|
||||
' return os.sep',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
describe('重写一块代码的注释', () => {
|
||||
it('块前面的注释(几段隔着空行的也算)换成模型交回的正文,套回 # 和缩进;提示只有技能命令和块的范围', async () => {
|
||||
const repo = await pythonBlock(SOURCE, 3)
|
||||
expect(repo.commentTarget).toEqual({ kind: 'replace', startLine: 2, endLine: 4 })
|
||||
const file = memoryFile(SOURCE)
|
||||
const prompts: string[] = []
|
||||
const outcome = await rewriteBlockComment(job(repo), file.deps(async (prompt) => {
|
||||
prompts.push(prompt)
|
||||
return '仓库:按名字找文件。'
|
||||
}))
|
||||
expect(outcome).toEqual({ status: 'done' })
|
||||
expect(prompts).toEqual(['/ecc:writing-code-comments src/a.py:3-8'])
|
||||
expect(file.text()).toBe(SOURCE.replace('# 旧的说明\n\n# 第二段', '# 仓库:按名字找文件。'))
|
||||
})
|
||||
|
||||
it('还没有注释的块:用行注释记号新写一段,插在块上面,缩进同块的第一行;说明(docstring)不动', async () => {
|
||||
const source = 'class Repo:\n def find(self):\n """找。"""\n return 1\n'
|
||||
const find = await pythonBlock(source, 2)
|
||||
expect(find.commentTarget).toEqual({ kind: 'insert', line: 1 })
|
||||
const file = memoryFile(source)
|
||||
expect(await rewriteBlockComment(job(find), file.deps(async () => '按名字找\n\n找不到时返回 1'))).toEqual({ status: 'done' })
|
||||
expect(file.text()).toBe('class Repo:\n # 按名字找\n #\n # 找不到时返回 1\n def find(self):\n """找。"""\n return 1\n')
|
||||
})
|
||||
|
||||
it('文件开头的说明卡片换的是它自己:范围就是这段注释', async () => {
|
||||
const source = '"""旧的模块说明。"""\n\nimport os\n'
|
||||
const header = await pythonBlock(source, 1)
|
||||
expect(header).toMatchObject({ kind: 'comment', commentTarget: { kind: 'replace', startLine: 0, endLine: 0 } })
|
||||
const file = memoryFile(source)
|
||||
const prompts: string[] = []
|
||||
await rewriteBlockComment(job(header), file.deps(async (prompt) => {
|
||||
prompts.push(prompt)
|
||||
return '新的模块说明。'
|
||||
}))
|
||||
expect(prompts).toEqual(['/ecc:writing-code-comments src/a.py:1-1'])
|
||||
expect(file.text()).toBe('"""新的模块说明。"""\n\nimport os\n')
|
||||
})
|
||||
|
||||
it('没交回结构化的正文、模型出错:失败,文件不动', async () => {
|
||||
const repo = await pythonBlock(SOURCE, 3)
|
||||
const silent = memoryFile(SOURCE)
|
||||
expect(await rewriteBlockComment(job(repo), silent.deps(async () => undefined))).toEqual({ status: 'failed', error: '模型没有按要求交回注释正文,没有改文件。' })
|
||||
expect(silent.writes).toEqual([])
|
||||
const broken = memoryFile(SOURCE)
|
||||
expect(await rewriteBlockComment(job(repo), broken.deps(async () => {
|
||||
throw new Error('额度用完了')
|
||||
}))).toEqual({ status: 'failed', error: '没答成:额度用完了' })
|
||||
expect(broken.writes).toEqual([])
|
||||
})
|
||||
|
||||
it('重写期间这块代码变了:失败,不写;被叫停:stopped,不写', async () => {
|
||||
const repo = await pythonBlock(SOURCE, 3)
|
||||
const file = memoryFile(SOURCE)
|
||||
let edited = SOURCE
|
||||
const deps = file.deps(async () => {
|
||||
edited = SOURCE.replace('return os.sep', 'return "/"')
|
||||
return '新说明'
|
||||
})
|
||||
expect(prompt?.startsWith(`${COMMENT_SKILL}\n`)).toBe(true)
|
||||
expect(prompt).toContain('只按这个技能重写这一段注释')
|
||||
expect(prompt).toContain('这段注释现在的正文:\n旧')
|
||||
expect(prompt).not.toContain('"""')
|
||||
expect(prompt).toContain('截断了')
|
||||
expect(prompt).toContain('line 0')
|
||||
expect(prompt).not.toContain('line 129')
|
||||
})
|
||||
|
||||
it('普通注释只带后面紧挨着的那一块,不带整份文件', () => {
|
||||
const prompt = commentRewritePrompt({
|
||||
comment: '# 旧',
|
||||
source: '整份文件不该出现',
|
||||
startLine: 4,
|
||||
endLine: 4,
|
||||
earlierKinds: ['function'],
|
||||
following: 'def stay():\n return 1',
|
||||
})
|
||||
expect(prompt).toContain('def stay():')
|
||||
expect(prompt).not.toContain('整份文件不该出现')
|
||||
expect(isModuleComment('# 旧', [])).toBe(false)
|
||||
expect(isModuleComment('"""旧"""', [])).toBe(true)
|
||||
expect(isModuleComment('"""旧"""', ['function'])).toBe(false)
|
||||
})
|
||||
|
||||
it('函数上的说明用这块代码当上下文,不当成文件顶上的说明', () => {
|
||||
const prompt = commentRewritePrompt({
|
||||
comment: '"""旧说明"""',
|
||||
source: 'def run():\n """旧说明"""\n return 1\n\ndef other():\n return 2',
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
earlierKinds: [],
|
||||
attached: 'def run():\n return 1',
|
||||
})
|
||||
expect(prompt).toContain('def run():')
|
||||
expect(prompt).toContain('return 1')
|
||||
expect(prompt).not.toContain('def other()')
|
||||
const changing: CommentRewriteDeps = { ...deps, lineAt: index => splitLines(edited)[index] }
|
||||
expect(await rewriteBlockComment(job(repo), changing)).toEqual({ status: 'failed', error: '这块代码在重写期间变了,没有写回。' })
|
||||
expect(file.writes).toEqual([])
|
||||
|
||||
const stop = new AbortController()
|
||||
const stopped = memoryFile(SOURCE)
|
||||
const result = await rewriteBlockComment(job(repo), { ...stopped.deps(async () => {
|
||||
stop.abort()
|
||||
return '新说明'
|
||||
}), signal: stop.signal })
|
||||
expect(result).toEqual({ status: 'stopped' })
|
||||
expect(stopped.writes).toEqual([])
|
||||
})
|
||||
|
||||
it('用 // 写 TypeScript 的块:JSDoc 是说明,不当成要换掉的注释', async () => {
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const source = '/** 读配置。 */\nexport function load() {}\n'
|
||||
const [view] = await withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator))
|
||||
expect(view.commentTarget).toEqual({ kind: 'insert', line: 0 })
|
||||
const edit = commentEdit(view.commentTarget as CommentTarget, index => splitLines(source)[index], '启动时读一次', ts.lineComment)
|
||||
expect(edit).toEqual({ kind: 'insert', line: 0, text: '// 启动时读一次' })
|
||||
})
|
||||
})
|
||||
|
||||
it('模型套了代码围栏时只留里面的正文', () => {
|
||||
expect(modelCommentBody('```\n新说明\n```')).toBe('新说明')
|
||||
expect(modelCommentBody(' ')).toBeUndefined()
|
||||
describe('按记号找块', () => {
|
||||
it('先序找,找不到是 undefined', () => {
|
||||
const tree = [{ anchor: 'a', children: [{ anchor: 'b', children: [] }] }, { anchor: 'c', children: [] }]
|
||||
expect(findBlock(tree, 'b')?.anchor).toBe('b')
|
||||
expect(findBlock(tree, 'x')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user