feat: track loc by day
This commit is contained in:
@@ -5,13 +5,13 @@
|
|||||||
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.
|
||||||
|
|
||||||
It keeps global lifetime totals:
|
It keeps daily totals:
|
||||||
|
|
||||||
- `added`: characters inserted
|
- `added`: characters inserted
|
||||||
- `deleted`: characters removed
|
- `deleted`: characters removed
|
||||||
- `net`: `added - deleted`
|
- `net`: `added - deleted`
|
||||||
|
|
||||||
The statusline text shows `net`, for example:
|
The statusline text shows today's `net`, for example:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
LOC +123
|
LOC +123
|
||||||
@@ -72,8 +72,8 @@ The plugin defines these commands when it is on your runtimepath:
|
|||||||
|
|
||||||
- `:LocEnable` starts tracking.
|
- `:LocEnable` starts tracking.
|
||||||
- `:LocDisable` stops tracking.
|
- `:LocDisable` stops tracking.
|
||||||
- `:LocReset` clears the counters.
|
- `:LocReset` clears today's counters.
|
||||||
- `:LocStats` prints the current counters.
|
- `:LocStats` prints today's counters.
|
||||||
|
|
||||||
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:
|
||||||
@@ -102,6 +102,20 @@ When `data_path` is `nil`, stats are stored at:
|
|||||||
stdpath("data")/loc.nvim/stats.json
|
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
|
## API
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
@@ -113,7 +127,7 @@ require("loc").statusline()
|
|||||||
require("loc").stats()
|
require("loc").stats()
|
||||||
```
|
```
|
||||||
|
|
||||||
`stats()` returns:
|
`stats()` returns today's counters:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
{
|
{
|
||||||
|
|||||||
+43
-15
@@ -17,8 +17,7 @@ local state = {
|
|||||||
snapshots = {},
|
snapshots = {},
|
||||||
save_pending = false,
|
save_pending = false,
|
||||||
dirty = false,
|
dirty = false,
|
||||||
added = 0,
|
days = {},
|
||||||
deleted = 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local function data_path()
|
local function data_path()
|
||||||
@@ -45,6 +44,21 @@ local function json_decode(value)
|
|||||||
return vim.fn.json_decode(value)
|
return vim.fn.json_decode(value)
|
||||||
end
|
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)
|
local function char_count(value)
|
||||||
if value == "" then
|
if value == "" then
|
||||||
return 0
|
return 0
|
||||||
@@ -150,8 +164,10 @@ local function apply_change(added, deleted)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
state.added = state.added + added
|
local day = day_stats(today_key())
|
||||||
state.deleted = state.deleted + deleted
|
|
||||||
|
day.added = day.added + added
|
||||||
|
day.deleted = day.deleted + deleted
|
||||||
state.dirty = true
|
state.dirty = true
|
||||||
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
@@ -165,10 +181,7 @@ local function save_now()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local path = data_path()
|
local path = data_path()
|
||||||
local payload = json_encode({
|
local payload = json_encode(state.days)
|
||||||
added = state.added,
|
|
||||||
deleted = state.deleted,
|
|
||||||
})
|
|
||||||
|
|
||||||
local ok = pcall(function()
|
local ok = pcall(function()
|
||||||
vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p")
|
vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p")
|
||||||
@@ -219,12 +232,14 @@ local function load_stats()
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not state.config.persist then
|
if not state.config.persist then
|
||||||
|
state.days = {}
|
||||||
state.loaded = true
|
state.loaded = true
|
||||||
state.loaded_path = path
|
state.loaded_path = path
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.fn.filereadable(path) ~= 1 then
|
if vim.fn.filereadable(path) ~= 1 then
|
||||||
|
state.days = {}
|
||||||
state.loaded = true
|
state.loaded = true
|
||||||
state.loaded_path = path
|
state.loaded_path = path
|
||||||
return
|
return
|
||||||
@@ -234,9 +249,18 @@ local function load_stats()
|
|||||||
return json_decode(table.concat(vim.fn.readfile(path), "\n"))
|
return json_decode(table.concat(vim.fn.readfile(path), "\n"))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
state.days = {}
|
||||||
|
state.dirty = false
|
||||||
|
|
||||||
if ok and type(parsed) == "table" then
|
if ok and type(parsed) == "table" then
|
||||||
state.added = tonumber(parsed.added) or 0
|
for date, stats in pairs(parsed) do
|
||||||
state.deleted = tonumber(parsed.deleted) or 0
|
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
|
state.loaded = true
|
||||||
@@ -320,8 +344,10 @@ end
|
|||||||
function M.reset()
|
function M.reset()
|
||||||
ensure_setup()
|
ensure_setup()
|
||||||
|
|
||||||
state.added = 0
|
local day = day_stats(today_key())
|
||||||
state.deleted = 0
|
|
||||||
|
day.added = 0
|
||||||
|
day.deleted = 0
|
||||||
state.dirty = true
|
state.dirty = true
|
||||||
save_now()
|
save_now()
|
||||||
vim.cmd.redrawstatus()
|
vim.cmd.redrawstatus()
|
||||||
@@ -330,10 +356,12 @@ end
|
|||||||
function M.stats()
|
function M.stats()
|
||||||
ensure_setup()
|
ensure_setup()
|
||||||
|
|
||||||
|
local day = day_stats(today_key())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
added = state.added,
|
added = day.added,
|
||||||
deleted = state.deleted,
|
deleted = day.deleted,
|
||||||
net = state.added - state.deleted,
|
net = day.added - day.deleted,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+71
-2
@@ -8,6 +8,30 @@ local function assert_equal(actual, expected, message)
|
|||||||
end
|
end
|
||||||
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 function assert_change(old, new, expected_added, expected_deleted)
|
||||||
local added, deleted = loc._measure_change(old, new)
|
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("héllo", "hallo", 1, 1)
|
||||||
assert_change("one\ntwo", "one\nthree", 4, 2)
|
assert_change("one\ntwo", "one\nthree", 4, 2)
|
||||||
|
|
||||||
local tmp = vim.fn.tempname()
|
local today = vim.fn.strftime("%Y-%m-%d")
|
||||||
loc.setup({ auto_enable = false, data_path = tmp, save_delay_ms = 1 })
|
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()
|
loc.reset()
|
||||||
|
|
||||||
local stats = loc.stats()
|
local stats = loc.stats()
|
||||||
@@ -31,4 +70,34 @@ assert_equal(stats.added, 0, "reset added")
|
|||||||
assert_equal(stats.deleted, 0, "reset deleted")
|
assert_equal(stats.deleted, 0, "reset deleted")
|
||||||
assert_equal(loc.statusline(), "LOC +0", "statusline")
|
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")
|
print("loc.nvim tests passed")
|
||||||
|
|||||||
Reference in New Issue
Block a user