Skip to content

Commit ca1d932

Browse files
committed
fix: track line changes only from git and save events
1 parent 46bcce0 commit ca1d932

File tree

3 files changed

+285
-228
lines changed

3 files changed

+285
-228
lines changed

lua/codeme/init.lua

Lines changed: 40 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,30 @@
1-
local M = {}
1+
-- CodeMe
2+
-- - Plugin tracks file activity
3+
-- - Git diff calculated on save
4+
-- - Backend stores and aggregates data
25

3-
-- File state cache for tracking line changes
4-
local file_state = {}
6+
local M = {}
57

6-
local config = {
7-
codeme_bin = "codeme", -- Binary name in PATH
8-
auto_track = true, -- Auto track on file save and open
9-
track_on_idle = false, -- Track on cursor idle (not implemented yet)
10-
verbose = false, -- Show tracking notifications
11-
auto_install = true, -- Auto-install binary if not found
12-
-- Goals configuration
13-
goals = {
14-
daily_hours = 5, -- Daily goal in hours (set to 0 to disable)
15-
daily_lines = 1000, -- Daily goal in lines (set to 0 to disable)
16-
},
8+
M.config = {
9+
codeme_bin = os.getenv("CODEME_BIN") or "codeme",
10+
verbose = false,
11+
auto_track = true,
1712
}
1813

19-
-- Expose config for other modules
20-
function M.get_config()
21-
return config
22-
end
23-
2414
function M.setup(opts)
25-
config = vim.tbl_deep_extend("force", config, opts or {})
15+
opts = opts or {}
16+
M.config = vim.tbl_deep_extend("force", M.config, opts)
2617

27-
-- Setup highlights
18+
-- Setup highlights (must be before commands/tracking)
2819
local highlights = require("codeme.highlights")
2920
highlights.setup()
3021
highlights.setup_autocmd() -- Auto-reload on colorscheme change
3122

32-
-- Check if binary is installed
33-
local installer = require("codeme.installer")
34-
if config.auto_install and not installer.is_installed() then
35-
installer.ensure_installed(function(success)
36-
if success then
37-
local bin = installer.get_binary()
38-
if bin then
39-
config.codeme_bin = bin
40-
end
41-
M.setup_tracking()
42-
end
43-
end)
44-
else
45-
-- Update config with actual binary path
46-
local bin = installer.get_binary()
47-
if bin then
48-
config.codeme_bin = bin
49-
end
23+
M.setup_commands()
24+
25+
if M.config.auto_track then
5026
M.setup_tracking()
5127
end
52-
53-
M.setup_commands()
5428
end
5529

5630
function M.setup_commands()
@@ -71,239 +45,77 @@ function M.setup_commands()
7145
end, { desc = "Show project breakdown" })
7246

7347
vim.api.nvim_create_user_command("CodeMeTrack", function()
74-
M.track_current_file()
75-
if config.verbose then
76-
vim.notify("CodeMe: Tracked current file", vim.log.levels.INFO)
48+
M.track(true)
49+
if M.config.verbose then
50+
vim.notify("CodeMe: Heartbeat sent", vim.log.levels.INFO)
7751
end
78-
end, { desc = "Manually track current file" })
79-
80-
vim.api.nvim_create_user_command("CodeMeInstall", function()
81-
local installer = require("codeme.installer")
82-
installer.install_latest(function(success, err)
83-
if success then
84-
vim.notify("✓ codeme installed successfully", vim.log.levels.INFO)
85-
-- Update binary path
86-
config.codeme_bin = installer.get_binary() or "codeme"
87-
M.setup_tracking()
88-
else
89-
vim.notify("Failed to install codeme: " .. (err or "unknown error"), vim.log.levels.ERROR)
90-
end
91-
end)
92-
end, { desc = "Install/update codeme binary" })
93-
94-
vim.api.nvim_create_user_command("CodeMeVersion", function()
95-
local installer = require("codeme.installer")
96-
installer.get_version(function(version)
97-
if version then
98-
vim.notify("codeme " .. version, vim.log.levels.INFO)
99-
else
100-
vim.notify("codeme not installed", vim.log.levels.WARN)
101-
end
102-
end)
103-
end, { desc = "Show codeme version" })
52+
end, { desc = "Manually send heartbeat" })
10453
end
10554

