diff --git a/README.md b/README.md index ca1f91d..5c6bd70 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > Entirely GPT 5.5 generated 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: @@ -63,6 +63,7 @@ The plugin defines these commands when it is on your runtimepath: - `:LocDisable` stops tracking. - `:LocReset` clears 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 want to configure the plugin but start it manually: @@ -114,6 +115,7 @@ require("loc").reset() require("loc").save() require("loc").statusline() require("loc").stats() +require("loc").metrics() ``` `stats()` returns today's counters: diff --git a/lua/loc/init.lua b/lua/loc/init.lua index f995c6f..0c7890a 100644 --- a/lua/loc/init.lua +++ b/lua/loc/init.lua @@ -18,78 +18,72 @@ local state = { save_pending = false, dirty = false, 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() - if state.config and state.config.data_path then - return state.config.data_path - end - - return table.concat({ vim.fn.stdpath("data"), "loc.nvim", "stats.json" }, "/") + return (state.config and state.config.data_path) + or table.concat({ vim.fn.stdpath("data"), "loc.nvim", "stats.json" }, "/") end -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 json_encode = (vim.json and vim.json.encode) or vim.fn.json_encode +local json_decode = (vim.json and vim.json.decode) or vim.fn.json_decode local function today_key() return vim.fn.strftime("%Y-%m-%d") end local function day_stats(date) - local day = state.days[date] - - if not day then - day = { added = 0, deleted = 0 } - state.days[date] = day - end - - return day + state.days[date] = state.days[date] or { added = 0, deleted = 0 } + return state.days[date] end local function char_count(value) - if value == "" then - return 0 - end - + if value == "" then return 0 end local ok, count = pcall(vim.fn.strchars, value) - if ok then - return count - end - - return #value + return ok and count or #value end local function is_continuation_byte(byte) return byte and byte >= 0x80 and byte <= 0xbf 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)) +local function has_char_boundary_at(value, byte_index) + return byte_index <= 1 or byte_index > #value or not is_continuation_byte(value:byte(byte_index)) end -local function has_char_boundary_at(value, byte_index) - if byte_index <= 1 or byte_index > #value then - return true - 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 local function common_prefix_bytes(old, new) @@ -224,6 +218,388 @@ local function update_snapshot(bufnr) state.snapshots[bufnr] = current 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", "", close, opts) + + -- Disable scrolling and movement + local nop = function() end + local keys = { + "", "", "", "", + "", "", "", "", "k", "j", "h", "l", + "", "", "", "", "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 path = data_path() @@ -365,18 +741,61 @@ function M.stats() } end -function M.statusline() +function M.display_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 return string.format("%s %+d", prefix, stats.net) 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() ensure_setup() save_now() end 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 diff --git a/plugin/loc.lua b/plugin/loc.lua index e5e0bfe..36ca352 100644 --- a/plugin/loc.lua +++ b/plugin/loc.lua @@ -17,7 +17,7 @@ vim.api.nvim_create_user_command("LocReset", function() end, { desc = "Reset loc.nvim character counters" }) vim.api.nvim_create_user_command("LocStats", function() - local stats = require("loc").stats() + local stats = require("loc").display_stats() local message = string.format( "LOC added=%d deleted=%d net=%+d", stats.added, @@ -26,4 +26,8 @@ vim.api.nvim_create_user_command("LocStats", function() ) 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" }) diff --git a/test/loc_spec.lua b/test/loc_spec.lua index 57dbb00..d9f8fce 100644 --- a/test/loc_spec.lua +++ b/test/loc_spec.lua @@ -46,6 +46,14 @@ assert_change("hello", "hello world", 6, 0) assert_change("héllo", "hallo", 1, 1) 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 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.deleted, 2, "loaded daily deleted") 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() @@ -90,6 +98,7 @@ vim.api.nvim_exec_autocmds("TextChangedI", { buffer = bufnr }) local changed_stats = loc.stats() assert_equal(changed_stats.added, 2, "tracked edit added") assert_equal(changed_stats.deleted, 0, "tracked edit deleted") +assert_equal(loc.display_stats().added, 0, "tracked edit display added") loc.save() 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].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")