🐛 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

@@ -22,7 +22,7 @@ export interface BlockTreeState {
hidden: ReadonlySet<string>
/** 本来被筛掉、跳到定义时临时显示的块 */
temporary: ReadonlySet<string>
/** 被正在编辑的祖先盖住、不画的块(它们的行在祖先的编辑范围里),见 coveredByEditing */
/** 被正在编辑的祖先盖住、不画的块(它们的代码在祖先的编辑框里),见 coveredByEditing */
covered: ReadonlyMap<string, string>
/** 种类叫什么(种类图标的悬停提示和 aria-label):翻译插件给种类起的名字(库规则的种类是规则给的 badge.label),没给就是 id */
kindName: (kind: string) => string
@@ -8,43 +8,46 @@ function block(id: string, kind: string, [startLine, endLine]: [number, number],
}
/**
* 库规则的卡片(1)编辑范围是整个类(2–12 行),两个字段中间写着的方法(1.0)和它的主体(1.0.0)、类末尾的方法(1.1)都在这段里;
* 普通的类(2)只编辑头部,方法(2.0)不在头部里;一行写完的 if(3)和它的返回(3.0)在同一行。
* 编辑范围是块的完整源码:库规则的卡片(1)是整个类(2–12 行),两个字段中间写着的方法(1.0)和它的主体(1.0.0)、
* 类末尾的方法(1.1)都在这段里;普通的类(2)同样盖住它的方法(2.0);`} else {` 写在一行时 if(3)和 else(4)共用第 20 行。
*/
const BLOCKS = [
block('0', 'import', [0, 0]),
block('1', 'sqlalchemy.table', [2, 12], [
block('1.0', 'function', [5, 5], [block('1.0.0', 'return', [6, 6])]),
block('1.0', 'function', [5, 6], [block('1.0.0', 'return', [6, 6])]),
block('1.1', 'function', [11, 12]),
]),
block('2', 'class', [14, 14], [block('2.0', 'function', [15, 16])]),
block('3', 'if', [18, 18], [block('3.0', 'return', [18, 18])]),
block('2', 'class', [14, 16], [block('2.0', 'function', [15, 16])]),
block('3', 'if', [18, 20], [block('3.0', 'return', [19, 19])]),
block('4', 'else', [20, 22], [block('4.0', 'return', [21, 21])]),
]
describe('同一行不会同时在两处能改', () => {
it('卡片编辑时,它留下的子块(字段中间的方法连同主体)不画,跳到它们时改闪卡片', () => {
it('块编辑时它的子孙都不画(代码都在它的编辑框里),跳到它们时改闪它', () => {
expect(coveredByEditing(BLOCKS, new Set(['1']))).toEqual(new Map([['1.0', '1'], ['1.0.0', '1'], ['1.1', '1']]))
expect(coveredByEditing(BLOCKS, new Set(['2']))).toEqual(new Map([['2.0', '2']]))
})
it('只编辑头部的块不盖住子块;一行写完的块编辑时盖住同一行的子块', () => {
expect(coveredByEditing(BLOCKS, new Set(['2', '2.0']))).toEqual(new Map())
expect(coveredByEditing(BLOCKS, new Set(['3']))).toEqual(new Map([['3.0', '3']]))
it('子块编辑时只盖住它自己的子孙,父块、兄弟块照常画', () => {
expect(coveredByEditing(BLOCKS, new Set(['1.0']))).toEqual(new Map([['1.0.0', '1.0']]))
expect(coveredByEditing(BLOCKS, new Set(['1.1', '3']))).toEqual(new Map([['3.0', '3']]))
})
it('没在编辑的卡片不盖住子块:子块照常画、照常能单独编辑', () => {
expect(coveredByEditing(BLOCKS, new Set(['1.0']))).toEqual(new Map())
it('祖先和子孙都记着在编辑时(文档刷新前的状态),算页面上画着的最外层那块', () => {
expect(coveredByEditing(BLOCKS, new Set(['1', '1.0']))).toEqual(new Map([['1.0', '1'], ['1.0.0', '1'], ['1.1', '1']]))
})
it('点「编辑」:编辑范围和它重叠的别的块退出编辑,不重叠的留着', () => {
it('点「编辑」:编辑范围和它重叠的别的块(祖先、子孙、共用一行的兄弟块)退出编辑,不重叠的留着', () => {
expect(startEditing(BLOCKS, ['1.0', '1.1', '2'], '1')).toEqual(['2', '1'])
expect(startEditing(BLOCKS, ['1', '2'], '1.0')).toEqual(['2', '1.0'])
expect(startEditing(BLOCKS, ['2'], '2.0')).toEqual(['2', '2.0'])
expect(startEditing(BLOCKS, ['2'], '2.0')).toEqual(['2.0'])
expect(startEditing(BLOCKS, ['0', '3'], '4')).toEqual(['0', '4'])
})
it('任何时候正在编辑、画出来的几块,编辑范围两两不重叠', () => {
const all = (list: BlockView[]): BlockView[] => list.flatMap(item => [item, ...all(item.children)])
let editing: string[] = []
for (const id of ['1.0', '1.0.0', '2', '2.0', '1', '3.0', '3', '1.1'])
for (const id of ['1.0', '1.0.0', '2', '2.0', '1', '3.0', '3', '4', '1.1'])
editing = startEditing(BLOCKS, editing, id)
const covered = coveredByEditing(BLOCKS, new Set(editing))
const shown = all(BLOCKS).filter(item => editing.includes(item.id) && !covered.has(item.id))
+15 -14
View File
@@ -2,9 +2,8 @@ import type { BlockView } from './types'
/**
* 同一时刻能编辑的几段源码不重叠,同一行不会同时在两处能改:
* ① 块编辑时,编辑范围和它重叠的子孙块不画(库规则的卡片编辑整个类,留下的方法也在这段里;
* 一行写完的 `if x: return 1`,子块和头部在同一行),它们下面的子孙也跟着不画;
* ② 点「编辑」时,编辑范围和这块重叠的别的块退出编辑。
* ① 块的编辑范围是它的完整源码,子孙都在它的编辑框里:块编辑时它的子孙都不画;
* ② 点「编辑」时,编辑范围和这块重叠的别的块(祖先、子孙,`} else {` 这样共用一行的兄弟块)退出编辑。
*/
type Lines = Pick<BlockView, 'startLine' | 'endLine'>
@@ -13,22 +12,24 @@ function overlaps(a: Lines, b: Lines): boolean {
return a.startLine <= b.endLine && b.startLine <= a.endLine
}
/** 正在编辑的块盖住、不画的块 → 盖住它的那块(页面上画着的、正在编辑的祖先):跳到定义落在被盖住的块上时改闪它。 */
/** 正在编辑的块的子孙不画 → 盖住它的那块(页面上画着的、正在编辑的祖先):跳到定义落在被盖住的块上时改闪它。 */
export function coveredByEditing(blocks: readonly BlockView[], editing: ReadonlySet<string>): Map<string, string> {
const covered = new Map<string, string>()
const visit = (list: readonly BlockView[], editors: readonly BlockView[], owner: string | undefined): void => {
const cover = (list: readonly BlockView[], by: string): void => {
for (const block of list) {
const by = owner ?? editors.find(editor => overlaps(editor, block))?.id
if (by !== undefined) {
covered.set(block.id, by)
visit(block.children, editors, by)
}
else {
visit(block.children, editing.has(block.id) ? [...editors, block] : editors, undefined)
}
covered.set(block.id, by)
cover(block.children, by)
}
}
visit(blocks, [], undefined)
const visit = (list: readonly BlockView[]): void => {
for (const block of list) {
if (editing.has(block.id))
cover(block.children, block.id)
else
visit(block.children)
}
}
visit(blocks)
return covered
}
+1 -1
View File
@@ -102,7 +102,7 @@ interface BlockViewBase {
anchor: string
/** BlockKind,或库规则认领的块的种类 id(带点,例如 sqlalchemy.table);webview 不认识后者,只按 badge 画 */
kind: string
/** 编辑范围的起止行,0 起:一般是 header,库规则认领的块是原块整条语句(盖住它留下的子块,见 coveredByEditing) */
/** 编辑范围的起止行,0 起:块的完整源码,头部连同各子孙块、结尾(块编辑时子孙都不画,见 coveredByEditing) */
startLine: number
endLine: number
/** 编辑范围的源码,去掉共同缩进 */