✨ feat(vscode): 代码阅读预览按侧栏筛选算代码差异(未提交的更改对比上一次提交、审查对比 PR 目标分支;兄弟块按名字 / 相似度对齐,卡片内按行 / 值 / 词比对),算好后补发给预览
This commit is contained in:
1 parent
3db44f84ae
commit
2a5806c6b5
23 files changed
+1527
-31
No files matched your search
@@ -0,0 +1,234 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { SourcedBlocks } from './blockDiff'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readBlockViews } from '../blockView'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { runtime, WASM_DIR } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
import { alignSiblings, diffBlockTrees, similarity } from './blockDiff'
|
||||
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
async function python(source: string): Promise<SourcedBlocks> {
|
||||
const { blocks } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source)
|
||||
return { blocks, source }
|
||||
}
|
||||
|
||||
/** 手写的一块:只填比对用得到的几样。 */
|
||||
function view(anchor: string, kind: string, code: string, extra: Partial<BlockView> = {}): BlockView {
|
||||
return {
|
||||
id: anchor,
|
||||
anchor,
|
||||
kind,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
headerEndLine: 0,
|
||||
code,
|
||||
codeTokens: [],
|
||||
attributes: { kind },
|
||||
children: [],
|
||||
translation: [{ text: code, role: 'plain' }],
|
||||
...extra,
|
||||
} as BlockView
|
||||
}
|
||||
|
||||
/** 名字写在源码开头:defines 指向它。 */
|
||||
function named(anchor: string, kind: string, name: string, code: string, source: { text: string }): BlockView {
|
||||
const startIndex = source.text.length
|
||||
source.text += name
|
||||
return view(anchor, kind, code, { defines: { startIndex, endIndex: startIndex + name.length } })
|
||||
}
|
||||
|
||||
const RELEASE_LUA = `_RELEASE_LUA = """
|
||||
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('DEL', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
"""`
|
||||
|
||||
const BEFORE = `"""Redis 互斥锁: SET NX EX 抢锁, Lua 比对 token 再删."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
${RELEASE_LUA}
|
||||
|
||||
|
||||
async def try_acquire(redis: Any, key: str, ttl_sec: int) -> bool:
|
||||
"""抢锁; 抢到返回持有者 token, 已被占用返回 None."""
|
||||
token = uuid.uuid4().hex
|
||||
ok = await redis.set(key, token, nx=True, ex=ttl_sec)
|
||||
return token if ok else None
|
||||
|
||||
|
||||
async def _is_owner(redis: Any, key: str, token: str) -> bool:
|
||||
"""锁仍归 token 时返回真."""
|
||||
return await redis.get(key) == token
|
||||
|
||||
|
||||
async def release(redis: Any, key: str, token: str) -> bool:
|
||||
"""只在锁仍归 token 时删掉; 返回是否真的删了."""
|
||||
result = await redis.eval(_RELEASE_LUA, 1, key)
|
||||
return int(result) == 1
|
||||
`
|
||||
|
||||
const AFTER = `"""Redis 互斥锁: SET NX EX 抢锁, Lua 比对 token 再续期 / 再删."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
${RELEASE_LUA}
|
||||
|
||||
_RENEW_LUA = """
|
||||
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('EXPIRE', KEYS[1], ARGV[2])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
"""
|
||||
|
||||
|
||||
async def try_acquire(redis: Any, key: str, ttl_sec: int, token_prefix: str = "") -> str | None:
|
||||
"""抢锁; 抢到返回持有者 token, 已被占用返回 None."""
|
||||
token = f"{token_prefix}{uuid.uuid4().hex}"
|
||||
ok = await redis.set(key, token, nx=True, ex=ttl_sec)
|
||||
return token if ok else None
|
||||
|
||||
|
||||
async def renew(redis: Any, key: str, token: str, ttl_sec: int) -> bool:
|
||||
"""只在锁仍归 token 时把 TTL 续回 ttl_sec; 返回是否续上."""
|
||||
result = await redis.eval(_RENEW_LUA, 1, key, token, str(ttl_sec))
|
||||
return int(result) == 1
|
||||
|
||||
|
||||
async def release(redis: Any, key: str, token: str) -> bool:
|
||||
"""只在锁仍归 token 时删掉; 返回是否真的删了."""
|
||||
result = await redis.eval(_RELEASE_LUA, 1, key, token)
|
||||
return int(result) == 1
|
||||
`
|
||||
|
||||
describe('similarity', () => {
|
||||
it('一样是 1,毫不相干是 0,改了一个词在中间', () => {
|
||||
expect(similarity('a = 1', 'a = 1')).toBe(1)
|
||||
expect(similarity('foo(bar)', 'x')).toBe(0)
|
||||
const score = similarity('print(a, b)', 'print(a, c)')
|
||||
expect(score).toBeGreaterThan(0.5)
|
||||
expect(score).toBeLessThan(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('alignSiblings', () => {
|
||||
it('有名字的按种类 + 名字配,挪了位置也配得上;改名算一删一增', () => {
|
||||
const source = { text: '' }
|
||||
const before = [named('f', 'function', 'f', 'def f(): pass', source), named('g', 'function', 'g', 'def g(): pass', source)]
|
||||
const after = [named('g2', 'function', 'g', 'def g(): return 1', source), named('h', 'function', 'h', 'def f(): pass', source)]
|
||||
const { toOld, toNew } = alignSiblings(before, source.text, after, source.text)
|
||||
expect(toOld).toEqual([1, undefined])
|
||||
expect(toNew).toEqual([undefined, 0])
|
||||
})
|
||||
|
||||
it('同名的照先后配', () => {
|
||||
const source = { text: '' }
|
||||
const before = [named('a', 'function', 'f', 'one', source), named('b', 'function', 'f', 'two', source)]
|
||||
const after = [named('c', 'function', 'f', 'one', source), named('d', 'function', 'f', 'two!', source)]
|
||||
expect(alignSiblings(before, source.text, after, source.text).toOld).toEqual([0, 1])
|
||||
})
|
||||
|
||||
it('种类不同的同名块不配', () => {
|
||||
const source = { text: '' }
|
||||
const before = [named('a', 'const', 'x', 'const x = 1', source)]
|
||||
const after = [named('b', 'variable', 'x', 'let x = 1', source)]
|
||||
expect(alignSiblings(before, source.text, after, source.text).toOld).toEqual([undefined])
|
||||
})
|
||||
|
||||
it('没名字的:源码没变的按最长公共子序列配,中间的按相似度配成修改,不像的算一删一增', () => {
|
||||
const before = [view('1', 'statement', 'print(a)'), view('2', 'statement', 'call(alpha, beta, gamma)'), view('3', 'statement', 'print(b)')]
|
||||
const after = [
|
||||
view('1', 'statement', 'print(a)'),
|
||||
view('x', 'statement', 'totally_new()'),
|
||||
view('2', 'statement', 'call(alpha, beta, delta)'),
|
||||
view('3', 'statement', 'print(b)'),
|
||||
]
|
||||
expect(alignSiblings(before, '', after, '').toOld).toEqual([0, undefined, 1, 2])
|
||||
})
|
||||
|
||||
it('没名字的块种类不同就不按相似度配', () => {
|
||||
const before = [view('1', 'statement', 'call(alpha, beta)')]
|
||||
const after = [view('2', 'return', 'call(alpha, beta, gamma)')]
|
||||
expect(alignSiblings(before, '', after, '').toOld).toEqual([undefined])
|
||||
})
|
||||
|
||||
it('有名字的块不按相似度配:改名算一删一增', () => {
|
||||
const source = { text: '' }
|
||||
const before = [named('a', 'function', 'old_name', 'def old_name(x): return x + 1', source)]
|
||||
const after = [named('b', 'function', 'new_name', 'def new_name(x): return x + 1', source)]
|
||||
expect(alignSiblings(before, source.text, after, source.text).toOld).toEqual([undefined])
|
||||
})
|
||||
})
|
||||
|
||||
describe('diffBlockTrees', () => {
|
||||
it('原型的 mutex.py:新增 2 · 修改 3 · 删除 1,只数最外层', async () => {
|
||||
const diff = diffBlockTrees(await python(BEFORE), await python(AFTER))
|
||||
expect(diff.stats).toEqual({ add: 2, mod: 3, del: 1 })
|
||||
})
|
||||
|
||||
it('新增的块连同子孙都算新增,只有最外面那块挂小标', async () => {
|
||||
const after = await python(AFTER)
|
||||
const diff = diffBlockTrees(await python(BEFORE), after)
|
||||
const renew = after.blocks.find(block => block.anchor === 'function renew')!
|
||||
expect(diff.blocks[renew.anchor]).toEqual({ change: 'add', outermost: true })
|
||||
for (const child of renew.children)
|
||||
expect(diff.blocks[child.anchor]).toEqual({ change: 'add' })
|
||||
})
|
||||
|
||||
it('修改的块:自己的内容改了带标记,子块照样比;只有子块改了的父块不算自己改了', async () => {
|
||||
const after = await python(AFTER)
|
||||
const diff = diffBlockTrees(await python(BEFORE), after)
|
||||
const tryAcquire = after.blocks.find(block => block.anchor === 'function try_acquire')!
|
||||
expect(diff.blocks[tryAcquire.anchor]).toMatchObject({ change: 'mod', own: true })
|
||||
expect(diff.blocks[tryAcquire.anchor].marks?.rows).toBeDefined()
|
||||
const [token, ok, back] = tryAcquire.children
|
||||
expect(diff.blocks[token.anchor]).toMatchObject({ change: 'mod', own: true })
|
||||
expect(diff.blocks[ok.anchor]).toBeUndefined()
|
||||
expect(diff.blocks[back.anchor]).toBeUndefined()
|
||||
|
||||
const release = after.blocks.find(block => block.anchor === 'function release')!
|
||||
expect(diff.blocks[release.anchor]).toEqual({ change: 'mod' })
|
||||
expect(diff.blocks[release.children[0].anchor]).toMatchObject({ change: 'mod', own: true })
|
||||
})
|
||||
|
||||
it('删掉的块放回原来的位置:前面配上的兄弟之后,再越过紧跟着的新增块;只带它自己的内容', async () => {
|
||||
const after = await python(AFTER)
|
||||
const diff = diffBlockTrees(await python(BEFORE), after)
|
||||
expect(diff.removed).toHaveLength(1)
|
||||
const [removed] = diff.removed
|
||||
expect(removed).toMatchObject({ parent: '', after: 'function renew' })
|
||||
expect(removed.block.anchor).toBe('function _is_owner')
|
||||
expect(removed.block.children).toEqual([])
|
||||
expect(removed.block.defines).toBeUndefined()
|
||||
const renew = after.blocks.find(block => block.anchor === 'function renew')!
|
||||
expect(removed.line).toBe(renew.endLine + 1)
|
||||
const names = JSON.stringify(removed.block)
|
||||
expect(names).not.toContain('"offset"')
|
||||
})
|
||||
|
||||
it('没变的文件没有改动', async () => {
|
||||
const before = await python(BEFORE)
|
||||
expect(diffBlockTrees(before, await python(BEFORE))).toEqual({ blocks: {}, removed: [], stats: { add: 0, mod: 0, del: 0 } })
|
||||
})
|
||||
|
||||
it('子块里删掉的块挂在父块下面,排在最前面时没有 after', async () => {
|
||||
const before = await python('def f():\n a = 1\n b = 2\n')
|
||||
const after = await python('def f():\n b = 2\n')
|
||||
const diff = diffBlockTrees(before, after)
|
||||
expect(diff.stats).toEqual({ add: 0, mod: 1, del: 0 })
|
||||
expect(diff.removed).toHaveLength(1)
|
||||
expect(diff.removed[0]).toMatchObject({ parent: 'function f', line: 1 })
|
||||
expect(diff.removed[0].after).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user