feat: add :LocMetrics heatmap and refactor core for LOC reduction

This commit is contained in:
2026-05-10 23:10:20 +00:00
parent c36814ad06
commit eb7ffe0c9a
4 changed files with 580 additions and 54 deletions
+3 -1
View File
@@ -3,7 +3,7 @@
> Entirely GPT 5.5 generated > 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. insert mode, then displays estimated lines of code by dividing characters by 35.
It keeps daily totals: It keeps daily totals:
@@ -63,6 +63,7 @@ The plugin defines these commands when it is on your runtimepath:
- `:LocDisable` stops tracking. - `:LocDisable` stops tracking.
- `:LocReset` clears today's counters. - `:LocReset` clears today's counters.
- `:LocStats` prints today's counters. - `:LocStats` prints today's counters.
- `:LocMetrics` opens a floating 4-week net LOC heatmap.
Calling `require("loc").setup()` enables tracking by default. Use this if you Calling `require("loc").setup()` enables tracking by default. Use this if you
want to configure the plugin but start it manually: want to configure the plugin but start it manually:
@@ -114,6 +115,7 @@ require("loc").reset()
require("loc").save() require("loc").save()
require("loc").statusline() require("loc").statusline()
require("loc").stats() require("loc").stats()
require("loc").metrics()
``` ```
`stats()` returns today's counters: `stats()` returns today's counters:
+468 -49
View File
@@ -18,78 +18,72 @@ local state = {
save_pending = false, save_pending = false,
dirty = false, dirty = false,
days = {}, days = {},
metrics_render = 0,
} }
local metrics_color_stops = {
{ at = 0.00, color = "#f0b6c1" },
{ at = 0.35, color = "#eba1ae" },
{ at = 0.65, color = "#df617a" },
{ at = 1.00, color = "#b90f36" },
}
local metrics_zero_style = {
bg = "#252628",
value_fg = "#e5e39a",
date_fg = "#8f9096",
}
local metrics_low_text_style = {
value_fg = "#2b2529",
date_fg = "#58565b",
}
local metrics_high_text_style = {
value_fg = "#ffd6df",
date_fg = "#f0aebb",
}
local weekday_names = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }
local chars_per_loc = 35
local metrics_cell_width = 7
local metrics_cell_height = 3
local metrics_column_gap = 2
local metrics_row_gap = 1
local function data_path() local function data_path()
if state.config and state.config.data_path then return (state.config and state.config.data_path)
return state.config.data_path or table.concat({ vim.fn.stdpath("data"), "loc.nvim", "stats.json" }, "/")
end end
return table.concat({ vim.fn.stdpath("data"), "loc.nvim", "stats.json" }, "/") local json_encode = (vim.json and vim.json.encode) or vim.fn.json_encode
end local json_decode = (vim.json and vim.json.decode) or vim.fn.json_decode
local function json_encode(value)
if vim.json and vim.json.encode then
return vim.json.encode(value)
end
return vim.fn.json_encode(value)
end
local function json_decode(value)
if vim.json and vim.json.decode then
return vim.json.decode(value)
end
return vim.fn.json_decode(value)
end
local function today_key() 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 day_stats(date)
local day = state.days[date] state.days[date] = state.days[date] or { added = 0, deleted = 0 }
return state.days[date]
if not day then
day = { added = 0, deleted = 0 }
state.days[date] = day
end
return day
end end
local function char_count(value) local function char_count(value)
if value == "" then if value == "" then return 0 end
return 0
end
local ok, count = pcall(vim.fn.strchars, value) local ok, count = pcall(vim.fn.strchars, value)
if ok then return ok and count or #value
return count
end
return #value
end end
local function is_continuation_byte(byte) local function is_continuation_byte(byte)
return byte and byte >= 0x80 and byte <= 0xbf return byte and byte >= 0x80 and byte <= 0xbf
end end
local function has_char_boundary_after(value, byte_index)
if byte_index <= 0 or byte_index >= #value then
return true
end
return not is_continuation_byte(value:byte(byte_index + 1))
end
local function has_char_boundary_at(value, byte_index) local function has_char_boundary_at(value, byte_index)
if byte_index <= 1 or byte_index > #value then return byte_index <= 1 or byte_index > #value or not is_continuation_byte(value:byte(byte_index))
return true
end end
return not is_continuation_byte(value:byte(byte_index)) local function has_char_boundary_after(value, byte_index)
return has_char_boundary_at(value, byte_index + 1)
end end
local function common_prefix_bytes(old, new) local function common_prefix_bytes(old, new)
@@ -224,6 +218,388 @@ local function update_snapshot(bufnr)
state.snapshots[bufnr] = current state.snapshots[bufnr] = current
end end
local function clamp(value, min, max)
return math.max(min, math.min(max, value))
end
local function chars_to_loc(chars)
return (chars < 0 and -1 or 1) * math.floor((math.abs(chars) / chars_per_loc) + 0.5)
end
local function hex_to_rgb(color)
return {
r = tonumber(color:sub(2, 3), 16),
g = tonumber(color:sub(4, 5), 16),
b = tonumber(color:sub(6, 7), 16),
}
end
local function rgb_to_hex(rgb)
return string.format("#%02x%02x%02x", rgb.r, rgb.g, rgb.b)
end
local function lerp_number(start, finish, t)
return math.floor(start + ((finish - start) * t) + 0.5)
end
local function lerp_color(start, finish, t)
local from = hex_to_rgb(start)
local to = hex_to_rgb(finish)
return rgb_to_hex({
r = lerp_number(from.r, to.r, t),
g = lerp_number(from.g, to.g, t),
b = lerp_number(from.b, to.b, t),
})
end
local function metrics_color(t)
t = clamp(t or 0, 0, 1)
for index = 1, #metrics_color_stops - 1 do
local left = metrics_color_stops[index]
local right = metrics_color_stops[index + 1]
if t <= right.at then
local span = right.at - left.at
local local_t = span == 0 and 0 or (t - left.at) / span
return lerp_color(left.color, right.color, local_t)
end
end
return metrics_color_stops[#metrics_color_stops].color
end
local function metrics_tile_style(net, max_abs_net)
local abs_net = math.abs(net or 0)
if abs_net == 0 or max_abs_net == 0 then
return vim.tbl_extend("force", {}, metrics_zero_style)
end
local t = clamp(abs_net / max_abs_net, 0, 1)
local text_style = t >= 0.75 and metrics_high_text_style or metrics_low_text_style
return {
bg = metrics_color(t),
value_fg = text_style.value_fg,
date_fg = text_style.date_fg,
}
end
local function highlight_color(name, key)
local ok, highlight = pcall(vim.api.nvim_get_hl, 0, { name = name, link = false })
if not ok or type(highlight) ~= "table" or type(highlight[key]) ~= "number" then
return nil
end
return highlight[key]
end
local function resolve_metrics_background()
return {
bg = highlight_color("StatusLine", "bg") or highlight_color("Normal", "bg"),
fg = highlight_color("StatusLine", "fg") or highlight_color("Normal", "fg"),
}
end
local function metrics_dates(now)
now = now or vim.fn.localtime()
local dates = {}
local today_weekday = tonumber(vim.fn.strftime("%w", now))
local current_week_start = now - (today_weekday * 86400)
local range_start = current_week_start - (21 * 86400)
for offset = 0, 27 do
local epoch = range_start + (offset * 86400)
dates[#dates + 1] = {
date = vim.fn.strftime("%Y-%m-%d", epoch),
epoch = epoch,
weekday = tonumber(vim.fn.strftime("%w", epoch)),
}
end
return dates
end
local function metrics_display_date(epoch)
return vim.fn.strftime("%m/%d", epoch)
end
local function stored_day_stats(date)
local day = state.days[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
local function display_day_stats(stats)
local loc_added = chars_to_loc(stats.added)
local loc_deleted = chars_to_loc(stats.deleted)
local loc_net = chars_to_loc(stats.net)
return loc_added, loc_deleted, loc_net
end
local function collect_metrics_entries(now)
local dates = metrics_dates(now)
local entries = {}
local max_abs_net = 0
local total_net = 0
for index, item in ipairs(dates) do
local stats = stored_day_stats(item.date)
local loc_added, loc_deleted, loc_net = display_day_stats(stats)
local loc_abs_net = math.abs(loc_net)
local entry = {
index = index,
date = item.date,
display_date = metrics_display_date(item.epoch),
weekday = item.weekday,
column = math.floor((index - 1) / 7) + 1,
added = stats.added,
deleted = stats.deleted,
net = stats.net,
abs_net = math.abs(stats.net),
loc_added = loc_added,
loc_deleted = loc_deleted,
loc_net = loc_net,
loc_abs_net = loc_abs_net,
}
max_abs_net = math.max(max_abs_net, loc_abs_net)
total_net = total_net + loc_net
entries[#entries + 1] = entry
end
return entries, entries[#entries].column, max_abs_net, total_net
end
local function max_display_width(lines)
local width = 0
for _, line in ipairs(lines) do
width = math.max(width, vim.fn.strdisplaywidth(line))
end
return width
end
local function centered_text(value, width)
local display_width = vim.fn.strdisplaywidth(value)
if display_width >= width then return value end
local left = math.floor((width - display_width) / 2)
return string.rep(" ", left) .. value .. string.rep(" ", width - display_width - left)
end
local function clamped_metric_value(value)
value = clamp(value, -9999, 9999)
return (value > 0 and "+" or "") .. value
end
local function build_metrics_lines(column_count, total_net)
local horizontal_padding = 4
local vertical_padding = 1
local lines = {
"LOC metrics",
"",
}
local cells = {}
for week = 1, column_count do
local row_lines = {}
local row_cells = {}
for _ = 1, metrics_cell_height do
row_lines[#row_lines + 1] = ""
end
for weekday = 0, 6 do
local start_col = #row_lines[1]
for row = 1, metrics_cell_height do
row_lines[row] = row_lines[row] .. string.rep(" ", metrics_cell_width)
end
row_cells[weekday] = {
line = #lines,
value_line = #lines + 1,
date_line = #lines + 2,
start_col = start_col,
end_col = #row_lines[1],
}
if weekday < 6 then
local gap = string.rep(" ", metrics_column_gap)
for row = 1, metrics_cell_height do
row_lines[row] = row_lines[row] .. gap
end
end
end
cells[week] = row_cells
for _, line in ipairs(row_lines) do
lines[#lines + 1] = line
end
if week < column_count then
for _ = 1, metrics_row_gap do
lines[#lines + 1] = ""
end
end
end
lines[#lines + 1] = ""
lines[#lines + 1] = string.format("4-week net: %+d", total_net)
local padded_lines = {}
for _ = 1, vertical_padding do
padded_lines[#padded_lines + 1] = ""
end
for _, line in ipairs(lines) do
padded_lines[#padded_lines + 1] = string.rep(" ", horizontal_padding) .. line .. string.rep(" ", horizontal_padding)
end
for _ = 1, vertical_padding do
padded_lines[#padded_lines + 1] = ""
end
for _, row_cells in pairs(cells) do
for _, cell in pairs(row_cells) do
cell.line = cell.line + vertical_padding
cell.value_line = cell.value_line + vertical_padding
cell.date_line = cell.date_line + vertical_padding
cell.start_col = cell.start_col + horizontal_padding
cell.end_col = cell.end_col + horizontal_padding
end
end
return padded_lines, cells
end
local function set_metrics_keymaps(bufnr, winid)
local function close()
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_close(winid, true)
end
end
local opts = { buffer = bufnr, nowait = true, silent = true }
vim.keymap.set("n", "q", close, opts)
vim.keymap.set("n", "<Esc>", close, opts)
-- Disable scrolling and movement
local nop = function() end
local keys = {
"<PageUp>", "<PageDown>", "<ScrollWheelUp>", "<ScrollWheelDown>",
"<Up>", "<Down>", "<Left>", "<Right>", "k", "j", "h", "l",
"<C-f>", "<C-b>", "<C-d>", "<C-u>", "gg", "G"
}
for _, key in ipairs(keys) do
vim.keymap.set("n", key, nop, opts)
end
end
local function open_metrics_window(lines)
local width = max_display_width(lines)
local height = #lines
local columns = vim.o.columns
local rows = vim.o.lines
width = math.min(width, math.max(1, columns - 4))
height = math.min(height, math.max(1, rows - 4))
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].buftype = "nofile"
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].filetype = "locmetrics"
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.bo[bufnr].modifiable = false
local winid = vim.api.nvim_open_win(bufnr, true, {
relative = "editor",
width = width,
height = height,
row = math.max(0, math.floor((rows - height) / 2) - 1),
col = math.max(0, math.floor((columns - width) / 2)),
style = "minimal",
})
vim.wo[winid].wrap = false
vim.wo[winid].cursorline = false
vim.wo[winid].number = false
vim.wo[winid].relativenumber = false
vim.wo[winid].scrolloff = 0
vim.wo[winid].sidescrolloff = 0
set_metrics_keymaps(bufnr, winid)
return bufnr, winid
end
local function set_metrics_window_highlights(winid)
local normal_group = "LocMetricsNormal" .. state.metrics_render
vim.api.nvim_set_hl(0, normal_group, resolve_metrics_background())
vim.wo[winid].winhl = "Normal:" .. normal_group .. ",NormalFloat:" .. normal_group
end
local function apply_metrics_highlights(bufnr, cells, entries, max_abs_net)
local namespace = vim.api.nvim_create_namespace("loc_metrics")
local title_group = "LocMetricsTitle" .. state.metrics_render
vim.api.nvim_set_hl(0, title_group, { italic = true })
vim.api.nvim_buf_add_highlight(bufnr, namespace, title_group, 1, 4, 15)
for _, entry in ipairs(entries) do
local style = metrics_tile_style(entry.loc_net, max_abs_net)
local group = string.format("LocMetricsCell%d_%02d", state.metrics_render, entry.index)
local date_group = string.format("LocMetricsDate%d_%02d", state.metrics_render, entry.index)
local cell = cells[entry.column][entry.weekday]
local date = centered_text(entry.display_date, metrics_cell_width)
vim.api.nvim_set_hl(0, group, { bg = style.bg, fg = style.value_fg })
vim.api.nvim_set_hl(0, date_group, { bg = style.bg, fg = style.date_fg, italic = true })
if entry.loc_net == 0 then
vim.api.nvim_buf_set_text(bufnr, cell.value_line, cell.start_col, cell.value_line, cell.end_col, { date })
else
local value = centered_text(clamped_metric_value(entry.loc_net), metrics_cell_width)
vim.api.nvim_buf_set_text(bufnr, cell.value_line, cell.start_col, cell.value_line, cell.end_col, { value })
vim.api.nvim_buf_set_text(bufnr, cell.date_line, cell.start_col, cell.date_line, cell.end_col, { date })
end
for row = 0, metrics_cell_height - 1 do
vim.api.nvim_buf_add_highlight(bufnr, namespace, group, cell.line + row, cell.start_col, cell.end_col)
end
local date_line = entry.loc_net == 0 and cell.value_line or cell.date_line
vim.api.nvim_buf_add_highlight(bufnr, namespace, date_group, date_line, cell.start_col, cell.end_col)
end
end
local function load_stats() local function load_stats()
local path = data_path() local path = data_path()
@@ -365,18 +741,61 @@ function M.stats()
} }
end end
function M.statusline() function M.display_stats()
local stats = M.stats() local stats = M.stats()
local added, deleted, net = display_day_stats(stats)
return {
added = added,
deleted = deleted,
net = net,
}
end
function M.statusline()
local stats = M.display_stats()
local prefix = state.config.statusline_prefix local prefix = state.config.statusline_prefix
return string.format("%s %+d", prefix, stats.net) return string.format("%s %+d", prefix, stats.net)
end end
function M.metrics()
ensure_setup()
local entries, column_count, max_abs_net, total_net = collect_metrics_entries()
local lines, cells = build_metrics_lines(column_count, total_net)
local bufnr, winid = open_metrics_window(lines)
state.metrics_render = state.metrics_render + 1
set_metrics_window_highlights(winid)
vim.bo[bufnr].modifiable = true
apply_metrics_highlights(bufnr, cells, entries, max_abs_net)
vim.bo[bufnr].modifiable = false
return {
bufnr = bufnr,
winid = winid,
entries = entries,
total_net = total_net,
max_abs_net = max_abs_net,
}
end
function M.save() function M.save()
ensure_setup() ensure_setup()
save_now() save_now()
end end
M._measure_change = measure_change M._measure_change = measure_change
M._metrics_color = metrics_color
M._metrics_dates = metrics_dates
M._metrics_entries = collect_metrics_entries
M._metrics_background = resolve_metrics_background
M._metrics_centered_text = centered_text
M._metrics_value = clamped_metric_value
M._metrics_tile_style = metrics_tile_style
M._metrics_display_date = metrics_display_date
M._chars_to_loc = chars_to_loc
return M return M
+6 -2
View File
@@ -17,7 +17,7 @@ vim.api.nvim_create_user_command("LocReset", function()
end, { desc = "Reset loc.nvim character counters" }) end, { desc = "Reset loc.nvim character counters" })
vim.api.nvim_create_user_command("LocStats", function() vim.api.nvim_create_user_command("LocStats", function()
local stats = require("loc").stats() local stats = require("loc").display_stats()
local message = string.format( local message = string.format(
"LOC added=%d deleted=%d net=%+d", "LOC added=%d deleted=%d net=%+d",
stats.added, stats.added,
@@ -26,4 +26,8 @@ vim.api.nvim_create_user_command("LocStats", function()
) )
vim.notify(message, vim.log.levels.INFO) vim.notify(message, vim.log.levels.INFO)
end, { desc = "Show loc.nvim character counters" }) end, { desc = "Show loc.nvim estimated LOC counters" })
vim.api.nvim_create_user_command("LocMetrics", function()
require("loc").metrics()
end, { desc = "Show loc.nvim 30-day LOC metrics heatmap" })
+102 -1
View File
@@ -46,6 +46,14 @@ assert_change("hello", "hello world", 6, 0)
assert_change("héllo", "hallo", 1, 1) assert_change("héllo", "hallo", 1, 1)
assert_change("one\ntwo", "one\nthree", 4, 2) assert_change("one\ntwo", "one\nthree", 4, 2)
assert_equal(loc._chars_to_loc(0), 0, "chars to loc zero")
assert_equal(loc._chars_to_loc(17), 0, "chars to loc below half")
assert_equal(loc._chars_to_loc(18), 1, "chars to loc rounds half up")
assert_equal(loc._chars_to_loc(35), 1, "chars to loc one line")
assert_equal(loc._chars_to_loc(52), 1, "chars to loc below one and a half")
assert_equal(loc._chars_to_loc(53), 2, "chars to loc above one and a half")
assert_equal(loc._chars_to_loc(-70), -2, "chars to loc negative")
local today = vim.fn.strftime("%Y-%m-%d") local today = vim.fn.strftime("%Y-%m-%d")
local old_day = "2000-01-01" local old_day = "2000-01-01"
@@ -61,7 +69,7 @@ local loaded_stats = loc.stats()
assert_equal(loaded_stats.added, 7, "loaded daily added") assert_equal(loaded_stats.added, 7, "loaded daily added")
assert_equal(loaded_stats.deleted, 2, "loaded daily deleted") assert_equal(loaded_stats.deleted, 2, "loaded daily deleted")
assert_equal(loaded_stats.net, 5, "loaded daily net") assert_equal(loaded_stats.net, 5, "loaded daily net")
assert_equal(loc.statusline(), "LOC +5", "daily statusline") assert_equal(loc.statusline(), "LOC +0", "daily statusline uses estimated loc")
loc.reset() loc.reset()
@@ -90,6 +98,7 @@ vim.api.nvim_exec_autocmds("TextChangedI", { buffer = bufnr })
local changed_stats = loc.stats() local changed_stats = loc.stats()
assert_equal(changed_stats.added, 2, "tracked edit added") assert_equal(changed_stats.added, 2, "tracked edit added")
assert_equal(changed_stats.deleted, 0, "tracked edit deleted") assert_equal(changed_stats.deleted, 0, "tracked edit deleted")
assert_equal(loc.display_stats().added, 0, "tracked edit display added")
loc.save() loc.save()
loc.disable() loc.disable()
@@ -100,4 +109,96 @@ local changed = read_json(change_path)
assert_equal(changed[today].added, 2, "tracked edit saved today added") assert_equal(changed[today].added, 2, "tracked edit saved today added")
assert_equal(changed[today].deleted, 0, "tracked edit saved today deleted") assert_equal(changed[today].deleted, 0, "tracked edit saved today deleted")
local metrics_epoch = vim.fn.strptime("%Y-%m-%d", "2026-05-10")
local metrics_dates = loc._metrics_dates(metrics_epoch)
assert_equal(#metrics_dates, 28, "metrics date count")
assert_equal(metrics_dates[1].date, "2026-04-19", "metrics first date")
assert_equal(metrics_dates[28].date, "2026-05-16", "metrics last date")
assert_equal(loc._metrics_color(0), "#f0b6c1", "metrics active color start")
assert_equal(loc._metrics_color(1), "#b90f36", "metrics active color end")
local zero_style = loc._metrics_tile_style(0, 9999)
assert_equal(zero_style.bg, "#252628", "metrics zero tile background")
assert_equal(zero_style.value_fg, "#e5e39a", "metrics zero value foreground")
assert_equal(zero_style.date_fg, "#8f9096", "metrics zero date foreground")
local low_style = loc._metrics_tile_style(2000, 9999)
assert_equal(low_style.bg, "#edaab6", "metrics low tile background")
assert_equal(low_style.value_fg, "#2b2529", "metrics low value foreground")
assert_equal(low_style.date_fg, "#58565b", "metrics low date foreground")
local high_style = loc._metrics_tile_style(9999, 9999)
assert_equal(high_style.bg, "#b90f36", "metrics high tile background")
assert_equal(high_style.value_fg, "#ffd6df", "metrics high value foreground")
assert_equal(high_style.date_fg, "#f0aebb", "metrics high date foreground")
assert_equal(loc._metrics_value(12000), "+9999", "metrics positive value clamps")
assert_equal(loc._metrics_value(-12000), "-9999", "metrics negative value clamps")
assert_equal(loc._metrics_value(0), "0", "metrics zero value")
assert_equal(loc._metrics_centered_text("+7", 7), " +7 ", "metrics centered value")
assert_equal(loc._metrics_display_date(metrics_dates[1].epoch), "04/19", "metrics display date")
vim.api.nvim_set_hl(0, "Normal", { fg = 0x112233, bg = 0x445566 })
vim.api.nvim_set_hl(0, "NormalFloat", { fg = 0xaabbcc, bg = 0xddeeff })
vim.api.nvim_set_hl(0, "StatusLine", { fg = 0x778899, bg = 0x0a0b0c })
local metrics_background = loc._metrics_background()
assert_equal(metrics_background.fg, 0x778899, "metrics background fg from StatusLine")
assert_equal(metrics_background.bg, 0x0a0b0c, "metrics background bg from StatusLine")
vim.api.nvim_set_hl(0, "Normal", { fg = 0x010203, bg = 0x040506 })
vim.api.nvim_set_hl(0, "StatusLine", {})
metrics_background = loc._metrics_background()
assert_equal(metrics_background.fg, 0x010203, "metrics background fg falls back to Normal")
assert_equal(metrics_background.bg, 0x040506, "metrics background bg falls back to Normal")
local metrics_path = vim.fn.tempname()
write_json(metrics_path, {
[metrics_dates[1].date] = { added = 245, deleted = 0 },
[metrics_dates[10].date] = { added = 0, deleted = 105 },
["2026-05-10"] = { added = 350, deleted = 0 },
})
loc.setup({ auto_enable = false, data_path = metrics_path, save_delay_ms = 1 })
local entries, column_count, max_abs_net, total_net = loc._metrics_entries(metrics_epoch)
assert_equal(#entries, 28, "metrics entry count")
assert_equal(column_count, 4, "metrics column count")
assert_equal(entries[1].net, 245, "metrics first entry raw net")
assert_equal(entries[1].loc_net, 7, "metrics first entry loc net")
assert_equal(entries[1].display_date, "04/19", "metrics first entry display date")
assert_equal(entries[2].net, 0, "metrics missing entry net")
assert_equal(entries[10].net, -105, "metrics negative raw entry net")
assert_equal(entries[10].loc_net, -3, "metrics negative loc entry net")
assert_equal(entries[22].date, "2026-05-10", "metrics current week starts with today")
assert_equal(entries[22].column, 4, "metrics today is in current week column")
assert_equal(entries[22].weekday, 0, "metrics today is Sunday row")
assert_equal(entries[28].date, "2026-05-16", "metrics includes future week end")
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(total_net, 14, "metrics total net")
local render = loc.metrics()
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")
local metrics_lines = vim.api.nvim_buf_get_lines(render.bufnr, 0, -1, false)
local rendered = table.concat(metrics_lines, "\n")
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, 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("04/19") ~= nil, true, "metrics renders date label")
assert_equal(rendered:match("04/20") ~= 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("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("FloatBorder") == nil, true, "metrics window does not set a border highlight")
vim.api.nvim_win_close(render.winid, true)
print("loc.nvim tests passed") print("loc.nvim tests passed")