✨ feat(vscode): 代码阅读预览按侧栏筛选算代码差异(未提交的更改对比上一次提交、审查对比 PR 目标分支;兄弟块按名字 / 相似度对齐,卡片内按行 / 值 / 词比对),算好后补发给预览

This commit is contained in:
cruldra committed 2026-09-30 08:35:17 +08:00
1 parent 3db44f84ae
commit 2a5806c6b5
23 files changed
+1527 -31

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
import type { BlockView } from '../blockView'
import type { Inline, PropertyItem, PropertyRow, Segment, TokenRole } from '../contract'
import { describe, expect, it } from 'vitest'
import { blockMarks, inlineDiff, itemsDiff, markdownDiff, pairByKey, removedItems, removedView, rowsDiff, textDiff, withoutSource } from './viewDiff'
function seg(text: string, role: TokenRole = 'plain', offset?: number): Segment {
return { text, role, ...(offset !== undefined && { offset }) }
}
function item(...cells: string[]): PropertyItem {
return { cells: cells.map(cell => [seg(cell, 'variable')]) }
}
function row(label: string, value: Inline[] | { markdown: string }, items?: PropertyItem[]): PropertyRow {
return { label, value, ...(items && { items }) }
}
function textView(translation: Inline[] | { markdown: string }, extra: Partial<BlockView> = {}): BlockView {
return { id: '0', anchor: 'a', kind: 'statement', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes: {}, children: [], translation, ...extra } as BlockView
}
function propertyView(properties: PropertyRow[]): BlockView {
return { id: '0', anchor: 'a', kind: 'function', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes: {}, children: [], properties }
}
describe('inlineDiff', () => {
it('一样(只是源码下标不同)时没有改动', () => {
expect(inlineDiff([seg('a', 'variable', 1)], [seg('a', 'variable', 9)])).toBeUndefined()
})
it('大部分没变时逐截标:没变的、新加的照新版本(带下标),删掉的取旧版本、去掉下标', () => {
const before = [seg('call', 'function', 0), seg('('), seg('key', 'variable', 5), seg(')')]
const after = [seg('call', 'function', 0), seg('('), seg('key', 'variable', 5), seg(', '), seg('token', 'variable', 10), seg(')')]
expect(inlineDiff(before, after)).toEqual([
{ segments: [seg('call', 'function', 0), seg('('), seg('key', 'variable', 5)] },
{ segments: [seg(', '), seg('token', 'variable', 10)], change: 'add' },
{ segments: [seg(')')] },
])
})
it('改得多时整段写成旧值 → 新值,旧值去掉下标', () => {
expect(inlineDiff([seg('布尔', 'type', 3)], [seg('字符串', 'type'), seg(' | '), seg('空', 'storage')])).toEqual([
{ segments: [seg('布尔', 'type')], change: 'del' },
{ segments: [seg('字符串', 'type'), seg(' | '), seg('空', 'storage')], change: 'add' },
])
})
it('框按样式、框头和框里的词比', () => {
const group = (label: string): Inline => ({ style: 'iteration', label, segments: [seg('x', 'variable', 2)] })
expect(inlineDiff([group('列表')], [group('列表')])).toBeUndefined()
expect(inlineDiff([group('列表')], [group('生成器')])).toHaveLength(2)
})
})
describe('markdownDiff', () => {
it('按词比,中文按字', () => {
expect(markdownDiff('Lua 比对 token 再删.', 'Lua 比对 token 再续期 / 再删.')).toEqual([
{ text: 'Lua 比对 token 再' },
{ text: '续期 / 再', change: 'add' },
{ text: '删.' },
])
})
it('删掉的字取旧版本', () => {
expect(markdownDiff('放锁前必须比对', '放锁都必须比对')).toEqual([
{ text: '放锁' },
{ text: '前', change: 'del' },
{ text: '都', change: 'add' },
{ text: '必须比对' },
])
})
it('一样时没有改动', () => {
expect(markdownDiff('同样', '同样')).toBeUndefined()
})
})
describe('textDiff', () => {
it('着色的词逐截比,Markdown 逐词比,一种换成另一种时按字逐词比', () => {
expect(textDiff([seg('a')], [seg('b')])).toHaveProperty('inline')
expect(textDiff({ markdown: 'a' }, { markdown: 'b' })).toHaveProperty('markdown')
expect(textDiff([seg('无', 'hint')], { markdown: '说明' })).toEqual({ markdown: [{ text: '无', change: 'del' }, { text: '说明', change: 'add' }] })
})
})
describe('pairByKey / removedItems', () => {
it('同 key 照先后配;没配上的旧项排在它前面最近配上的那项所配的新项后面', () => {
const before = ['a', 'x', 'b', 'y']
const after = ['b', 'a', 'c']
const { toOld, toNew } = pairByKey(before, after, value => value)
expect(toOld).toEqual([2, 0, undefined])
expect(toNew).toEqual([1, undefined, 0, undefined])
expect(removedItems(before, toNew, value => value)).toEqual([{ after: 1, value: 'x' }, { after: 0, value: 'y' }])
})
it('最前面没配上的排在最前面', () => {
const { toNew } = pairByKey(['x', 'a'], ['a'], value => value)
expect(removedItems(['x', 'a'], toNew, value => value)).toEqual([{ after: -1, value: 'x' }])
})
})
describe('itemsDiff', () => {
it('按第一列配:新参数算加行,没了的参数算删行,配上的逐格比', () => {
const before = [item('redis', 'Any'), item('ttl', '整数')]
const after = [item('redis', 'Any'), item('ttl_sec', '整数'), item('prefix', '字符串')]
const diff = itemsDiff(before, after)
expect(diff?.changed).toEqual({ 1: { added: true }, 2: { added: true } })
expect(diff?.removed).toEqual([{ after: 0, value: item('ttl', '整数') }])
})
it('配上的行某一格变了只标那一格', () => {
const diff = itemsDiff([item('key', '字符串')], [item('key', '字节串')])
expect(Object.keys(diff?.changed[0].cells ?? {})).toEqual(['1'])
})
it('新加的行下面各层也是新加的', () => {
const added: PropertyItem = { cells: [[seg('k')]], items: [item('inner')], table: { columns: ['名称'], items: [item('arg')] } }
expect(itemsDiff([], [added])?.changed[0]).toEqual({
added: true,
items: { changed: { 0: { added: true } }, removed: [] },
table: { changed: { 0: { added: true } }, removed: [] },
})
})
it('都没有、或一样时没有改动', () => {
expect(itemsDiff(undefined, undefined)).toBeUndefined()
expect(itemsDiff([item('a')], [item('a')])).toBeUndefined()
})
})
describe('rowsDiff', () => {
it('按属性名配:值变了逐截标,多出来的行算加行,没了的行算删行', () => {
const before = [row('名称', [seg('f', 'function')]), row('输出参数', [seg('无', 'hint')]), row('装饰器', [seg('@x')])]
const after = [row('名称', [seg('f', 'function')]), row('异步', [seg('是')]), row('输出参数', [seg('布尔', 'type')])]
const diff = rowsDiff(before, after)
expect(diff?.changed[1]).toEqual({ added: true })
expect(diff?.changed[2].value).toBeDefined()
expect(diff?.removed).toEqual([{ after: 2, value: row('装饰器', [seg('@x')]) }])
})
it('子行改了时只标子行,值(子行的汇总)不另标', () => {
const before = [row('输入参数', [seg('1 个', 'hint')], [item('a')])]
const after = [row('输入参数', [seg('2 个', 'hint')], [item('a'), item('b')])]
expect(rowsDiff(before, after)?.changed[0]).toEqual({ items: { changed: { 1: { added: true } }, removed: [] } })
})
it('新加的行带着子行时,子行也都是新加的', () => {
const diff = rowsDiff([], [row('输入参数', [seg('1 个')], [item('a')])])
expect(diff?.changed[0]).toEqual({ added: true, items: { changed: { 0: { added: true } }, removed: [] } })
})
})
describe('blockMarks', () => {
it('一行译文、摊开实参的表、说明各比各的', () => {
const before = textView([seg('调用 f')], { doc: { markdown: '旧说明' } } as Partial<BlockView>)
const after = textView([seg('调用 g')], { table: { columns: ['实参'], items: [item('x')] } } as Partial<BlockView>)
const marks = blockMarks(before, after)
expect(marks?.translation).toBeDefined()
expect(marks?.table?.changed).toEqual({ 0: { added: true } })
expect(marks?.doc).toEqual({ markdown: [{ text: '旧说明', change: 'del' }] })
})
it('一块是属性表、一块是一行译文时没法逐行比', () => {
expect(blockMarks(textView([seg('a')]), propertyView([row('名称', [seg('a')])]))).toBeUndefined()
})
it('内容一样时没有标记', () => {
expect(blockMarks(propertyView([row('名称', [seg('a', 'variable', 1)])]), propertyView([row('名称', [seg('a', 'variable', 7)])]))).toBeUndefined()
})
})
describe('removedView / withoutSource', () => {
it('删掉的块只带自己的内容,词不带源码下标,没有名字的范围', () => {
const view = propertyView([row('名称', [seg('f', 'function', 4)], [{ cells: [[{ ...seg('x', 'variable', 9), refers: { startIndex: 1, endIndex: 2 } }]] }])])
const removed = removedView({ ...view, defines: { startIndex: 4, endIndex: 5 }, children: [textView([seg('child', 'plain', 1)])] })
expect(removed.children).toEqual([])
expect(removed.defines).toBeUndefined()
expect(JSON.stringify(removed)).not.toMatch(/"offset"|"refers"/)
})
it('框里的词也去掉下标,颜色留着', () => {
const color = { dark: '#fff', light: '#000' }
expect(withoutSource([{ style: 'iteration', label: '列表', segments: [{ ...seg('x', 'variable', 3), color }] }])).toEqual([
{ style: 'iteration', label: '列表', segments: [{ text: 'x', role: 'variable', color }] },
])
})
})