🐛 fix(vscode): 代码阅读编辑父卡片时显示整块代码(含子块)

- 块的编辑范围改成它在源码里的完整范围:头部连同各子孙块、收尾括号,保存时按整段写回;子卡片只有它自己连同子孙
- 范围由宿主统一算(blockSpans),各语言插件同一条路:else / elif / catch 这类展平的兄弟块按各块开头切开整条语句,最后一块到语句结尾(Shell 的 fi 归它)
- 库规则卡片的编辑范围并进同一套算法,去掉 claimedShape
- 块编辑时它的子孙都不画(代码都在编辑框里),点「编辑」时重叠的祖先、子孙、共用一行的兄弟块退出编辑
- 问 AI 的外层块开头改用新加的 headerEndLine,位置记号照旧按头部算,已有问答不受影响
- 着色改成整份源码算一次、各块按行取,编辑范围变大后排视图数据不变慢
This commit is contained in:
cruldra committed 2026-09-30 07:24:10 +08:00
1 parent af63d2bc5c
commit 70ea22543f
13 files changed
+251 -143

No files matched your search

+102 -41
View File
@@ -1,9 +1,9 @@
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
import type { TreeSitterRuntime } from './runtime'
import { blockAttributes } from './attributes'
import { commonIndent, splitLines } from './editText'
import { activeRules, claimBlock, claimedShape, importsIn } from './libraryRules'
import { activeRules, claimBlock, importsIn } from './libraryRules'
import { BlockAnchors } from './qa/anchor'
interface BlockViewBase {
@@ -14,11 +14,13 @@ interface BlockViewBase {
/** BlockKind,或库规则卡片的种类 id(RuleCard.kind,带点,例如 sqlalchemy.table)。 */
kind: string
/**
* 编辑范围的起止行,0 起:一般是 header;库规则认领的块是原块整条语句(见 claimedShape),盖住它留下的子块的行。
* 同一时刻能编辑的几段不重叠:块编辑时,webview 不画编辑范围和它重叠的子孙块,点「编辑」时和它重叠的别的块退出编辑。
* 编辑范围的起止行,0 起:块在源码里的完整范围,头部连同各子孙块、结尾(见 blockSpans);库规则认领的块是原块整条语句。
* 块编辑时它的子孙都在编辑框里,webview 不画它们;点「编辑」时和它重叠的别的块退出编辑,同一行不会同时在两处能改。
*/
startLine: number
endLine: number
/** 块自己那部分(头部)的最后一行:复合语句到主体开始之前,库规则认领的块是整条语句。问 AI 时贴外层块的开头用。 */
headerEndLine: number
/** 编辑范围的整行源码,去掉共同缩进。 */
code: string
/** 源码着色(拼起来等于 code)。 */
@@ -70,22 +72,29 @@ interface ColorLayer {
tokenRole: (leaf: Node) => TokenRole
}
/** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段;后面的层盖过前面的。 */
function colorLines(layers: readonly ColorLayer[], source: string, lines: Line[], startRow: number, endRow: number, indent: number): Segment[] {
const from = lines[startRow].start
const to = lines[endRow].end
const owner = new Int32Array(to - from).fill(-1)
/** 源码每个字符归哪个叶子(-1 是不归哪个叶子的空白),和各叶子的词性:一份源码算一次,各块按行取。 */
interface Coloring {
owner: Int32Array
roles: TokenRole[]
}
/** 各着色层依次铺上去,后面的层(嵌着的代码)盖住前面的。 */
function coloring(layers: readonly ColorLayer[], source: string): Coloring {
const owner = new Int32Array(source.length).fill(-1)
const roles: TokenRole[] = []
for (const { root, tokenRole } of layers) {
const leaves: Node[] = []
leavesIn(root.descendantForIndex(from, to) ?? root, from, to, leaves)
leavesIn(root, 0, source.length, leaves)
for (const leaf of leaves) {
roles.push(tokenRole(leaf))
for (let at = Math.max(leaf.startIndex, from); at < Math.min(leaf.endIndex, to); at++)
owner[at - from] = roles.length - 1
owner.fill(roles.length - 1, leaf.startIndex, Math.min(leaf.endIndex, source.length))
}
}
return { owner, roles }
}
/** 按语法树叶子给 [startRow, endRow] 这几行着色:每个有词性的叶子一段,空白、标点等 plain 的连成一段。 */
function colorLines({ owner, roles }: Coloring, source: string, lines: Line[], startRow: number, endRow: number, indent: number): Segment[] {
const segments: Segment[] = []
let current = -2
const put = (char: string, who: number): void => {
@@ -100,9 +109,9 @@ function colorLines(layers: readonly ColorLayer[], source: string, lines: Line[]
for (let row = startRow; row <= endRow; row++) {
const line = lines[row]
if (row > startRow)
put('\n', owner[line.start - 1 - from])
put('\n', owner[line.start - 1])
for (let at = line.start + Math.min(indent, line.end - line.start); at < line.end; at++)
put(source[at], owner[at - from])
put(source[at], owner[at])
}
return segments
}
@@ -170,8 +179,50 @@ export interface RuleRun {
const NO_RULES: RuleRun = { rules: [], report: () => {} }
/** 源码里一段的结尾:到哪个字符为止(不含),那个字符在第几行。 */
type SpanEnd = Pick<SourceSpan, 'endIndex' | 'endRow'>
function nodeEnd(node: Node): SpanEnd {
return { endIndex: node.endIndex, endRow: node.endPosition.row }
}
function later(a: SpanEnd, b: SpanEnd): SpanEnd {
return b.endIndex > a.endIndex ? b : a
}
/** 各子孙块的头部、节点里最靠后的结尾,和 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)
}
/** index 之前最后一个非空白字符的结尾,不早于头部开头。 */
function lastCharBefore(source: string, header: SourceSpan, index: number): SpanEnd {
let end = index
while (end > header.startIndex && /\s/.test(source[end - 1]))
end--
return { endIndex: end, endRow: header.startRow + splitLines(source.slice(header.startIndex, end)).length - 1 }
}
/**
* 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。
* 一串兄弟块各自在源码里的完整范围:从头部开头,到头部、所在语句、各子孙块里最靠后的那个字符。
* 语言插件把 else、catch 这类子句展平成紧跟着的兄弟块,它们的节点都落在打头那块的节点(整条 if / try)里:
* 这条语句按各块的开头切开,每块到下一块开头之前最后一个非空白字符为止,最后一块到语句结尾(Shell 的 fi 归它)。
*/
function blockSpans(list: readonly CodeBlock[], source: string): SourceSpan[] {
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 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 }
})
}
/**
* 代码块树 → 视图数据:块的完整范围整行源码去共同缩进、按语法树着色,调用翻译插件。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
* 并进去的子块不再单独成块。
*/
@@ -186,12 +237,12 @@ export function toBlockViews(
const lines = lineRanges(source)
const texts = splitLines(source)
const outer = blocks[0]?.node.tree.rootNode
const colors: ColorLayer[] = [
const colors = coloring([
...(outer ? [{ root: outer, tokenRole: document.language.tokenRole }] : []),
...[...embedded.values()].map(part => ({ root: part.root, tokenRole: part.document.language.tokenRole })),
]
/** startRow..endRow 这几行整行源码去共同缩进,和它的着色(嵌着的代码按它那种语言着色)。 */
const sourceRows = (startRow: number, endRow: number): Pick<BlockView, 'startLine' | 'endLine' | 'code' | 'codeTokens'> => {
], source)
/** 一段占的整行源码去共同缩进,和它的着色(嵌着的代码按它那种语言着色)。 */
const sourceRows = ({ startRow, endRow }: SourceSpan): Pick<BlockView, 'startLine' | 'endLine' | 'code' | 'codeTokens'> => {
const rows = texts.slice(startRow, endRow + 1)
const indent = commonIndent(rows).length
return {
@@ -201,11 +252,16 @@ export function toBlockViews(
codeTokens: colorLines(colors, source, lines, startRow, endRow, indent),
}
}
/** 一段占的整行源码:没名字的块按它认(见 BlockAnchors.place)。 */
const rowsText = ({ startRow, endRow }: SourceSpan): string => texts.slice(startRow, endRow + 1).join('\n')
/** 按源码先后逐块给位置记号:先序遍历,块的记号在它的子块之前要 */
const anchors = new BlockAnchors()
/** 一种语言的块怎么转:用这种语言的源码、翻译插件和库规则(嵌着的代码没有库规则)。 */
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun): (block: CodeBlock, id: string, scope: string) => BlockView {
/**
* 一种语言的块怎么转:用这种语言的源码、翻译插件和库规则(嵌着的代码没有库规则)。
* 转的是一串兄弟块:各块的完整范围(编辑范围)按这一串算(见 blockSpans),id 是 prefix 接上它在这一串里的位置(从 from 数起)。
*/
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun): (list: readonly CodeBlock[], prefix: string, scope: string, from?: number) => BlockView[] {
const imports = importsIn(layerBlocks)
const active = activeRules(layerRules.rules, imports)
const context: RuleContext = {
@@ -231,58 +287,63 @@ export function toBlockViews(
return []
const steps = part.steps.blocks
const [only] = steps
const kids = steps.length === 1 && only ? only.children : steps
const build = layer(steps, part.document, part.translator, NO_RULES)
return kids.map((child, i) => build(child, `${id}.${from + i}`, scope))
return layer(steps, part.document, part.translator, NO_RULES)(steps.length === 1 && only ? only.children : steps, `${id}.`, scope, from)
}
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):头部是编辑范围,子块照常交给各规则看。scope 见 BlockAnchors.place */
function plain(block: CodeBlock, id: string, scope: string): BlockView {
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):编辑范围是 span,头部是块自己那部分,子块照常交给各规则看。scope 见 BlockAnchors.place */
function plain(block: CodeBlock, span: SourceSpan, id: string, scope: string): BlockView {
const name = ownName(block)
const rows = sourceRows(block.header.startRow, block.header.endRow)
const { anchor, inner } = anchors.place(scope, block.kind, name?.text, rows.code)
const { anchor, inner } = anchors.place(scope, block.kind, name?.text, rowsText(block.header))
return {
id,
anchor,
kind: block.kind,
...rows,
...sourceRows(span),
headerEndLine: block.header.endRow,
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
attributes: blockAttributes(block),
children: [
...block.children.map((child, i) => build(child, `${id}.${i}`, inner)),
...list(block.children, `${id}.`, inner),
...embeddedChildren(block, id, inner, block.children.length),
],
...('facts' in block ? { properties: layerTranslator ? layerTranslator.describe(block, layerDocument) : [] } : textView(block)),
}
}
function build(block: CodeBlock, id: string, scope: string): BlockView {
/**
* 块先问启用的库规则,没认领就照它自己的样子。认领了的:规则讲的是整条语句,头部就是整个范围;
* 规则没并进来的子块(方法、别的成员)不管写在哪,都照源码顺序留作它的子块。
*/
function build(block: CodeBlock, span: SourceSpan, id: string, scope: string): BlockView {
const claim = active.length > 0 ? claimBlock(block, active, context, layerRules.report) : undefined
if (!claim)
return plain(block, id, scope)
const { span, kept } = claimedShape(block, claim.block.absorbs)
return plain(block, span, id, scope)
const kept = block.children.filter(child => !claim.block.absorbs.includes(child))
if ('as' in claim.block) {
const { attributes } = block
return plain({ ...claim.block.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, id, scope)
return plain({ ...claim.block.as, node: block.node, header: span, children: kept, ...(attributes && { attributes }) }, span, id, scope)
}
const { kind, badge, rows, defines, attributes } = claim.block
const lines = sourceRows(span.startRow, span.endRow)
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), lines.code)
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), rowsText(span))
return {
id,
anchor,
kind,
...lines,
...sourceRows(span),
headerEndLine: span.endRow,
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
badge,
children: kept.map((child, i) => build(child, `${id}.${i}`, inner)),
children: list(kept, `${id}.`, inner),
properties: rows,
}
}
return build
function list(siblings: readonly CodeBlock[], prefix: string, scope: string, from = 0): BlockView[] {
const spans = blockSpans(siblings, source)
return siblings.map((block, i) => build(block, spans[i], `${prefix}${from + i}`, scope))
}
return list
}
const build = layer(blocks, document, translator, rules)
return blocks.map((block, i) => build(block, String(i), ''))
return layer(blocks, document, translator, rules)(blocks, '', '')
}
/**