备份存的是「改动前」,所以一条记录的 diff 两端是它自己和更新一份的 状态(最新一条对比当前文件)。行级 diff 先掐公共前后缀再做 LCS,一次 改动通常只碰几行,表撑得住;大面积不同时退化成整段删增。 合并窗做在服务端而不是前端防抖:普通保存(opts.merge)发现最新备份 还在窗内就不新建,连点删除共享一条历史、一步撤回。窗锚定在这一串的 第一下,不滚雪球;还原、导入、撤回重做不合并,各占一条。合并时没有 备份文件,重载失败的还原改用刚读出的原文写回。顺带消除了备份名只精 确到秒、同秒保存互相覆盖的隐患。
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
import { diffHunks, diffLines } from './diff.ts'
|
|
|
|
const lines = (rows: ReturnType<typeof diffLines>) => rows.map((r) => `${r.kind}:${r.text}`)
|
|
|
|
describe('行级 diff', () => {
|
|
it('相同的内容全是 same,折叠后没有片段', () => {
|
|
const rows = diffLines('a\nb\nc', 'a\nb\nc')
|
|
expect(rows.every((r) => r.kind === 'same')).toBe(true)
|
|
expect(diffHunks(rows)).toEqual([])
|
|
})
|
|
|
|
it('中间插入一行', () => {
|
|
expect(lines(diffLines('a\nc', 'a\nb\nc'))).toEqual(['same:a', 'add:b', 'same:c'])
|
|
})
|
|
|
|
it('中间删除一行', () => {
|
|
expect(lines(diffLines('a\nb\nc', 'a\nc'))).toEqual(['same:a', 'del:b', 'same:c'])
|
|
})
|
|
|
|
it('改一行等于删旧行加新行', () => {
|
|
expect(lines(diffLines('a\nx\nc', 'a\ny\nc'))).toEqual(['same:a', 'del:x', 'add:y', 'same:c'])
|
|
})
|
|
|
|
it('从空到有、从有到空', () => {
|
|
expect(lines(diffLines('', 'a'))).toEqual(['del:', 'add:a'])
|
|
expect(lines(diffLines('a\nb', ''))).toEqual(['del:a', 'del:b', 'add:'])
|
|
})
|
|
|
|
it('公共前后缀之外的多处改动都找得到', () => {
|
|
const before = ['p1', 'p2', 'x', 'm', 'y', 's1'].join('\n')
|
|
const after = ['p1', 'p2', 'x2', 'm', 'y2', 's1'].join('\n')
|
|
expect(lines(diffLines(before, after))).toEqual([
|
|
'same:p1',
|
|
'same:p2',
|
|
'del:x',
|
|
'add:x2',
|
|
'same:m',
|
|
'del:y',
|
|
'add:y2',
|
|
'same:s1',
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('折叠成片段', () => {
|
|
it('改动行带前后上下文,远处的未改动行不进片段', () => {
|
|
const rows = diffLines(
|
|
['1', '2', '3', '4', '5', '6', '7', '8', '9'].join('\n'),
|
|
['1', '2', '3', 'x', '5', '6', '7', '8', '9'].join('\n'),
|
|
)
|
|
const hunks = diffHunks(rows, 1)
|
|
expect(hunks).toHaveLength(1)
|
|
expect(hunks[0]!.map((r) => r.text)).toEqual(['3', '4', 'x', '5'])
|
|
})
|
|
|
|
it('相距很远的两处改动分成两个片段,挨得近就合并成一个', () => {
|
|
const before = ['a', '1', '2', '3', '4', '5', '6', '7', 'b'].join('\n')
|
|
const afterFar = ['a2', '1', '2', '3', '4', '5', '6', '7', 'b2'].join('\n')
|
|
expect(diffHunks(diffLines(before, afterFar), 1)).toHaveLength(2)
|
|
const afterNear = ['a2', '1', '2', '3', '4', '5', '6', '7b', 'b'].join('\n')
|
|
expect(diffHunks(diffLines(before, afterNear), 4)).toHaveLength(1)
|
|
})
|
|
})
|