Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions lua/shelltime/heartbeat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ local pending_heartbeats = {}
-- Last heartbeat time per file (for debouncing)
local last_heartbeat_time = {}

-- Last activity state (for duplicate detection)
local last_activity = {
file_path = nil,
line_number = nil,
cursor_position = nil,
}

-- Autocmd group
local augroup = nil

Expand Down Expand Up @@ -82,6 +89,38 @@ local function should_send_heartbeat(file_path, is_write)
return false
end

--- Check if activity is a duplicate (same file and cursor position)
---@param file_path string File path
---@param line_number number Line number (1-indexed)
---@param cursor_position number Cursor column (0-indexed)
---@param is_write boolean Whether this is a write event
---@return boolean True if duplicate (should skip)
local function is_duplicate_activity(file_path, line_number, cursor_position, is_write)
-- Write events are never considered duplicates
if is_write then
return false
end

-- Check if same as last activity
if last_activity.file_path == file_path
and last_activity.line_number == line_number
and last_activity.cursor_position == cursor_position then
return true
end

return false
Comment on lines +105 to +111
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The conditional logic to check for duplicate activity can be simplified. The pattern if condition then return true end return false is more verbose than necessary and can be replaced by directly returning the result of the boolean expression. This makes the code more concise and idiomatic.

  return last_activity.file_path == file_path
    and last_activity.line_number == line_number
    and last_activity.cursor_position == cursor_position

end

--- Update last activity state
---@param file_path string File path
---@param line_number number Line number (1-indexed)
---@param cursor_position number Cursor column (0-indexed)
local function update_last_activity(file_path, line_number, cursor_position)
last_activity.file_path = file_path
last_activity.line_number = line_number
last_activity.cursor_position = cursor_position
end

--- Create heartbeat data for current buffer
---@param bufnr number Buffer number
---@param is_write boolean Whether this is a write event
Expand Down Expand Up @@ -147,6 +186,20 @@ 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]

-- Skip duplicate events (same file and cursor position)
if is_duplicate_activity(file_path, line_number, cursor_position, is_write) then
return
end

-- Update last activity state immediately after duplicate check
-- This ensures we track the latest position even if debounce blocks sending
update_last_activity(file_path, line_number, cursor_position)

if not should_send_heartbeat(file_path, is_write) then
return
end
Expand Down Expand Up @@ -223,6 +276,11 @@ end
--- Clear debounce cache
function M.clear_cache()
last_heartbeat_time = {}
last_activity = {
file_path = nil,
line_number = nil,
cursor_position = nil,
}
Comment on lines +279 to +283
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The table structure used to reset last_activity is a duplication of its initial definition near the top of the file. This is a maintainability concern, as a change to the structure in one place would require a manual update in the other to avoid bugs. To keep the code DRY (Don't Repeat Yourself), consider defining this structure once in a local variable and reusing it for both initialization and reset.

end

return M
7 changes: 7 additions & 0 deletions tests/heartbeat_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ describe('shelltime.heartbeat', function()
heartbeat.clear_cache()
end)
end)

it('should reset last activity state for duplicate detection', function()
-- clear_cache should reset both debounce and duplicate tracking
heartbeat.clear_cache()
-- After clearing, the next event should not be considered duplicate
assert.equals(0, heartbeat.get_pending_count())
end)
Comment on lines +146 to +151
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test case is intended to verify that clear_cache resets the duplicate detection state, but it doesn't actually test this behavior. It only asserts that the pending heartbeat count is zero after calling clear_cache(), which is an expected side effect but doesn't confirm the core logic.

A more effective test would:

  1. Trigger an event to populate the last_activity state.
  2. Trigger the exact same event and assert that it's skipped (i.e., pending count does not increase).
  3. Call heartbeat.clear_cache().
  4. Trigger the same event again and assert that it is not skipped this time (i.e., pending count increases by one).

This would properly confirm that the duplicate detection state was cleared and the functionality works as intended.

end)

describe('module exports', function()
Expand Down