From 3b1672dad446f5023a2c3d3e54c589a73807dc39 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:05:47 +0000 Subject: [PATCH 1/2] fix(heartbeat): stop losing and misattributing coding activity The server builds coding sessions from heartbeats and closes a session after a 10 minute gap, so lost or wrong heartbeats skew measured effort. - Re-queue heartbeats when the send to the daemon fails instead of dropping them; the queue is capped at 5000 (oldest dropped first). - Flush pending heartbeats on VimLeavePre, waiting up to 1.5s. Sessions shorter than the 2 minute flush interval were lost entirely on :wq. - Use the autocmd's buffer (args.buf) instead of the current buffer, so :wa / :wqa record a write for each written file rather than N writes for the current one. The cursor comes from a window showing that buffer, or is omitted. - Only apply the same-cursor duplicate check to navigation events; edits that keep the cursor in place (x, dd) were dropped. - Escape the dot in the .git pattern; '/.git/' also matched '/egit/'. - Build UUIDs from uv.random. Reseeding math.random from the clock on every call made collisions likely across Neovim instances, and the server silently drops a heartbeat whose id already exists. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X5wQC8wrU4kY1jBfbzdquX --- lua/shelltime/heartbeat.lua | 88 +++++++++++++++++------- lua/shelltime/sender.lua | 39 +++++++++++ lua/shelltime/utils/system.lua | 38 ++++++++-- tests/heartbeat_spec.lua | 122 +++++++++++++++++++++++++++++++++ tests/sender_spec.lua | 87 +++++++++++++++++++++++ tests/system_spec.lua | 18 +++++ 6 files changed, 362 insertions(+), 30 deletions(-) diff --git a/lua/shelltime/heartbeat.lua b/lua/shelltime/heartbeat.lua index ab3d2a5..b3b3ab5 100644 --- a/lua/shelltime/heartbeat.lua +++ b/lua/shelltime/heartbeat.lua @@ -13,6 +13,9 @@ local PLUGIN_VERSION = '0.0.4' -- x-release-please-version -- Pending heartbeats queue local pending_heartbeats = {} +-- Upper bound for the queue while the daemon is unreachable +local MAX_PENDING = 5000 + -- Last heartbeat time per file (for debouncing) local last_heartbeat_time = {} @@ -55,7 +58,7 @@ local function is_valid_buffer(bufnr) end -- Skip .git directory files - if file_path:match('/.git/') then + if file_path:match('[/\\]%.git[/\\]') then return false end @@ -121,18 +124,37 @@ local function update_last_activity(file_path, line_number, cursor_position) last_activity.cursor_position = cursor_position end ---- Create heartbeat data for current buffer +--- Get the cursor of a window showing the buffer +--- Events like BufWritePost (:wa) can fire for buffers other than the current one. +---@param bufnr number Buffer number +---@return number|nil line_number Line number (1-indexed) +---@return number|nil cursor_position Cursor column (0-indexed) +local function get_cursor(bufnr) + local winid = 0 + if vim.api.nvim_win_get_buf(0) ~= bufnr then + winid = vim.fn.bufwinid(bufnr) + if winid == -1 then + return nil, nil + end + end + + local cursor = vim.api.nvim_win_get_cursor(winid) + return cursor[1], cursor[2] +end + +--- Create heartbeat data for a buffer ---@param bufnr number Buffer number ---@param is_write boolean Whether this is a write event +---@param line_number number|nil Line number (1-indexed) +---@param cursor_position number|nil Cursor column (0-indexed) ---@return table|nil Heartbeat data or nil -local function create_heartbeat(bufnr, is_write) +local function create_heartbeat(bufnr, is_write, line_number, cursor_position) local file_path = vim.api.nvim_buf_get_name(bufnr) if file_path == '' then return nil end local project_root = system.get_project_root(file_path) - local cursor = vim.api.nvim_win_get_cursor(0) return { heartbeatId = system.uuid(), @@ -145,8 +167,8 @@ local function create_heartbeat(bufnr, is_write) branch = git.get_branch(file_path), language = lang.get_language(vim.bo[bufnr].filetype, file_path), lines = vim.api.nvim_buf_line_count(bufnr), - lineNumber = cursor[1], -- Already 1-indexed - cursorPosition = cursor[2], -- 0-indexed column + lineNumber = line_number, + cursorPosition = cursor_position, editor = 'neovim', editorVersion = system.get_editor_version(), plugin = 'shelltime', @@ -158,10 +180,19 @@ local function create_heartbeat(bufnr, is_write) } end +--- Drop the oldest heartbeats once the queue exceeds MAX_PENDING +local function trim_queue() + local overflow = #pending_heartbeats - MAX_PENDING + if overflow > 0 then + pending_heartbeats = vim.list_slice(pending_heartbeats, overflow + 1) + end +end + --- Add heartbeat to pending queue ---@param heartbeat table Heartbeat data local function add_heartbeat(heartbeat) table.insert(pending_heartbeats, heartbeat) + trim_queue() if config.get('debug') then vim.notify( @@ -172,14 +203,14 @@ local function add_heartbeat(heartbeat) end --- Handle editor event +---@param bufnr number Buffer the event fired for ---@param is_write boolean Whether this is a write event -local function on_event(is_write) +---@param is_navigation boolean Whether this is a navigation event (BufEnter, cursor moves) +local function on_event(bufnr, is_write, is_navigation) if not config.is_enabled() then return end - local bufnr = vim.api.nvim_get_current_buf() - if not is_valid_buffer(bufnr) then return end @@ -187,12 +218,11 @@ local function on_event(is_write) local file_path = vim.api.nvim_buf_get_name(bufnr) -- Get cursor position for duplicate detection - local cursor = vim.api.nvim_win_get_cursor(0) - local line_number = cursor[1] - local cursor_position = cursor[2] + local line_number, cursor_position = get_cursor(bufnr) - -- Skip duplicate events (same file and cursor position) - if is_duplicate_activity(file_path, line_number, cursor_position, is_write) then + -- Skip repeated navigation events (same file and cursor position). + -- Edits always count, even when the cursor stays in place (x, dd). + if is_navigation and is_duplicate_activity(file_path, line_number, cursor_position, is_write) then return end @@ -204,7 +234,7 @@ local function on_event(is_write) return end - local heartbeat = create_heartbeat(bufnr, is_write) + local heartbeat = create_heartbeat(bufnr, is_write, line_number, cursor_position) if heartbeat then add_heartbeat(heartbeat) end @@ -221,32 +251,32 @@ function M.start() -- File opened vim.api.nvim_create_autocmd('BufEnter', { group = augroup, - callback = function() - on_event(false) + callback = function(args) + on_event(args.buf, false, true) end, }) -- Text changed vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { group = augroup, - callback = function() - on_event(false) + callback = function(args) + on_event(args.buf, false, false) end, }) - -- File saved + -- File saved (args.buf is the written buffer, which for :wa is not the current one) vim.api.nvim_create_autocmd('BufWritePost', { group = augroup, - callback = function() - on_event(true) + callback = function(args) + on_event(args.buf, true, false) end, }) -- Cursor moved vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { group = augroup, - callback = function() - on_event(false) + callback = function(args) + on_event(args.buf, false, true) end, }) end @@ -267,6 +297,16 @@ function M.flush() return heartbeats end +--- Put heartbeats that could not be delivered back at the front of the queue +---@param heartbeats table[] Heartbeats to retry +function M.requeue(heartbeats) + if #heartbeats == 0 then + return + end + pending_heartbeats = vim.list_extend(vim.list_extend({}, heartbeats), pending_heartbeats) + trim_queue() +end + --- Get pending heartbeat count ---@return number Count function M.get_pending_count() diff --git a/lua/shelltime/sender.lua b/lua/shelltime/sender.lua index fb4074f..080bb7c 100644 --- a/lua/shelltime/sender.lua +++ b/lua/shelltime/sender.lua @@ -10,6 +10,12 @@ local M = {} -- Flush timer local flush_timer = nil +-- Autocmd group for the exit flush +local augroup = nil + +-- How long Neovim may wait on exit for pending heartbeats to be sent +local EXIT_FLUSH_TIMEOUT = 1500 + -- Connection status local is_connected = false @@ -28,6 +34,11 @@ local function send_heartbeats(callback) socket.send_heartbeats(heartbeats, function(success, err) is_connected = success + if not success then + -- Keep them for the next flush instead of dropping them + heartbeat.requeue(heartbeats) + end + if config.get('debug') then if success then vim.notify( @@ -64,6 +75,16 @@ function M.start() end) end) + -- Sessions are often shorter than the flush interval, so send what is + -- pending before Neovim exits. + augroup = vim.api.nvim_create_augroup('ShellTimeSender', { clear = true }) + vim.api.nvim_create_autocmd('VimLeavePre', { + group = augroup, + callback = function() + M.flush_sync(EXIT_FLUSH_TIMEOUT) + end, + }) + -- Check initial connection status and CLI version vim.schedule(function() is_connected = socket.is_connected_sync() @@ -86,6 +107,11 @@ function M.stop() flush_timer:close() flush_timer = nil end + + if augroup then + vim.api.nvim_del_augroup_by_id(augroup) + augroup = nil + end end --- Force flush pending heartbeats @@ -94,6 +120,19 @@ function M.flush(callback) send_heartbeats(callback) end +--- Flush pending heartbeats and block until sent or timed out +---@param timeout number Maximum wait in milliseconds +---@return boolean True if the send finished within the timeout +function M.flush_sync(timeout) + local done = false + send_heartbeats(function() + done = true + end) + return vim.wait(timeout, function() + return done + end, 10) +end + --- Get connection status ---@return boolean function M.is_connected() diff --git a/lua/shelltime/utils/system.lua b/lua/shelltime/utils/system.lua index 3019ffe..9d98d7c 100644 --- a/lua/shelltime/utils/system.lua +++ b/lua/shelltime/utils/system.lua @@ -92,15 +92,41 @@ function M.get_project_name(project_root) return tail end +-- Whether the math.random fallback has been seeded +local seeded = false + +--- Get 16 random bytes, from the OS when possible +---@return number[] Byte values (0-255) +local function random_bytes() + local uv = vim.uv or vim.loop + local ok, bytes = pcall(uv.random, 16) + if ok and type(bytes) == 'string' and #bytes == 16 then + return { bytes:byte(1, 16) } + end + + -- Seed once: reseeding on every call from the clock repeats sequences + -- across calls and Neovim instances, and heartbeat ids must be unique. + if not seeded then + math.randomseed(uv.hrtime() + uv.os_getpid()) + seeded = true + end + local result = {} + for i = 1, 16 do + result[i] = math.random(0, 255) + end + return result +end + --- Generate UUID v4 ---@return string UUID string function M.uuid() - math.randomseed(os.time() + os.clock() * 1000000) - local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - return string.gsub(template, '[xy]', function(c) - local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) - return string.format('%x', v) - end) + local b = random_bytes() + b[7] = (b[7] % 16) + 0x40 -- version 4 + b[9] = (b[9] % 64) + 0x80 -- RFC 4122 variant + return string.format( + '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x', + unpack(b) + ) end --- Get current Unix timestamp in seconds diff --git a/tests/heartbeat_spec.lua b/tests/heartbeat_spec.lua index 9f43a7c..31cd7f1 100644 --- a/tests/heartbeat_spec.lua +++ b/tests/heartbeat_spec.lua @@ -180,6 +180,128 @@ describe('shelltime.heartbeat', function() end) end) + describe('requeue', function() + it('should put heartbeats back ahead of newer ones', function() + heartbeat.requeue({ { entity = 'newer' } }) + heartbeat.requeue({ { entity = 'older' } }) + + local pending = heartbeat.flush() + assert.equals(2, #pending) + assert.equals('older', pending[1].entity) + assert.equals('newer', pending[2].entity) + end) + + it('should ignore an empty list', function() + heartbeat.requeue({}) + assert.equals(0, heartbeat.get_pending_count()) + end) + + it('should cap the queue and drop the oldest heartbeats', function() + local list = {} + for i = 1, 5001 do + list[i] = { entity = 'file-' .. i } + end + + heartbeat.requeue(list) + + local pending = heartbeat.flush() + assert.equals(5000, #pending) + assert.equals('file-2', pending[1].entity) + assert.equals('file-5001', pending[5000].entity) + end) + end) + + describe('autocmd events (integration)', function() + local buffers = {} + + local function file_buffer(path) + local bufnr = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(bufnr, path) + table.insert(buffers, bufnr) + return bufnr + end + + before_each(function() + config._set_for_testing({ debounce_interval = 0 }) + heartbeat.start() + end) + + after_each(function() + heartbeat.stop() + for _, bufnr in ipairs(buffers) do + pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) + end + buffers = {} + end) + + it('should attribute a write to the written buffer, not the current one', function() + local current = file_buffer('/tmp/shelltime-test/current.lua') + local written = file_buffer('/tmp/shelltime-test/written.lua') + vim.api.nvim_set_current_buf(current) + heartbeat.flush() -- drop the BufEnter heartbeat + + vim.api.nvim_exec_autocmds('BufWritePost', { buffer = written }) + + local pending = heartbeat.flush() + assert.equals(1, #pending) + assert.equals('/tmp/shelltime-test/written.lua', pending[1].entity) + assert.is_true(pending[1].isWrite) + -- Not shown in any window, so there is no cursor to report + assert.is_nil(pending[1].lineNumber) + end) + + it('should report the cursor of the current window', function() + local current = file_buffer('/tmp/shelltime-test/current.lua') + vim.api.nvim_set_current_buf(current) + heartbeat.flush() -- drop the BufEnter heartbeat + + vim.api.nvim_exec_autocmds('BufWritePost', { buffer = current }) + + local pending = heartbeat.flush() + assert.equals(1, #pending) + assert.equals(1, pending[1].lineNumber) + assert.equals(0, pending[1].cursorPosition) + end) + + it('should skip files inside .git', function() + local bufnr = file_buffer('/tmp/shelltime-test/.git/COMMIT_EDITMSG') + + vim.api.nvim_exec_autocmds('BufWritePost', { buffer = bufnr }) + + assert.equals(0, heartbeat.get_pending_count()) + end) + + it('should track directories that only look like .git', function() + local bufnr = file_buffer('/tmp/shelltime-test/egit/main.lua') + + vim.api.nvim_exec_autocmds('BufWritePost', { buffer = bufnr }) + + assert.equals(1, heartbeat.get_pending_count()) + end) + + it('should count edits that leave the cursor in place', function() + local bufnr = file_buffer('/tmp/shelltime-test/edit.lua') + vim.api.nvim_set_current_buf(bufnr) + heartbeat.flush() -- drop the BufEnter heartbeat + + vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr }) + vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr }) + + assert.equals(2, heartbeat.get_pending_count()) + end) + + it('should skip repeated cursor events at the same position', function() + local bufnr = file_buffer('/tmp/shelltime-test/nav.lua') + vim.api.nvim_set_current_buf(bufnr) + heartbeat.flush() -- drop the BufEnter heartbeat + + vim.api.nvim_exec_autocmds('CursorMoved', { buffer = bufnr }) + vim.api.nvim_exec_autocmds('CursorMoved', { buffer = bufnr }) + + assert.equals(0, heartbeat.get_pending_count()) + end) + end) + describe('buffer validation (integration)', function() -- These tests verify buffer validation through behavior diff --git a/tests/sender_spec.lua b/tests/sender_spec.lua index 9f8f8a1..86ae702 100644 --- a/tests/sender_spec.lua +++ b/tests/sender_spec.lua @@ -120,6 +120,93 @@ describe('shelltime.sender', function() sender.flush() end) end) + + it('should re-queue heartbeats when the send fails', function() + heartbeat.requeue({ { entity = 'a' }, { entity = 'b' } }) + socket_stub = stub(socket, 'send_heartbeats') + socket_stub.invokes(function(heartbeats, callback) + callback(false, 'Connection failed: ENOENT') + end) + + local called = false + sender.flush(function(success) + called = true + assert.is_false(success) + end) + + vim.wait(100, function() return called end) + assert.is_true(called) + local pending = heartbeat.flush() + assert.equals(2, #pending) + assert.equals('a', pending[1].entity) + end) + + it('should not keep heartbeats after a successful send', function() + heartbeat.requeue({ { entity = 'a' } }) + socket_stub = stub(socket, 'send_heartbeats') + socket_stub.invokes(function(heartbeats, callback) + callback(true, nil) + end) + + local called = false + sender.flush(function() + called = true + end) + + vim.wait(100, function() return called end) + assert.equals(0, heartbeat.get_pending_count()) + end) + end) + + describe('flush_sync', function() + local socket_stub + + after_each(function() + if socket_stub and socket_stub.revert then + socket_stub:revert() + socket_stub = nil + end + end) + + it('should wait for a scheduled send to finish', function() + heartbeat.requeue({ { entity = 'a' } }) + socket_stub = stub(socket, 'send_heartbeats') + socket_stub.invokes(function(heartbeats, callback) + vim.schedule(function() + callback(true, nil) + end) + end) + + assert.is_true(sender.flush_sync(500)) + assert.stub(socket_stub).was_called(1) + assert.equals(0, heartbeat.get_pending_count()) + end) + + it('should give up after the timeout', function() + heartbeat.requeue({ { entity = 'a' } }) + socket_stub = stub(socket, 'send_heartbeats') + + assert.is_false(sender.flush_sync(20)) + end) + end) + + describe('exit flush', function() + it('should register a VimLeavePre autocmd on start', function() + sender.start() + + local autocmds = vim.api.nvim_get_autocmds({ group = 'ShellTimeSender', event = 'VimLeavePre' }) + assert.equals(1, #autocmds) + + sender.stop() + end) + + it('should remove the autocmd on stop', function() + sender.start() + sender.stop() + + local ok = pcall(vim.api.nvim_get_autocmds, { group = 'ShellTimeSender' }) + assert.is_false(ok) + end) end) describe('is_connected', function() diff --git a/tests/system_spec.lua b/tests/system_spec.lua index ce5d1e0..485079a 100644 --- a/tests/system_spec.lua +++ b/tests/system_spec.lua @@ -56,6 +56,24 @@ describe('shelltime.utils.system', function() assert.equals('-', id:sub(19, 19)) assert.equals('-', id:sub(24, 24)) end) + + it('should fall back to math.random when OS randomness is unavailable', function() + local uv = vim.uv or vim.loop + local random_stub = require('luassert.stub')(uv, 'random') + random_stub.invokes(function() + error('not supported') + end) + + local ids = {} + for _ = 1, 100 do + local id = system.uuid() + assert.matches('^%x%x%x%x%x%x%x%x%-%x%x%x%x%-4%x%x%x%-[89ab]%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$', id) + assert.is_nil(ids[id], 'UUID collision detected') + ids[id] = true + end + + random_stub:revert() + end) end) describe('get_timestamp', function() From bf135ae43c8e15cc22a93b1cc3f8b0d812e9ea77 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:14:55 +0000 Subject: [PATCH 2/2] revert(heartbeat): keep using the current buffer in autocmds During BufWritePost for :wa / :wqa, Neovim makes the written buffer current (aucmd_prepbuf), so nvim_get_current_buf() already pointed at the right file and the args.buf change fixed nothing. The test that seemed to show the bug relied on nvim_exec_autocmds not switching buffers, which Neovim nightly now does, so it failed there. Restore the original buffer and cursor lookup and rewrite the integration specs to make the buffer current first. Verified on Neovim 0.11.4 and nightly (0.13.0-dev). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X5wQC8wrU4kY1jBfbzdquX --- lua/shelltime/heartbeat.lua | 58 ++++++++++++++----------------------- tests/heartbeat_spec.lua | 46 +++++------------------------ 2 files changed, 29 insertions(+), 75 deletions(-) diff --git a/lua/shelltime/heartbeat.lua b/lua/shelltime/heartbeat.lua index b3b3ab5..7efb89c 100644 --- a/lua/shelltime/heartbeat.lua +++ b/lua/shelltime/heartbeat.lua @@ -124,37 +124,18 @@ local function update_last_activity(file_path, line_number, cursor_position) last_activity.cursor_position = cursor_position end ---- Get the cursor of a window showing the buffer ---- Events like BufWritePost (:wa) can fire for buffers other than the current one. ----@param bufnr number Buffer number ----@return number|nil line_number Line number (1-indexed) ----@return number|nil cursor_position Cursor column (0-indexed) -local function get_cursor(bufnr) - local winid = 0 - if vim.api.nvim_win_get_buf(0) ~= bufnr then - winid = vim.fn.bufwinid(bufnr) - if winid == -1 then - return nil, nil - end - end - - local cursor = vim.api.nvim_win_get_cursor(winid) - return cursor[1], cursor[2] -end - ---- Create heartbeat data for a buffer +--- Create heartbeat data for current buffer ---@param bufnr number Buffer number ---@param is_write boolean Whether this is a write event ----@param line_number number|nil Line number (1-indexed) ----@param cursor_position number|nil Cursor column (0-indexed) ---@return table|nil Heartbeat data or nil -local function create_heartbeat(bufnr, is_write, line_number, cursor_position) +local function create_heartbeat(bufnr, is_write) local file_path = vim.api.nvim_buf_get_name(bufnr) if file_path == '' then return nil end local project_root = system.get_project_root(file_path) + local cursor = vim.api.nvim_win_get_cursor(0) return { heartbeatId = system.uuid(), @@ -167,8 +148,8 @@ local function create_heartbeat(bufnr, is_write, line_number, cursor_position) branch = git.get_branch(file_path), language = lang.get_language(vim.bo[bufnr].filetype, file_path), lines = vim.api.nvim_buf_line_count(bufnr), - lineNumber = line_number, - cursorPosition = cursor_position, + lineNumber = cursor[1], -- Already 1-indexed + cursorPosition = cursor[2], -- 0-indexed column editor = 'neovim', editorVersion = system.get_editor_version(), plugin = 'shelltime', @@ -203,14 +184,15 @@ local function add_heartbeat(heartbeat) end --- Handle editor event ----@param bufnr number Buffer the event fired for ---@param is_write boolean Whether this is a write event ---@param is_navigation boolean Whether this is a navigation event (BufEnter, cursor moves) -local function on_event(bufnr, is_write, is_navigation) +local function on_event(is_write, is_navigation) if not config.is_enabled() then return end + local bufnr = vim.api.nvim_get_current_buf() + if not is_valid_buffer(bufnr) then return end @@ -218,7 +200,9 @@ local function on_event(bufnr, is_write, is_navigation) local file_path = vim.api.nvim_buf_get_name(bufnr) -- Get cursor position for duplicate detection - local line_number, cursor_position = get_cursor(bufnr) + local cursor = vim.api.nvim_win_get_cursor(0) + local line_number = cursor[1] + local cursor_position = cursor[2] -- Skip repeated navigation events (same file and cursor position). -- Edits always count, even when the cursor stays in place (x, dd). @@ -234,7 +218,7 @@ local function on_event(bufnr, is_write, is_navigation) return end - local heartbeat = create_heartbeat(bufnr, is_write, line_number, cursor_position) + local heartbeat = create_heartbeat(bufnr, is_write) if heartbeat then add_heartbeat(heartbeat) end @@ -251,32 +235,32 @@ function M.start() -- File opened vim.api.nvim_create_autocmd('BufEnter', { group = augroup, - callback = function(args) - on_event(args.buf, false, true) + callback = function() + on_event(false, true) end, }) -- Text changed vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { group = augroup, - callback = function(args) - on_event(args.buf, false, false) + callback = function() + on_event(false, false) end, }) - -- File saved (args.buf is the written buffer, which for :wa is not the current one) + -- File saved vim.api.nvim_create_autocmd('BufWritePost', { group = augroup, - callback = function(args) - on_event(args.buf, true, false) + callback = function() + on_event(true, false) end, }) -- Cursor moved vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { group = augroup, - callback = function(args) - on_event(args.buf, false, true) + callback = function() + on_event(false, true) end, }) end diff --git a/tests/heartbeat_spec.lua b/tests/heartbeat_spec.lua index 31cd7f1..ed3a428 100644 --- a/tests/heartbeat_spec.lua +++ b/tests/heartbeat_spec.lua @@ -214,10 +214,13 @@ describe('shelltime.heartbeat', function() describe('autocmd events (integration)', function() local buffers = {} - local function file_buffer(path) + -- Open a named file buffer in the current window, dropping its BufEnter heartbeat + local function open_buffer(path) local bufnr = vim.api.nvim_create_buf(true, false) vim.api.nvim_buf_set_name(bufnr, path) table.insert(buffers, bufnr) + vim.api.nvim_set_current_buf(bufnr) + heartbeat.flush() return bufnr end @@ -234,37 +237,8 @@ describe('shelltime.heartbeat', function() buffers = {} end) - it('should attribute a write to the written buffer, not the current one', function() - local current = file_buffer('/tmp/shelltime-test/current.lua') - local written = file_buffer('/tmp/shelltime-test/written.lua') - vim.api.nvim_set_current_buf(current) - heartbeat.flush() -- drop the BufEnter heartbeat - - vim.api.nvim_exec_autocmds('BufWritePost', { buffer = written }) - - local pending = heartbeat.flush() - assert.equals(1, #pending) - assert.equals('/tmp/shelltime-test/written.lua', pending[1].entity) - assert.is_true(pending[1].isWrite) - -- Not shown in any window, so there is no cursor to report - assert.is_nil(pending[1].lineNumber) - end) - - it('should report the cursor of the current window', function() - local current = file_buffer('/tmp/shelltime-test/current.lua') - vim.api.nvim_set_current_buf(current) - heartbeat.flush() -- drop the BufEnter heartbeat - - vim.api.nvim_exec_autocmds('BufWritePost', { buffer = current }) - - local pending = heartbeat.flush() - assert.equals(1, #pending) - assert.equals(1, pending[1].lineNumber) - assert.equals(0, pending[1].cursorPosition) - end) - it('should skip files inside .git', function() - local bufnr = file_buffer('/tmp/shelltime-test/.git/COMMIT_EDITMSG') + local bufnr = open_buffer('/tmp/shelltime-test/.git/COMMIT_EDITMSG') vim.api.nvim_exec_autocmds('BufWritePost', { buffer = bufnr }) @@ -272,7 +246,7 @@ describe('shelltime.heartbeat', function() end) it('should track directories that only look like .git', function() - local bufnr = file_buffer('/tmp/shelltime-test/egit/main.lua') + local bufnr = open_buffer('/tmp/shelltime-test/egit/main.lua') vim.api.nvim_exec_autocmds('BufWritePost', { buffer = bufnr }) @@ -280,9 +254,7 @@ describe('shelltime.heartbeat', function() end) it('should count edits that leave the cursor in place', function() - local bufnr = file_buffer('/tmp/shelltime-test/edit.lua') - vim.api.nvim_set_current_buf(bufnr) - heartbeat.flush() -- drop the BufEnter heartbeat + local bufnr = open_buffer('/tmp/shelltime-test/edit.lua') vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr }) vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr }) @@ -291,9 +263,7 @@ describe('shelltime.heartbeat', function() end) it('should skip repeated cursor events at the same position', function() - local bufnr = file_buffer('/tmp/shelltime-test/nav.lua') - vim.api.nvim_set_current_buf(bufnr) - heartbeat.flush() -- drop the BufEnter heartbeat + local bufnr = open_buffer('/tmp/shelltime-test/nav.lua') vim.api.nvim_exec_autocmds('CursorMoved', { buffer = bufnr }) vim.api.nvim_exec_autocmds('CursorMoved', { buffer = bufnr })