✨ 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

+110 -31
View File
@@ -5,7 +5,7 @@ import type { ImportedName } from './symbolLinks'
import { blockAttributes } from './attributes'
import { KIND_ICONS } from './contract'
import { commonIndent, splitLines } from './editText'
import { commentSpan } from './languages/shared'
import { isCommentBlock } from './languages/shared'
import { activeRules, claimBlock, importsIn } from './libraryRules'
import { BlockAnchors } from './qa/anchor'
import { importedNames } from './symbolLinks'
@@ -38,9 +38,13 @@ interface BlockViewBase {
* 同一个 id 只留第一个。项目入口那一块的第一个换成入口(见 entries/entryMarks.ts)。webview 按这个画图标,不自己用种类查。
*/
concepts: BlockConcept[]
/** 能重写的那段注释的起止行,0 起。注释块是整块;函数、类等是它的说明。没有说明就没有,不显示重写按钮。 */
commentStartLine?: number
commentEndLine?: number
/**
* 「注释」一行(讲这块的普通注释,BlockShape.comments)排在第几行,0 起:属性表卡片是 properties 里的下标,
* 一行译文的卡片是 rows 里的下标。没有注释就没有。
*/
commentRow?: number
/** 「重写注释」写到哪(见 CommentTarget);不能写注释的块(嵌着的代码里的、语言没说行注释怎么写又还没有注释的)没有。 */
commentTarget?: CommentTarget
/** 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色(RuleCard.badge)。图标在 concepts 里。预览同类块一样默认收起它的子块。 */
badge?: KindBadge
/** 库规则挂在父卡片子行上的子块才有(见 RuleSlot):展开时画在父卡片属性表第 row 行下面,不排在父卡片的子块里。 */
@@ -55,10 +59,19 @@ export interface BlockSlot {
}
/**
* 发给 webview 的一块(可序列化):函数、类、枚举、异常、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(TextTranslation.table),块的说明(TextBlock.doc,翻译插件给的 Markdown 原文)。
* 「重写注释」写到哪,行号 0 起:replace 是换掉 startLine..endLine 这几行(块前面那几段普通注释;文件开头的说明卡片是它自己),
* insert 是块还没有注释,新写一段插在 line 这一行上面(块的第一行),缩进同这一行。
*/
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
export type CommentTarget
= | { kind: 'replace', startLine: number, endLine: number }
| { kind: 'insert', line: number }
/**
* 发给 webview 的一块(可序列化):函数、类、枚举、异常、导入、常量、变量块和库规则的卡片预览时是属性表,文件开头的说明是 Markdown,其余是一行译文。
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(TextTranslation.table),块的说明(TextBlock.doc,翻译插件给的 Markdown 原文),
* 几行属性(rows:「注释」一行)。
*/
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } | { properties: PropertyRow[] })
interface Line { start: number, end: number }
@@ -211,9 +224,20 @@ function later(a: SpanEnd, b: SpanEnd): SpanEnd {
return b.endIndex > a.endIndex ? b : a
}
/** 各子孙块的头部、节点里最靠后的结尾,和 end 比取靠后的:子孙一般写在块的节点里,Dockerfile 阶段的指令写在 FROM 后面。 */
/** 讲这块的注释(BlockShape.comments)里最靠后的结尾,和 end 比取靠后的:这一层最后一块代码后面的注释挂在它上面。 */
function commentsEnd(block: CodeBlock, end: SpanEnd): SpanEnd {
return (block.comments ?? []).reduce((acc, comment) => later(acc, comment.header), end)
}
/** 块的完整范围从哪起:头部和讲它的注释里最靠前的那个开头(注释写在块上面)。 */
function blockStart(block: CodeBlock): Pick<SourceSpan, 'startIndex' | 'startRow'> {
const first = block.comments?.[0]?.header
return first && first.startIndex < block.header.startIndex ? first : block.header
}
/** 各子孙块的头部、节点、讲它的注释里最靠后的结尾,和 end 比取靠后的:子孙一般写在块的节点里,Dockerfile 阶段的指令写在 FROM 后面。 */
function descendantsEnd(blocks: readonly CodeBlock[], end: SpanEnd): SpanEnd {
return blocks.reduce((acc, child) => descendantsEnd(child.children, later(later(acc, child.header), nodeEnd(child.node))), end)
return blocks.reduce((acc, child) => descendantsEnd(child.children, commentsEnd(child, later(later(acc, child.header), nodeEnd(child.node)))), end)
}
/** index 之前最后一个非空白字符的结尾,不早于头部开头。 */
@@ -224,28 +248,49 @@ function lastCharBefore(source: string, header: SourceSpan, index: number): Span
return { endIndex: end, endRow: header.startRow + splitLines(source.slice(header.startIndex, end)).length - 1 }
}
/** 一块在源码里的完整范围(whole,编辑范围),和不算讲它的注释时这条语句的范围(statement)。 */
interface BlockSpan {
whole: SourceSpan
statement: SourceSpan
}
/**
* 一串兄弟块各自在源码里的完整范围:从头部开头,到头部、所在语句、各子孙块里最靠后的那个字符。
* 一串兄弟块各自在源码里的范围:语句从头部开头,到头部、所在语句、各子孙块里最靠后的那个字符;完整范围再连同讲它的注释。
* 语言插件把 else、catch 这类子句展平成紧跟着的兄弟块,它们的节点都落在打头那块的节点(整条 if / try)里:
* 这条语句按各块的开头切开,每块到下一块开头之前最后一个非空白字符为止,最后一块到语句结尾(Shell 的 fi 归它)。
* 这条语句按各块的开头切开,每块到下一块开头(连同讲它的注释)之前最后一个非空白字符为止,最后一块到语句结尾(Shell 的 fi 归它)。
*/
function blockSpans(list: readonly CodeBlock[], source: string): SourceSpan[] {
function blockSpans(list: readonly CodeBlock[], source: string): BlockSpan[] {
let statement: Node | undefined
return list.map((block, i) => {
if (!statement || block.node.startIndex < statement.startIndex || block.node.endIndex > statement.endIndex)
statement = block.node
const next = list[i + 1]
const nextStart = next && Math.min(next.header.startIndex, next.node.startIndex)
const nextStart = next && Math.min(blockStart(next).startIndex, next.node.startIndex)
const until = nextStart !== undefined && nextStart < statement.endIndex ? lastCharBefore(source, block.header, nextStart) : nodeEnd(statement)
const { endIndex, endRow } = descendantsEnd(block.children, later(block.header, until))
return { startIndex: block.header.startIndex, endIndex, startRow: block.header.startRow, endRow }
const end = descendantsEnd(block.children, later(block.header, until))
const wholeEnd = commentsEnd(block, end)
const start = blockStart(block)
return {
whole: { startIndex: start.startIndex, startRow: start.startRow, endIndex: wholeEnd.endIndex, endRow: wholeEnd.endRow },
statement: { startIndex: block.header.startIndex, startRow: block.header.startRow, endIndex: end.endIndex, endRow: end.endRow },
}
})
}
/**
* 「注释」一行插进属性表:排在第一行(名称这类讲它是谁的一行)后面,有说明(值是 Markdown 的一行)时排在说明后面。
* 返回新的属性表和「注释」一行的下标。
*/
function withCommentRow(rows: readonly PropertyRow[], row: PropertyRow): { rows: PropertyRow[], at: number } {
const doc = rows.findIndex(item => 'markdown' in item.value)
const at = doc >= 0 ? doc + 1 : Math.min(1, rows.length)
return { rows: [...rows.slice(0, at), row, ...rows.slice(at)], at }
}
/**
* 代码块树 → 视图数据:块的完整范围整行源码去共同缩进、按语法树着色,调用翻译插件。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
* 并进去的子块不再单独成块。
* 并进去的子块不再单独成块。讲块的注释(BlockShape.comments)排成卡片上的「注释」一行(见 withCommentRow)。
*/
export function toBlockViews(
blocks: CodeBlock[],
@@ -281,8 +326,9 @@ export function toBlockViews(
/**
* 一种语言的块怎么转:用这种语言的源码、翻译插件和库规则(嵌着的代码没有库规则)。
* 转的是一串兄弟块:各块的完整范围(编辑范围)按这一串算(见 blockSpans),id 是 prefix 接上它在这一串里的位置(从 from 数起)。
* writable 为假时(嵌着的代码)块不能写注释:新写的行注释会写进外层语言的那几行里。
*/
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun): (list: readonly CodeBlock[], prefix: string, scope: string, from?: number) => BlockView[] {
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun, writable: boolean): (list: readonly CodeBlock[], prefix: string, scope: string, from?: number) => BlockView[] {
const imports = importsIn(layerBlocks)
const active = activeRules(layerRules.rules, imports)
const context: RuleContext = {
@@ -308,7 +354,7 @@ export function toBlockViews(
return []
const steps = part.steps.blocks
const [only] = steps
return layer(steps, part.document, part.translator, NO_RULES)(steps.length === 1 && only ? only.children : steps, `${id}.`, scope, from)
return layer(steps, part.document, part.translator, NO_RULES, false)(steps.length === 1 && only ? only.children : steps, `${id}.`, scope, from)
}
/**
* 这块的概念,从左到右,同一个 id 只留第一个:语言的种类(图标、名字用内置种类和翻译插件起的种类名),
@@ -337,9 +383,42 @@ export function toBlockViews(
}
return concepts
}
function commentLines(block: CodeBlock): { commentStartLine: number, commentEndLine: number } | undefined {
const span = commentSpan(block)
return span && { commentStartLine: span.startLine, commentEndLine: span.endLine }
/** 「注释」一行:讲这块的几段注释(BlockShape.comments)各读成一段 Markdown,空行隔开;名字是翻译插件给种类 comment 起的。没有注释、没有翻译插件时没有。 */
function commentRow(block: CodeBlock): PropertyRow | undefined {
if (!layerTranslator || !block.comments)
return undefined
const parts = block.comments.flatMap((comment) => {
const { text } = layerTranslator.translate(comment, layerDocument)
return 'markdown' in text && text.markdown !== '' ? [text.markdown] : []
})
const label = layerTranslator.attributeNames?.values.kind?.comment ?? 'comment'
return parts.length > 0 ? { label, value: { markdown: parts.join('\n\n') } } : undefined
}
/** 属性表卡片的属性表连同「注释」一行(见 withCommentRow)。 */
function cardRows(block: CodeBlock, rows: PropertyRow[]): Pick<BlockView, 'commentRow'> & { properties: PropertyRow[] } {
const row = commentRow(block)
if (!row)
return { properties: rows }
const { rows: properties, at } = withCommentRow(rows, row)
return { properties, commentRow: at }
}
/** 「重写注释」写到哪(见 CommentTarget):块前面的普通注释换掉,没有就新写在块上面。 */
function commentTarget(block: CodeBlock): Pick<BlockView, 'commentTarget'> {
if (!writable)
return {}
if (isCommentBlock(block))
return { commentTarget: { kind: 'replace', startLine: block.header.startRow, endLine: block.header.endRow } }
const leading = (block.comments ?? []).filter(comment => comment.header.startIndex < block.header.startIndex)
const [first] = leading
const last = leading.at(-1)
if (first && last)
return { commentTarget: { kind: 'replace', startLine: first.header.startRow, endLine: last.header.endRow } }
return layerDocument.language.lineComment ? { commentTarget: { kind: 'insert', line: block.header.startRow } } : {}
}
/** 一行译文的卡片:译文、实参表、说明,有注释时下面再排「注释」一行。 */
function textCard(block: TextBlock): Pick<BlockView, 'commentRow'> & ReturnType<typeof textView> & { rows?: PropertyRow[] } {
const row = commentRow(block)
return { ...textView(block), ...(row && { rows: [row], commentRow: 0 }) }
}
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):编辑范围是 span,头部是块自己那部分,子块照常交给各规则看。scope 见 BlockAnchors.place */
@@ -355,19 +434,19 @@ export function toBlockViews(
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
attributes: blockAttributes(block),
concepts: blockConcepts(block),
...commentLines(block),
...commentTarget(block),
children: [
...list(block.children, `${id}.`, inner),
...embeddedChildren(block, id, inner, block.children.length),
],
...('facts' in block ? { properties: layerTranslator ? layerTranslator.describe(block, layerDocument) : [] } : textView(block)),
...('facts' in block ? cardRows(block, layerTranslator ? layerTranslator.describe(block, layerDocument) : []) : textCard(block)),
}
}
/**
* 块先问启用的库规则,没认领就照它自己的样子。认领了的:规则讲的是整条语句,头部就是整个范围;
* 规则没并进来的子块(方法、别的成员)不管写在哪,都照源码顺序留作它的子块。规则略去的块(讲在了别的卡片里)不出块。
*/
function build(block: CodeBlock, span: SourceSpan, id: string, scope: string): BlockView | undefined {
function build(block: CodeBlock, { whole: span, statement }: BlockSpan, id: string, scope: string): BlockView | undefined {
const claim = active.length > 0 ? claimBlock(block, active, context, layerRules.report) : undefined
if (!claim)
return plain(block, span, id, scope)
@@ -376,25 +455,25 @@ export function toBlockViews(
return undefined
const kept = block.children.filter(child => !replacement.absorbs.includes(child))
if ('as' in replacement) {
const { attributes } = block
return plain({ ...replacement.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, span, id, scope)
const { attributes, comments } = block
return plain({ ...replacement.as, node: block.node, header: statement, children: kept, ...(attributes && { attributes }), ...(comments && { comments }) }, span, id, scope)
}
const { kind, badge, rows, defines, attributes } = replacement
const slots = new Map((replacement.slots ?? []).map(({ block: child, row, item }): [CodeBlock, BlockSlot] => [child, { row, item }]))
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), rowsText(span))
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), rowsText(statement))
return {
id,
anchor,
kind,
...sourceRows(span),
headerEndLine: span.endRow,
headerEndLine: statement.endRow,
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
concepts: blockConcepts(block, replacement),
...commentLines(block),
...commentTarget(block),
badge,
children: list(kept, `${id}.`, inner, 0, slots),
properties: rows,
...cardRows(block, rows),
}
}
/** 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。slots 是父卡片挂在它属性表子行上的那几块(见 BlockSlot)。 */
@@ -412,7 +491,7 @@ export function toBlockViews(
return list
}
return layer(blocks, document, translator, rules)(blocks, '', '')
return layer(blocks, document, translator, rules, true)(blocks, '', '')
}
/**