10655
function M.setup_tracking()
107-
if not config.auto_track then
108-
return
109-
end
110-
11156
local augroup = vim.api.nvim_create_augroup("CodeMeTrack", { clear = true })
11257

113-
-- Track on file save
58+
-- Track on save - this is where we calculate git diff
11459
vim.api.nvim_create_autocmd("BufWritePost", {
11560
group = augroup,
11661
callback = function()
117-
M.track_current_file()
62+
M.track(true)
11863
end,
64+
desc = "CodeMe: Track on save",
11965
})
12066

121-
-- Track when opening a file (but throttle to avoid spam)
67+
-- Track on open - just a heartbeat, no line counting
12268
vim.api.nvim_create_autocmd("BufReadPost", {
12369
group = augroup,
12470
callback = function()
125-
-- Delay to let buffer fully load
12671
vim.defer_fn(function()
127-
M.track_current_file()
72+
M.track(false)
12873
end, 100)
12974
end,
75+
desc = "CodeMe: Track on open",
13076
})
131-
132-
-- Track on focus gained (when switching back to Neovim)
133-
vim.api.nvim_create_autocmd("FocusGained", {
134-
group = augroup,
135-
callback = function()
136-
M.track_current_file()
137-
end,
138-
})
139-
end
140-
141-
-- Helper: Get git diff stats for a file
142-
local function get_git_diff(filepath)
143-
-- Check if we're in a git repo
144-
if vim.v.shell_error ~= 0 then
145-
return nil -- Not a git repo
146-
end
147-
148-
-- Get git diff stats (staged + unstaged changes)
149-
local cmd = string.format("git diff HEAD --numstat -- %s 2>/dev/null", vim.fn.shellescape(filepath))
150-
local output = vim.fn.system(cmd)
151-
if vim.v.shell_error ~= 0 or output == "" then
152-
return nil
153-
end
154-
155-
-- Parse: "5 2 file.lua" = 5 added, 2 deleted = 7 total changes
156-
local added, deleted = output:match("^(%d+)%s+(%d+)")
157-
if added and deleted then
158-
return tonumber(added) + tonumber(deleted)
159-
end
160-
return nil
16177
end
16278

