fix: handle concurrent updates from multiple sessions
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
# loc.nvim
|
# loc.nvim
|
||||||
|
|
||||||
> Entirely GPT 5.5 generated
|
|
||||||
|
|
||||||
A tiny Neovim plugin that tracks characters added and deleted while you are in
|
A tiny Neovim plugin that tracks characters added and deleted while you are in
|
||||||
insert mode, then displays estimated lines of code by dividing characters by 35.
|
insert mode, then displays estimated lines of code by dividing characters by 35.
|
||||||
|
|
||||||
@@ -82,9 +80,8 @@ Defaults:
|
|||||||
```lua
|
```lua
|
||||||
require("loc").setup({
|
require("loc").setup({
|
||||||
auto_enable = true,
|
auto_enable = true,
|
||||||
persist = true,
|
|
||||||
data_path = nil,
|
data_path = nil,
|
||||||
save_delay_ms = 1000,
|
flush_interval_ms = 10000,
|
||||||
statusline_prefix = "LOC",
|
statusline_prefix = "LOC",
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -106,8 +103,7 @@ Stats are stored by local date:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
With `persist = false`, daily counters stay in memory only for the current
|
Local edits are applied immediately in the UI, then flushed into `stats.json` every `flush_interval_ms`.
|
||||||
Neovim process.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -121,6 +117,8 @@ require("loc").stats()
|
|||||||
require("loc").metrics()
|
require("loc").metrics()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`save()` flushes this process's pending deltas immediately.
|
||||||
|
|
||||||
`stats()` returns today's counters:
|
`stats()` returns today's counters:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|||||||
+302
-113
@@ -2,22 +2,19 @@ local M = {}
|
|||||||
|
|
||||||
local defaults = {
|
local defaults = {
|
||||||
auto_enable = true,
|
auto_enable = true,
|
||||||
persist = true,
|
|
||||||
data_path = nil,
|
data_path = nil,
|
||||||
save_delay_ms = 1000,
|
flush_interval_ms = 10000,
|
||||||
statusline_prefix = "LOC",
|
statusline_prefix = "LOC",
|
||||||
}
|
}
|
||||||
|
|
||||||
local state = {
|
local state = {
|
||||||
config = nil,
|
config = nil,
|
||||||
loaded = false,
|
|
||||||
loaded_path = nil,
|
|
||||||
enabled = false,
|
enabled = false,
|
||||||
augroup = nil,
|
augroup = nil,
|
||||||
snapshots = {},
|
snapshots = {},
|
||||||
save_pending = false,
|
base_days = {},
|
||||||
dirty = false,
|
pending_days = {},
|
||||||
days = {},
|
flush_timer = nil,
|
||||||
metrics_render = 0,
|
metrics_render = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +47,9 @@ local metrics_cell_width = 7
|
|||||||
local metrics_cell_height = 3
|
local metrics_cell_height = 3
|
||||||
local metrics_column_gap = 2
|
local metrics_column_gap = 2
|
||||||
local metrics_row_gap = 1
|
local metrics_row_gap = 1
|
||||||
|
local uv = vim.uv or vim.loop
|
||||||
|
local lock_retry_count = 40
|
||||||
|
local lock_retry_delay_ms = 50
|
||||||
|
|
||||||
local function data_path()
|
local function data_path()
|
||||||
return (state.config and state.config.data_path)
|
return (state.config and state.config.data_path)
|
||||||
@@ -63,9 +63,194 @@ local function today_key()
|
|||||||
return vim.fn.strftime("%Y-%m-%d")
|
return vim.fn.strftime("%Y-%m-%d")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function day_stats(date)
|
local function redraw_statusline()
|
||||||
state.days[date] = state.days[date] or { added = 0, deleted = 0 }
|
vim.schedule(function()
|
||||||
return state.days[date]
|
vim.cmd.redrawstatus()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function copy_days(days)
|
||||||
|
local copy = {}
|
||||||
|
|
||||||
|
for date, stats in pairs(days or {}) do
|
||||||
|
if type(date) == "string" and type(stats) == "table" then
|
||||||
|
copy[date] = {
|
||||||
|
added = tonumber(stats.added) or 0,
|
||||||
|
deleted = tonumber(stats.deleted) or 0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_day(days, date)
|
||||||
|
days[date] = days[date] or { added = 0, deleted = 0 }
|
||||||
|
return days[date]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function merged_day_stats(date)
|
||||||
|
local base = state.base_days[date]
|
||||||
|
local pending = state.pending_days[date]
|
||||||
|
local added = (tonumber(base and base.added) or 0) + (tonumber(pending and pending.added) or 0)
|
||||||
|
local deleted = (tonumber(base and base.deleted) or 0) + (tonumber(pending and pending.deleted) or 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
added = added,
|
||||||
|
deleted = deleted,
|
||||||
|
net = added - deleted,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_pending_days()
|
||||||
|
return next(state.pending_days) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function merge_days(base_days, pending_days)
|
||||||
|
local merged = copy_days(base_days)
|
||||||
|
|
||||||
|
for date, stats in pairs(pending_days or {}) do
|
||||||
|
local day = ensure_day(merged, date)
|
||||||
|
|
||||||
|
day.added = day.added + (tonumber(stats.added) or 0)
|
||||||
|
day.deleted = day.deleted + (tonumber(stats.deleted) or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return merged
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stats_lock_path(path)
|
||||||
|
return path .. ".lock"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lock_info_path(lock_path)
|
||||||
|
return lock_path .. "/info.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stale_lock_age_seconds()
|
||||||
|
local interval = (state.config and state.config.flush_interval_ms) or defaults.flush_interval_ms
|
||||||
|
return math.max(math.ceil(interval / 1000) * 6, 30)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalize_days(days)
|
||||||
|
return copy_days(days)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_days_from_disk(path)
|
||||||
|
if vim.fn.filereadable(path) ~= 1 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, parsed = pcall(function()
|
||||||
|
return json_decode(table.concat(vim.fn.readfile(path), "\n"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not ok or type(parsed) ~= "table" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
return normalize_days(parsed)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reload_base_days()
|
||||||
|
state.base_days = read_days_from_disk(data_path())
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_days_to_disk(path, days)
|
||||||
|
local dir = vim.fn.fnamemodify(path, ":h")
|
||||||
|
local temp_path = string.format("%s.tmp.%d.%d", path, vim.fn.getpid(), os.time())
|
||||||
|
local payload = json_encode(days)
|
||||||
|
|
||||||
|
vim.fn.mkdir(dir, "p")
|
||||||
|
vim.fn.writefile({ payload }, temp_path)
|
||||||
|
|
||||||
|
local ok, err = uv.fs_rename(temp_path, path)
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
vim.fn.delete(temp_path)
|
||||||
|
error(err or "failed to rename stats file")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_lock_timestamp(lock_path)
|
||||||
|
local info_path = lock_info_path(lock_path)
|
||||||
|
|
||||||
|
if vim.fn.filereadable(info_path) == 1 then
|
||||||
|
local ok, parsed = pcall(function()
|
||||||
|
return json_decode(table.concat(vim.fn.readfile(info_path), "\n"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
if ok and type(parsed) == "table" and type(parsed.timestamp) == "number" then
|
||||||
|
return parsed.timestamp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local stat = uv.fs_stat(lock_path)
|
||||||
|
|
||||||
|
if stat and stat.mtime and type(stat.mtime.sec) == "number" then
|
||||||
|
return stat.mtime.sec
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lock_is_stale(lock_path)
|
||||||
|
local timestamp = read_lock_timestamp(lock_path)
|
||||||
|
|
||||||
|
if not timestamp then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return os.difftime(os.time(), timestamp) >= stale_lock_age_seconds()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function release_lock(lock_path)
|
||||||
|
pcall(vim.fn.delete, lock_path, "rf")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_lock_metadata(lock_path)
|
||||||
|
local metadata = {
|
||||||
|
pid = vim.fn.getpid(),
|
||||||
|
timestamp = os.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
pcall(vim.fn.writefile, { json_encode(metadata) }, lock_info_path(lock_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function with_stats_lock(fn)
|
||||||
|
local path = data_path()
|
||||||
|
local dir = vim.fn.fnamemodify(path, ":h")
|
||||||
|
local lock_path = stats_lock_path(path)
|
||||||
|
|
||||||
|
vim.fn.mkdir(dir, "p")
|
||||||
|
|
||||||
|
for _ = 1, lock_retry_count do
|
||||||
|
local ok, created = pcall(vim.fn.mkdir, lock_path)
|
||||||
|
|
||||||
|
if ok and created == 1 then
|
||||||
|
write_lock_metadata(lock_path)
|
||||||
|
|
||||||
|
local success, result, extra = xpcall(function()
|
||||||
|
return fn(path)
|
||||||
|
end, debug.traceback)
|
||||||
|
|
||||||
|
release_lock(lock_path)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
return false, result
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, result, extra
|
||||||
|
end
|
||||||
|
|
||||||
|
if lock_is_stale(lock_path) then
|
||||||
|
release_lock(lock_path)
|
||||||
|
else
|
||||||
|
vim.wait(lock_retry_delay_ms, function() return false end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false, string.format("failed to acquire stats lock: %s", lock_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function char_count(value)
|
local function char_count(value)
|
||||||
@@ -158,46 +343,11 @@ local function apply_change(added, deleted)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local day = day_stats(today_key())
|
local day = ensure_day(state.pending_days, today_key())
|
||||||
|
|
||||||
day.added = day.added + added
|
day.added = day.added + added
|
||||||
day.deleted = day.deleted + deleted
|
day.deleted = day.deleted + deleted
|
||||||
state.dirty = true
|
redraw_statusline()
|
||||||
|
|
||||||
vim.schedule(function()
|
|
||||||
vim.cmd.redrawstatus()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function save_now()
|
|
||||||
if not state.config or not state.config.persist or not state.dirty then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local path = data_path()
|
|
||||||
local payload = json_encode(state.days)
|
|
||||||
|
|
||||||
local ok = pcall(function()
|
|
||||||
vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p")
|
|
||||||
vim.fn.writefile({ payload }, path)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if ok then
|
|
||||||
state.dirty = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function schedule_save()
|
|
||||||
if not state.config or not state.config.persist or state.save_pending then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
state.save_pending = true
|
|
||||||
|
|
||||||
vim.defer_fn(function()
|
|
||||||
state.save_pending = false
|
|
||||||
save_now()
|
|
||||||
end, state.config.save_delay_ms)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_snapshot(bufnr)
|
local function update_snapshot(bufnr)
|
||||||
@@ -212,7 +362,6 @@ local function update_snapshot(bufnr)
|
|||||||
if previous then
|
if previous then
|
||||||
local added, deleted = measure_change(previous, current)
|
local added, deleted = measure_change(previous, current)
|
||||||
apply_change(added, deleted)
|
apply_change(added, deleted)
|
||||||
schedule_save()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
state.snapshots[bufnr] = current
|
state.snapshots[bufnr] = current
|
||||||
@@ -331,20 +480,7 @@ local function metrics_display_date(epoch)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function stored_day_stats(date)
|
local function stored_day_stats(date)
|
||||||
local day = state.days[date]
|
return merged_day_stats(date)
|
||||||
|
|
||||||
if type(day) ~= "table" then
|
|
||||||
return { added = 0, deleted = 0, net = 0 }
|
|
||||||
end
|
|
||||||
|
|
||||||
local added = tonumber(day.added) or 0
|
|
||||||
local deleted = tonumber(day.deleted) or 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
added = added,
|
|
||||||
deleted = deleted,
|
|
||||||
net = added - deleted,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function display_day_stats(stats)
|
local function display_day_stats(stats)
|
||||||
@@ -600,47 +736,77 @@ local function apply_metrics_highlights(bufnr, cells, entries, max_abs_net)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function load_stats()
|
local function flush_pending_now()
|
||||||
local path = data_path()
|
if not state.config or not has_pending_days() then
|
||||||
|
return true
|
||||||
if state.loaded and state.loaded_path == path then
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if not state.config.persist then
|
local flushed = copy_days(state.pending_days)
|
||||||
state.days = {}
|
local ok, result = with_stats_lock(function(path)
|
||||||
state.loaded = true
|
local merged = merge_days(read_days_from_disk(path), flushed)
|
||||||
state.loaded_path = path
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if vim.fn.filereadable(path) ~= 1 then
|
write_days_to_disk(path, merged)
|
||||||
state.days = {}
|
return merged
|
||||||
state.loaded = true
|
|
||||||
state.loaded_path = path
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, parsed = pcall(function()
|
|
||||||
return json_decode(table.concat(vim.fn.readfile(path), "\n"))
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
state.days = {}
|
if not ok then
|
||||||
state.dirty = false
|
return false, result
|
||||||
|
|
||||||
if ok and type(parsed) == "table" then
|
|
||||||
for date, stats in pairs(parsed) do
|
|
||||||
if type(date) == "string" and type(stats) == "table" then
|
|
||||||
state.days[date] = {
|
|
||||||
added = tonumber(stats.added) or 0,
|
|
||||||
deleted = tonumber(stats.deleted) or 0,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
state.loaded = true
|
reload_base_days()
|
||||||
state.loaded_path = path
|
state.pending_days = {}
|
||||||
|
redraw_statusline()
|
||||||
|
return true, result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function periodic_sync()
|
||||||
|
if not state.config then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if has_pending_days() then
|
||||||
|
local ok = flush_pending_now()
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
reload_base_days()
|
||||||
|
redraw_statusline()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stop_flush_timer()
|
||||||
|
if not state.flush_timer then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(function()
|
||||||
|
state.flush_timer:stop()
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not state.flush_timer:is_closing() then
|
||||||
|
state.flush_timer:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
state.flush_timer = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function start_flush_timer()
|
||||||
|
stop_flush_timer()
|
||||||
|
|
||||||
|
if not state.enabled or not state.config then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
state.flush_timer = assert(uv.new_timer())
|
||||||
|
state.flush_timer:start(
|
||||||
|
state.config.flush_interval_ms,
|
||||||
|
state.config.flush_interval_ms,
|
||||||
|
vim.schedule_wrap(periodic_sync)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensure_setup()
|
local function ensure_setup()
|
||||||
@@ -653,15 +819,27 @@ end
|
|||||||
|
|
||||||
function M.setup(opts)
|
function M.setup(opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
|
local was_enabled = state.enabled
|
||||||
|
|
||||||
|
if state.config then
|
||||||
|
flush_pending_now()
|
||||||
|
stop_flush_timer()
|
||||||
|
end
|
||||||
|
|
||||||
state.config = vim.tbl_deep_extend("force", defaults, opts)
|
state.config = vim.tbl_deep_extend("force", defaults, opts)
|
||||||
|
state.config.flush_interval_ms = math.max(1, tonumber(state.config.flush_interval_ms) or defaults.flush_interval_ms)
|
||||||
|
|
||||||
if opts.data_path == nil then
|
if opts.data_path == nil then
|
||||||
state.config.data_path = nil
|
state.config.data_path = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
load_stats()
|
state.base_days = {}
|
||||||
|
state.pending_days = {}
|
||||||
|
reload_base_days()
|
||||||
|
|
||||||
if state.config.auto_enable then
|
if was_enabled then
|
||||||
|
start_flush_timer()
|
||||||
|
elseif state.config.auto_enable then
|
||||||
M.enable()
|
M.enable()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -697,9 +875,17 @@ function M.enable()
|
|||||||
callback = function(args)
|
callback = function(args)
|
||||||
update_snapshot(args.buf)
|
update_snapshot(args.buf)
|
||||||
state.snapshots[args.buf] = nil
|
state.snapshots[args.buf] = nil
|
||||||
save_now()
|
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||||
|
group = state.augroup,
|
||||||
|
callback = function()
|
||||||
|
flush_pending_now()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
start_flush_timer()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.disable()
|
function M.disable()
|
||||||
@@ -707,6 +893,8 @@ function M.disable()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
stop_flush_timer()
|
||||||
|
|
||||||
if state.augroup then
|
if state.augroup then
|
||||||
pcall(vim.api.nvim_del_augroup_by_id, state.augroup)
|
pcall(vim.api.nvim_del_augroup_by_id, state.augroup)
|
||||||
end
|
end
|
||||||
@@ -714,31 +902,32 @@ function M.disable()
|
|||||||
state.enabled = false
|
state.enabled = false
|
||||||
state.augroup = nil
|
state.augroup = nil
|
||||||
state.snapshots = {}
|
state.snapshots = {}
|
||||||
save_now()
|
flush_pending_now()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.reset()
|
function M.reset()
|
||||||
ensure_setup()
|
ensure_setup()
|
||||||
|
|
||||||
local day = day_stats(today_key())
|
local today = today_key()
|
||||||
|
local ok = with_stats_lock(function(path)
|
||||||
|
local days = read_days_from_disk(path)
|
||||||
|
|
||||||
|
days[today] = { added = 0, deleted = 0 }
|
||||||
|
write_days_to_disk(path, days)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if ok then
|
||||||
|
reload_base_days()
|
||||||
|
state.pending_days = {}
|
||||||
|
end
|
||||||
|
|
||||||
day.added = 0
|
|
||||||
day.deleted = 0
|
|
||||||
state.dirty = true
|
|
||||||
save_now()
|
|
||||||
vim.cmd.redrawstatus()
|
vim.cmd.redrawstatus()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.stats()
|
function M.stats()
|
||||||
ensure_setup()
|
ensure_setup()
|
||||||
|
|
||||||
local day = day_stats(today_key())
|
return merged_day_stats(today_key())
|
||||||
|
|
||||||
return {
|
|
||||||
added = day.added,
|
|
||||||
deleted = day.deleted,
|
|
||||||
net = day.added - day.deleted,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.display_stats()
|
function M.display_stats()
|
||||||
@@ -784,7 +973,7 @@ end
|
|||||||
|
|
||||||
function M.save()
|
function M.save()
|
||||||
ensure_setup()
|
ensure_setup()
|
||||||
save_now()
|
flush_pending_now()
|
||||||
end
|
end
|
||||||
|
|
||||||
M._measure_change = measure_change
|
M._measure_change = measure_change
|
||||||
|
|||||||
+16
-6
@@ -63,7 +63,7 @@ write_json(daily_path, {
|
|||||||
[old_day] = { added = 4, deleted = 1 },
|
[old_day] = { added = 4, deleted = 1 },
|
||||||
})
|
})
|
||||||
|
|
||||||
loc.setup({ auto_enable = false, data_path = daily_path, save_delay_ms = 1 })
|
loc.setup({ auto_enable = false, data_path = daily_path, flush_interval_ms = 10000 })
|
||||||
|
|
||||||
local loaded_stats = loc.stats()
|
local loaded_stats = loc.stats()
|
||||||
assert_equal(loaded_stats.added, 7, "loaded daily added")
|
assert_equal(loaded_stats.added, 7, "loaded daily added")
|
||||||
@@ -85,7 +85,7 @@ assert_equal(daily[old_day].added, 4, "reset preserves old day added")
|
|||||||
assert_equal(daily[old_day].deleted, 1, "reset preserves old day deleted")
|
assert_equal(daily[old_day].deleted, 1, "reset preserves old day deleted")
|
||||||
|
|
||||||
local change_path = vim.fn.tempname()
|
local change_path = vim.fn.tempname()
|
||||||
loc.setup({ auto_enable = false, data_path = change_path, save_delay_ms = 1 })
|
loc.setup({ auto_enable = false, data_path = change_path, flush_interval_ms = 10000 })
|
||||||
loc.enable()
|
loc.enable()
|
||||||
|
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
@@ -161,7 +161,7 @@ write_json(metrics_path, {
|
|||||||
["2026-05-10"] = { added = 350, deleted = 0 },
|
["2026-05-10"] = { added = 350, deleted = 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
loc.setup({ auto_enable = false, data_path = metrics_path, save_delay_ms = 1 })
|
loc.setup({ auto_enable = false, data_path = metrics_path, flush_interval_ms = 10000 })
|
||||||
|
|
||||||
local entries, column_count, max_abs_net, total_net = loc._metrics_entries(metrics_epoch)
|
local entries, column_count, max_abs_net, total_net = loc._metrics_entries(metrics_epoch)
|
||||||
|
|
||||||
@@ -181,6 +181,16 @@ assert_equal(entries[28].loc_net, 0, "metrics future date loc net is zero")
|
|||||||
assert_equal(max_abs_net, 10, "metrics max abs net")
|
assert_equal(max_abs_net, 10, "metrics max abs net")
|
||||||
assert_equal(total_net, 14, "metrics total net")
|
assert_equal(total_net, 14, "metrics total net")
|
||||||
|
|
||||||
|
local render_path = vim.fn.tempname()
|
||||||
|
local render_dates = loc._metrics_dates()
|
||||||
|
write_json(render_path, {
|
||||||
|
[render_dates[1].date] = { added = 245, deleted = 0 },
|
||||||
|
[render_dates[10].date] = { added = 0, deleted = 105 },
|
||||||
|
[today] = { added = 350, deleted = 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
loc.setup({ auto_enable = false, data_path = render_path, flush_interval_ms = 10000 })
|
||||||
|
|
||||||
local render = loc.metrics()
|
local render = loc.metrics()
|
||||||
assert_equal(vim.api.nvim_win_is_valid(render.winid), true, "metrics window is valid")
|
assert_equal(vim.api.nvim_win_is_valid(render.winid), true, "metrics window is valid")
|
||||||
assert_equal(vim.api.nvim_buf_is_valid(render.bufnr), true, "metrics buffer is valid")
|
assert_equal(vim.api.nvim_buf_is_valid(render.bufnr), true, "metrics buffer is valid")
|
||||||
@@ -192,9 +202,9 @@ assert_equal(metrics_lines[2]:match("LOC metrics") ~= nil, true, "metrics title"
|
|||||||
assert_equal(metrics_lines[#metrics_lines - 1]:match("4%-week net: %+14") ~= nil, true, "metrics summary")
|
assert_equal(metrics_lines[#metrics_lines - 1]:match("4%-week net: %+14") ~= nil, true, "metrics summary")
|
||||||
assert_equal(#metrics_lines, 21, "metrics renders transposed three-line week rows with row gaps and outer padding")
|
assert_equal(#metrics_lines, 21, "metrics renders transposed three-line week rows with row gaps and outer padding")
|
||||||
assert_equal(rendered:match("%+7") ~= nil, true, "metrics renders net value")
|
assert_equal(rendered:match("%+7") ~= nil, true, "metrics renders net value")
|
||||||
assert_equal(rendered:match("04/19") ~= nil, true, "metrics renders date label")
|
assert_equal(rendered:match(loc._metrics_display_date(render_dates[1].epoch)) ~= nil, true, "metrics renders date label")
|
||||||
assert_equal(rendered:match("04/20") ~= nil, true, "metrics renders zero tile date label")
|
assert_equal(rendered:match(loc._metrics_display_date(render_dates[2].epoch)) ~= nil, true, "metrics renders zero tile date label")
|
||||||
assert_equal(rendered:match("05/16") ~= nil, true, "metrics renders future date label")
|
assert_equal(rendered:match(loc._metrics_display_date(render_dates[28].epoch)) ~= nil, true, "metrics renders future date label")
|
||||||
assert_equal(rendered:match("Sun") == nil, true, "metrics omits weekday labels")
|
assert_equal(rendered:match("Sun") == nil, true, "metrics omits weekday labels")
|
||||||
assert_equal(vim.wo[render.winid].winhl:match("LocMetricsNormal") ~= nil, true, "metrics window uses custom normal highlight")
|
assert_equal(vim.wo[render.winid].winhl:match("LocMetricsNormal") ~= nil, true, "metrics window uses custom normal highlight")
|
||||||
assert_equal(vim.wo[render.winid].winhl:match("FloatBorder") == nil, true, "metrics window does not set a border highlight")
|
assert_equal(vim.wo[render.winid].winhl:match("FloatBorder") == nil, true, "metrics window does not set a border highlight")
|
||||||
|
|||||||
Reference in New Issue
Block a user