✨ feat(vscode): 注释卡片可按规范重写注释

This commit is contained in:
cruldra committed 2026-10-03 03:22:20 +08:00
1 parent 724c81188c
commit 299acb979f
11 files changed
+717 -5

No files matched your search

+17 -1
View File
@@ -37,6 +37,9 @@ export function App(): ReactElement | null {
// 先于下面发 ready 的那个 effect 挂上收消息:ready 后扩展端马上发问答
const qa = useQa()
const [doc, setDoc] = useState<DocumentMessage | null>(null)
/** 正在按注释规范重写的块(记号)。点下去立刻记上,扩展端写完或失败再发 commentRewritten */
const rewritingRef = useRef(new Set<string>())
const [rewriting, setRewriting] = useState<ReadonlySet<string>>(NONE)
const [view, setView] = useState<BlockModes>(savedBlockModes)
const [focusId, setFocusId] = useState<string | null>(null)
/** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */
@@ -114,6 +117,10 @@ export function App(): ReactElement | null {
return
case 'diff':
setDiff(msg.diff)
return
case 'commentRewritten':
rewritingRef.current.delete(msg.anchor)
setRewriting(new Set(rewritingRef.current))
}
})
postMessage({ type: 'ready' })
@@ -126,6 +133,13 @@ export function App(): ReactElement | null {
}, [view])
const onFocused = useCallback(() => setFocusId(null), [])
const onRewriteComment = useCallback((anchor: string) => {
if (rewritingRef.current.has(anchor))
return
rewritingRef.current.add(anchor)
setRewriting(new Set(rewritingRef.current))
postMessage({ type: 'rewriteComment', anchor })
}, [])
const kindNames = useMemo(() => new Map(doc?.attributes.find(attribute => attribute.id === 'kind')?.values.map(value => [value.id, value.name])), [doc])
const kindName = useCallback((kind: string) => kindNames.get(kind) ?? kind, [kindNames])
const tree = useMemo((): BlockTreeState | null => doc && {
@@ -147,11 +161,13 @@ export function App(): ReactElement | null {
},
onToggleFold: id => setFolded([id], !collapsed.has(id)),
onSetFolds: folds => setView(prev => ({ ...prev, folds: { ...prev.folds, ...folds } })),
rewriting,
onRewriteComment,
...(qa.state.available && { qa }),
...(changes && { diff: changes }),
...(folded && { folded }),
onOpenRun: id => setOpenedRuns(prev => new Set([...prev, id])),
}, [doc, view, collapsed, filter, covered, kindName, focusId, onFocused, setFolded, qa, changes, folded])
}, [doc, view, collapsed, filter, covered, kindName, focusId, onFocused, setFolded, rewriting, onRewriteComment, qa, changes, folded])
if (!doc || !tree)
return null
@@ -2,8 +2,15 @@ import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { BlockTools } from './BlockTools'
function render(editing: boolean, ask?: { asking: boolean }): string {
return renderToStaticMarkup(<BlockTools editing={editing} onToggleEdit={() => {}} {...(ask && { asking: ask.asking, onToggleAsk: () => {} })} />)
function render(editing: boolean, ask?: { asking: boolean }, rewrite?: { rewriting: boolean }): string {
return renderToStaticMarkup(
<BlockTools
editing={editing}
onToggleEdit={() => {}}
{...(ask && { asking: ask.asking, onToggleAsk: () => {} })}
{...(rewrite && { rewriting: rewrite.rewriting, onRewrite: () => {} })}
/>,
)
}
/** 标签之间的字:图标(svg)没有字,按钮上有字就会出现在这里 */
@@ -31,4 +38,19 @@ describe('块右上角的图标按钮', () => {
expect(buttons[0].includes('data-active')).toBe(asking)
expect(buttons[1]).toContain('data-tooltip="编辑"')
})
it('「按注释规范重写」排在「问 AI」左边;正在重写时禁用并转圈,不用原生 title', () => {
const html = render(false, { asking: false }, { rewriting: false })
expect(visibleText(html)).toBe('')
expect(html).not.toContain(' title=')
const buttons = [...html.matchAll(/<button[^>]*>/g)].map(match => match[0])
expect(buttons).toHaveLength(3)
expect(buttons[0]).toContain('data-tooltip="按注释规范重写" aria-label="按注释规范重写"')
expect(buttons[0]).not.toContain('disabled')
expect(buttons[1]).toContain('data-tooltip="问 AI"')
expect(buttons[2]).toContain('data-tooltip="编辑"')
const busy = render(false, { asking: false }, { rewriting: true })
expect(busy).toContain('disabled=""')
expect(busy).toContain('cr-qa-spin')
})
})
@@ -1,23 +1,43 @@
import type { ReactElement } from 'react'
import { Eye, Pencil, Sparkles } from 'lucide-react'
import { Eye, LoaderCircle, Pencil, RefreshCw, Sparkles } from 'lucide-react'
/**
* 块右上角的一排图标按钮(普通块、属性表、库规则的卡片共用):只放图标,名字写在悬停提示(data-tooltip)和读屏用的 aria-label 里。
* 「按注释规范重写」:只给注释块。点了用写注释的技能重写这一段并写回文件;正在重写时转圈,不能再点。
* 「问 AI」:点开 / 收起块下面的问答区,展开时带按下的底色。
* 「编辑 / 预览」切换:预览时是铅笔「编辑」,编辑时是眼睛「预览」、带按下的底色。
*/
export function BlockTools({ editing, onToggleEdit, asking, onToggleAsk }: {
export function BlockTools({ editing, onToggleEdit, asking, onToggleAsk, rewriting, onRewrite }: {
editing: boolean
onToggleEdit: () => void
/** 块下面的问答区展开着 */
asking?: boolean
/** 没给就不显示「问 AI」(文件不在工作区文件夹里,问答没地方存) */
onToggleAsk?: () => void
/** 这段注释正在重写 */
rewriting?: boolean
/** 没给就不显示「按注释规范重写」(不是注释块) */
onRewrite?: () => void
}): ReactElement {
const label = editing ? '预览' : '编辑'
const Icon = editing ? Eye : Pencil
return (
<div className="flex h-5 shrink-0 items-center gap-0.5" role="group" aria-label="块操作">
{onRewrite && (
<button
type="button"
className="cr-tool flex h-5 w-6 items-center justify-center rounded"
data-tooltip="按注释规范重写"
aria-label="按注释规范重写"
aria-busy={rewriting === true}
disabled={rewriting === true}
onClick={onRewrite}
>
{rewriting
? <LoaderCircle size={14} strokeWidth={1.8} className="cr-qa-spin" aria-hidden />
: <RefreshCw size={14} strokeWidth={1.8} aria-hidden />}
</button>
)}
{onToggleAsk && (
<button
type="button"
@@ -67,6 +67,8 @@ function render(folds: Record<string, boolean>): string {
onToggleEdit: () => {},
onToggleFold: () => {},
onSetFolds: () => {},
rewriting: new Set(),
onRewriteComment: () => {},
onOpenRun: () => {},
}
return renderToStaticMarkup(<BlockLevel blocks={[CARD]} tree={tree} />)
@@ -37,6 +37,9 @@ export interface BlockTreeState {
onToggleFold: (id: string) => void
/** 一次记下几块、几行折叠还是展开(点子行展开一块、同一行里别的收起) */
onSetFolds: (folds: Record<string, boolean>) => void
/** 正在按注释规范重写的注释块(记号) */
rewriting: ReadonlySet<string>
onRewriteComment: (anchor: string) => void
/** 问 AI;文件不在工作区文件夹里(问答没地方存)时没有 */
qa?: QaTree
/** 代码差异:新增、修改的块和删掉的块;侧栏开着「未提交的更改」或审查工单、基准里有这个文件时才有 */
@@ -276,6 +279,8 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
onToggleEdit={() => tree.onToggleEdit(block.id)}
asking={tree.qa?.state.open.includes(block.anchor)}
onToggleAsk={tree.qa && (() => tree.qa?.onToggle(block.anchor))}
rewriting={tree.rewriting.has(block.anchor)}
onRewrite={block.kind === 'comment' ? () => tree.onRewriteComment(block.anchor) : undefined}
/>
</div>
)
@@ -119,6 +119,9 @@ body:is(.vscode-light, .vscode-high-contrast-light) [data-badge] { --kind: var(-
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.cr-tool:disabled {
opacity: 0.4;
}
/* 层级竖线:悬停一个块时,它子层的竖线换成它的种类色,标出这个块管到哪里为止(块和子层中间隔着问答区也算) */
.cr-guide {
@@ -406,6 +406,7 @@ export type ExtensionToWebview
| ({ type: 'askProfiles' } & ProfileChoice)
| { type: 'answer', askId: string, text: string, activity?: string }
| AnswerEnd
| { type: 'commentRewritten', anchor: string }
| { type: 'diff', diff: DocumentDiff | null }
/**
@@ -429,6 +430,7 @@ export type WebviewToExtension
| { type: 'setFilter', excluded: Excluded }
| { type: 'setOnlyChanges', on: boolean }
| { type: 'edit', startLine: number, endLine: number, text: string }
| { type: 'rewriteComment', anchor: string }
| { type: 'selectTranslator', id: string }
| { type: 'visibleLine', line: number }
| { type: 'probeDefinition', id: number, version: number, offset: number }