163-
-- Track current file
164-
function M.track_current_file()
165-
local file = vim.fn.expand("%:p")
166-
local lang = vim.bo.filetype
167-
local bufnr = vim.api.nvim_get_current_buf()
168-
local current_lines = vim.api.nvim_buf_line_count(bufnr)
169-
170-
-- Skip if no file or empty buffer
171-
if file == "" or not vim.api.nvim_buf_is_valid(bufnr) then
172-
return
173-
end
174-
175-
-- Skip certain filetypes
176-
local skip_fts = { "", "NvimTree", "neo-tree", "dashboard", "alpha", "help", "qf", "fugitive", "TelescopePrompt" }
177-
if vim.tbl_contains(skip_fts, lang) then
178-
return
179-
end
180-
181-
-- Skip non-file buffers
182-
if vim.bo.buftype ~= "" then
183-
return
184-
end
185-
186-
-- Try git diff first (more accurate for git repos)
187-
local lines_changed = get_git_diff(file)
188-
189-
-- Fallback to delta calculation if not in git or no changes detected
190-
if not lines_changed or lines_changed == 0 then
191-
local previous_lines = file_state[file] or current_lines
192-
lines_changed = math.abs(current_lines - previous_lines)
193-
end
194-
195-
-- Update cache
196-
file_state[file] = current_lines
197-
198-
-- Call codeme binary to track (with both changed and total)
199-
local cmd = string.format(
200-
"%s track --file %s --lang %s --lines %d --total %d",
201-
config.codeme_bin,
202-
vim.fn.shellescape(file),
203-
lang ~= "" and lang or "unknown",
204-
lines_changed, -- Changed lines
205-
current_lines -- Total lines for reference
206-
)
207-
208-
vim.fn.jobstart(cmd, {
209-
detach = true,
210-
on_exit = function(_, code)
211-
if code ~= 0 then
212-
if config.verbose then
213-
vim.notify("CodeMe: Failed to track file", vim.log.levels.WARN)
214-
end
215-
elseif config.verbose then
216-
vim.notify(
217-
string.format("CodeMe: Tracked %s (+%d lines)", vim.fn.fnamemodify(file, ":t"), lines_changed),
218-
vim.log.levels.INFO
219-
)
220-
end
221-
end,
79+
--- Send tracking heartbeat
80+
--- @param is_save boolean true if this is a save event
81+
function M.track(is_save)
82+
local tracker = require("codeme.tracker")
83+
tracker.send_heartbeat({
84+
is_save = is_save,
85+
codeme_bin = M.config.codeme_bin,
86+
verbose = M.config.verbose,
22287
})
22388
end
22489

225-
-- Get stats from codeme binary
226-
-- @param callback function to call with stats
227-
-- @param today_only boolean if true, only get today's stats
90+
--- Get stats from backend
22891
function M.get_stats(callback, today_only)
229-
local cmd = config.codeme_bin .. " stats --json"
92+
local cmd = M.config.codeme_bin .. " stats --json"
23093
if today_only then
23194
cmd = cmd .. " --today"
23295
end
23396

234-
-- DEBUG: Log the command being executed
235-
if config.verbose then
236-
vim.notify("CodeMe: Running command: " .. cmd, vim.log.levels.DEBUG)
237-
end
238-
23997
vim.fn.jobstart(cmd, {
24098
stdout_buffered = true,
24199
on_stdout = function(_, data)
242-
-- DEBUG: Log raw data received
243-
if config.verbose then
244-
vim.notify(string.format("CodeMe: Received %d data items", #data), vim.log.levels.DEBUG)
245-
end
246-
247100
if data and #data > 0 then
248-
-- Filter out empty strings AND lines that don't look like JSON
249-
-- (to handle shell initialization output like "Using Node v22.11.0")
250101
local filtered = vim.tbl_filter(function(line)
251-
if line == "" then
252-
return false
253-
end
254-
-- Only accept lines that start with { or are part of JSON
255-
-- First real line should start with {
256-
return line:match("^%s*{") or line:match("[,}%]]%s*$")
102+
return line ~= "" and (line:match("^%s*{") or line:match("[,}%]]%s*$"))
257103
end, data)
258104

259-
-- DEBUG: Log filtered count
260-
if config.verbose then
261-
vim.notify(string.format("CodeMe: After filtering: %d items", #filtered), vim.log.levels.DEBUG)
262-
end
263-
264105
if #filtered > 0 then
265106
local json_str = table.concat(filtered, "")
266-
267-
-- DEBUG: Log JSON string info
268-
if config.verbose then
269-
vim.notify(string.format("CodeMe: JSON string length: %d", #json_str), vim.log.levels.DEBUG)
270-
end
271-
272107
local ok, stats = pcall(vim.json.decode, json_str)
273108
if ok and stats then
274109
callback(stats)
275-
else
276-
-- Enhanced error message with details
277-
vim.notify(
278-
string.format(
279-
"CodeMe: Failed to parse stats - invalid JSON\nError: %s\nFirst 200 chars: %s",
280-
tostring(stats),
281-
json_str:sub(1, 200)
282-
),
283-
vim.log.levels.ERROR
284-
)
285110
end
286-
else
287-
vim.notify("CodeMe: No data after filtering empty lines", vim.log.levels.WARN)
288-
end
289-
else
290-
vim.notify("CodeMe: No data received from stdout", vim.log.levels.WARN)
291-
end
292-
end,
293-
on_stderr = function(_, data)
294-
if data and #data > 0 then
295-
local errors = table.concat(data, "\n")
296-
if errors ~= "" then
297-
vim.notify("CodeMe error: " .. errors, vim.log.levels.ERROR)
298111
end
299112
end
300113
end,
301-
on_exit = function(_, code)
302-
if code ~= 0 then
303-
vim.notify(string.format("CodeMe: Command exited with code %d", code), vim.log.levels.ERROR)
304-
end
305-
end,
306114
})
307115
end
308116

117+
function M.get_config()
118+
return M.config
119+
end
120+
309121
return M

0 commit comments

Comments
 (0)