neovim/test/unit/marktree_spec.lua
Björn Linse 55677ddc46 Add new marktree data structure for storing marks
This is inspired by Atom's "marker index" data structure to efficiently
adjust marks to text insertions deletions, but uses a wide B-tree
(derived from kbtree) to keep the nesting level down.
2020-01-14 12:57:31 +01:00

191 lines
5.7 KiB
Lua

local helpers = require("test.unit.helpers")(after_each)
local itp = helpers.gen_itp(it)
local ffi = helpers.ffi
local eq = helpers.eq
local ok = helpers.ok
local lib = helpers.cimport("./src/nvim/marktree.h")
local function tablelength(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
local function pos_leq(a, b)
return a[1] < b[1] or (a[1] == b[1] and a[2] <= b[2])
end
-- Checks that shadow and tree is consistent, and optionally
-- return the order
local function shadoworder(tree, shadow, iter, giveorder)
ok(iter ~= nil)
local status = lib.marktree_itr_first(tree, iter)
local count = 0
local pos2id, id2pos = {}, {}
local last
if not status and next(shadow) == nil then
return pos2id, id2pos
end
repeat
local mark = lib.marktree_itr_current(iter)
local id = tonumber(mark.id)
local spos = shadow[id]
if (mark.row ~= spos[1] or mark.col ~= spos[2]) then
error("invalid pos for "..id..":("..mark.row..", "..mark.col..") instead of ("..spos[1]..", "..spos[2]..")")
end
if mark.right_gravity ~= spos[3] then
error("invalid gravity for "..id..":("..mark.row..", "..mark.col..")")
end
if count > 0 then
if not pos_leq(last, spos) then
error("DISORDER")
end
end
count = count + 1
last = spos
if giveorder then
pos2id[count] = id
id2pos[id] = count
end
until not lib.marktree_itr_next(tree, iter)
local shadowlen = tablelength(shadow)
if shadowlen ~= count then
error("missed some keys? (shadow "..shadowlen..", tree "..count..")")
end
return id2pos, pos2id
end
local function shadowsplice(shadow, start, old_extent, new_extent)
local old_end = {start[1] + old_extent[1],
(old_extent[1] == 0 and start[2] or 0) + old_extent[2]}
local new_end = {start[1] + new_extent[1],
(new_extent[1] == 0 and start[2] or 0) + new_extent[2]}
local delta = {new_end[1] - old_end[1], new_end[2] - old_end[2]}
for _, pos in pairs(shadow) do
if pos_leq(start, pos) then
if pos_leq(pos, old_end) then
-- delete region
if pos[3] then -- right gravity
pos[1], pos[2] = new_end[1], new_end[2]
else
pos[1], pos[2] = start[1], start[2]
end
else
if pos[1] == old_end[1] then
pos[2] = pos[2] + delta[2]
end
pos[1] = pos[1] + delta[1]
end
end
end
end
local function dosplice(tree, shadow, start, old_extent, new_extent)
lib.marktree_splice(tree, start[1], start[2], old_extent[1], old_extent[2], new_extent[1], new_extent[2])
shadowsplice(shadow, start, old_extent, new_extent)
end
describe('marktree', function()
itp('works', function()
local tree = ffi.new("MarkTree[1]") -- zero initialized by luajit
local shadow = {}
local iter = ffi.new("MarkTreeIter[1]")
local iter2 = ffi.new("MarkTreeIter[1]")
for i = 1,100 do
for j = 1,100 do
local gravitate = (i%2) > 0
local id = tonumber(lib.marktree_put(tree, j, i, gravitate))
ok(id > 0)
eq(nil, shadow[id])
shadow[id] = {j,i,gravitate}
end
-- checking every insert is too slow, but this is ok
lib.marktree_check(tree)
end
-- ss = lib.mt_inspect_rec(tree)
-- io.stdout:write(ffi.string(ss))
-- io.stdout:flush()
local id2pos, pos2id = shadoworder(tree, shadow, iter)
eq({}, pos2id) -- not set if not requested
eq({}, id2pos)
for i,ipos in pairs(shadow) do
local pos = lib.marktree_lookup(tree, i, iter)
eq(ipos[1], pos.row)
eq(ipos[2], pos.col)
local k = lib.marktree_itr_current(iter)
eq(ipos[1], k.row)
eq(ipos[2], k.col, ipos[1])
lib.marktree_itr_next(tree, iter)
-- TODO(bfredl): use id2pos to check neighbour?
-- local k2 = lib.marktree_itr_current(iter)
end
for i,ipos in pairs(shadow) do
lib.marktree_itr_get(tree, ipos[1], ipos[2], iter)
local k = lib.marktree_itr_current(iter)
eq(i, tonumber(k.id))
eq(ipos[1], k.row)
eq(ipos[2], k.col)
end
ok(lib.marktree_itr_first(tree, iter))
local del = lib.marktree_itr_current(iter)
lib.marktree_del_itr(tree, iter, false)
shadow[tonumber(del.id)] = nil
shadoworder(tree, shadow, iter)
for _, ci in ipairs({0,-1,1,-2,2,-10,10}) do
for i = 1,100 do
lib.marktree_itr_get(tree, i, 50+ci, iter)
local k = lib.marktree_itr_current(iter)
local id = tonumber(k.id)
eq(shadow[id][1], k.row)
eq(shadow[id][2], k.col)
lib.marktree_del_itr(tree, iter, false)
shadow[id] = nil
end
lib.marktree_check(tree)
shadoworder(tree, shadow, iter)
end
-- NB: this is quite rudimentary. We rely on
-- functional tests exercising splicing quite a bit
lib.marktree_check(tree)
dosplice(tree, shadow, {2,2}, {0,5}, {1, 2})
lib.marktree_check(tree)
shadoworder(tree, shadow, iter)
dosplice(tree, shadow, {30,2}, {30,5}, {1, 2})
lib.marktree_check(tree)
shadoworder(tree, shadow, iter)
dosplice(tree, shadow, {5,3}, {0,2}, {0, 5})
shadoworder(tree, shadow, iter)
lib.marktree_check(tree)
-- build then burn (HOORAY! HOORAY!)
while next(shadow) do
lib.marktree_itr_first(tree, iter)
-- delete every other key for fun and profit
while true do
local k = lib.marktree_itr_current(iter)
lib.marktree_del_itr(tree, iter, false)
ok(shadow[tonumber(k.id)] ~= nil)
shadow[tonumber(k.id)] = nil
local stat = lib.marktree_itr_next(tree, iter)
if not stat then
break
end
end
lib.marktree_check(tree)
shadoworder(tree, shadow, iter2)
end
end)
end)