diff --git a/README.md b/README.md index 63e3730..e108120 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ A tiny Neovim plugin that tracks characters added and deleted while you are in insert mode. -It keeps global lifetime totals: +It keeps daily totals: - `added`: characters inserted - `deleted`: characters removed - `net`: `added - deleted` -The statusline text shows `net`, for example: +The statusline text shows today's `net`, for example: ```text LOC +123 @@ -72,8 +72,8 @@ The plugin defines these commands when it is on your runtimepath: - `:LocEnable` starts tracking. - `:LocDisable` stops tracking. -- `:LocReset` clears the counters. -- `:LocStats` prints the current counters. +- `:LocReset` clears today's counters. +- `:LocStats` prints today's counters. Calling `require("loc").setup()` enables tracking by default. Use this if you want to configure the plugin but start it manually: @@ -102,6 +102,20 @@ When `data_path` is `nil`, stats are stored at: stdpath("data")/loc.nvim/stats.json ``` +Stats are stored by local date: + +```json +{ + "2026-04-30": { + "added": 123, + "deleted": 45 + } +} +``` + +With `persist = false`, daily counters stay in memory only for the current +Neovim process. + ## API ```lua @@ -113,7 +127,7 @@ require("loc").statusline() require("loc").stats() ``` -`stats()` returns: +`stats()` returns today's counters: ```lua { diff --git a/lua/loc/init.lua b/lua/loc/init.lua index 123a2e4..f995c6f 100644 --- a/lua/loc/init.lua +++ b/lua/loc/init.lua @@ -17,8 +17,7 @@ local state = { snapshots = {}, save_pending = false, dirty = false, - added = 0, - deleted = 0, + days = {}, } local function data_path() @@ -45,6 +44,21 @@ local function json_decode(value) return vim.fn.json_decode(value) end +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 +end + local function char_count(value) if value == "" then return 0 @@ -150,8 +164,10 @@ local function apply_change(added, deleted) return end - state.added = state.added + added - state.deleted = state.deleted + deleted + local day = day_stats(today_key()) + + day.added = day.added + added + day.deleted = day.deleted + deleted state.dirty = true vim.schedule(function() @@ -165,10 +181,7 @@ local function save_now() end local path = data_path() - local payload = json_encode({ - added = state.added, - deleted = state.deleted, - }) + local payload = json_encode(state.days) local ok = pcall(function() vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p") @@ -219,12 +232,14 @@ local function load_stats() end if not state.config.persist then + state.days = {} state.loaded = true state.loaded_path = path return end if vim.fn.filereadable(path) ~= 1 then + state.days = {} state.loaded = true state.loaded_path = path return @@ -234,9 +249,18 @@ local function load_stats() return json_decode(table.concat(vim.fn.readfile(path), "\n")) end) + state.days = {} + state.dirty = false + if ok and type(parsed) == "table" then - state.added = tonumber(parsed.added) or 0 - state.deleted = tonumber(parsed.deleted) or 0 + 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 state.loaded = true @@ -320,8 +344,10 @@ end function M.reset() ensure_setup() - state.added = 0 - state.deleted = 0 + local day = day_stats(today_key()) + + day.added = 0 + day.deleted = 0 state.dirty = true save_now() vim.cmd.redrawstatus() @@ -330,10 +356,12 @@ end function M.stats() ensure_setup() + local day = day_stats(today_key()) + return { - added = state.added, - deleted = state.deleted, - net = state.added - state.deleted, + added = day.added, + deleted = day.deleted, + net = day.added - day.deleted, } end diff --git a/test/loc_spec.lua b/test/loc_spec.lua index 0404998..57dbb00 100644 --- a/test/loc_spec.lua +++ b/test/loc_spec.lua @@ -8,6 +8,30 @@ local function assert_equal(actual, expected, message) end 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 function read_json(path) + return json_decode(table.concat(vim.fn.readfile(path), "\n")) +end + +local function write_json(path, value) + vim.fn.writefile({ json_encode(value) }, path) +end + local function assert_change(old, new, expected_added, expected_deleted) local added, deleted = loc._measure_change(old, new) @@ -22,8 +46,23 @@ assert_change("hello", "hello world", 6, 0) assert_change("héllo", "hallo", 1, 1) assert_change("one\ntwo", "one\nthree", 4, 2) -local tmp = vim.fn.tempname() -loc.setup({ auto_enable = false, data_path = tmp, save_delay_ms = 1 }) +local today = vim.fn.strftime("%Y-%m-%d") +local old_day = "2000-01-01" + +local daily_path = vim.fn.tempname() +write_json(daily_path, { + [today] = { added = 7, deleted = 2 }, + [old_day] = { added = 4, deleted = 1 }, +}) + +loc.setup({ auto_enable = false, data_path = daily_path, save_delay_ms = 1 }) + +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") + loc.reset() local stats = loc.stats() @@ -31,4 +70,34 @@ assert_equal(stats.added, 0, "reset added") assert_equal(stats.deleted, 0, "reset deleted") assert_equal(loc.statusline(), "LOC +0", "statusline") +local daily = read_json(daily_path) +assert_equal(daily[today].added, 0, "reset today added on disk") +assert_equal(daily[today].deleted, 0, "reset today deleted on disk") +assert_equal(daily[old_day].added, 4, "reset preserves old day added") +assert_equal(daily[old_day].deleted, 1, "reset preserves old day deleted") + +local change_path = vim.fn.tempname() +loc.setup({ auto_enable = false, data_path = change_path, save_delay_ms = 1 }) +loc.enable() + +local bufnr = vim.api.nvim_create_buf(false, true) +vim.api.nvim_set_current_buf(bufnr) +vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { "a" }) +vim.api.nvim_exec_autocmds("InsertEnter", { buffer = bufnr }) +vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { "abc" }) +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") + +loc.save() +loc.disable() +vim.wait(20) +vim.api.nvim_buf_delete(bufnr, { force = true }) + +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") + print("loc.nvim tests passed")