Skip to content

Bug: Timer API mismatch in selection.lua - mixing vim.defer_fn with vim.loop.timer_stop #172

@pranaygp

Description

@pranaygp

Description

There's a timer API mismatch in lua/claudecode/selection.lua that mixes incompatible timer APIs. The code uses vim.defer_fn() to create a debounce timer but then calls vim.loop.timer_stop() to stop it. These are different timer systems:

  • vim.defer_fn() returns a timer handle that should be stopped using timer:stop() and timer:close()
  • vim.loop.timer_stop() expects a libuv timer created with vim.loop.new_timer() or vim.uv.new_timer()

Affected Code

File: lua/claudecode/selection.lua

Location 1: debounce_update() function (lines ~109-118)

function M.debounce_update()
  if M.state.debounce_timer then
    vim.loop.timer_stop(M.state.debounce_timer)  -- ❌ Wrong API
  end

  M.state.debounce_timer = vim.defer_fn(function()  -- Returns different timer type
    M.update_selection()
    M.state.debounce_timer = nil
  end, M.state.debounce_ms)
end

Location 2: disable() function (lines ~48-51)

if M.state.debounce_timer then
  vim.loop.timer_stop(M.state.debounce_timer)  -- ❌ Wrong API
  M.state.debounce_timer = nil
end

Potential Impact

This could cause undefined behavior or crashes, especially in newer Neovim versions where the timer handle types may have changed. In Neovim 0.11.x, vim.defer_fn returns a userdata object that may not be compatible with vim.loop.timer_stop().

Suggested Fix

Use consistent timer APIs. Either:

Option A: Use vim.uv.new_timer() consistently:

function M.debounce_update()
  if M.state.debounce_timer then
    M.state.debounce_timer:stop()
    M.state.debounce_timer:close()
    M.state.debounce_timer = nil
  end

  local timer = vim.uv.new_timer()
  M.state.debounce_timer = timer
  timer:start(M.state.debounce_ms, 0, vim.schedule_wrap(function()
    if M.state.debounce_timer == timer then
      M.state.debounce_timer = nil
    end
    timer:stop()
    timer:close()
    M.update_selection()
  end))
end

Option B: Use vim.fn.timer_stop() with vim.defer_fn:

function M.debounce_update()
  if M.state.debounce_timer then
    vim.fn.timer_stop(M.state.debounce_timer)
  end

  M.state.debounce_timer = vim.defer_fn(function()
    M.update_selection()
    M.state.debounce_timer = nil
  end, M.state.debounce_ms)
end

Environment

  • Neovim: 0.11.5
  • OS: macOS

Full Diff (Option A approach)

diff --git a/lua/claudecode/selection.lua b/lua/claudecode/selection.lua
index 9bbfed9..ac21196 100644
--- a/lua/claudecode/selection.lua
+++ b/lua/claudecode/selection.lua
@@ -46,7 +46,8 @@ function M.disable()
   M.server = nil
 
   if M.state.debounce_timer then
-    vim.loop.timer_stop(M.state.debounce_timer)
+    M.state.debounce_timer:stop()
+    M.state.debounce_timer:close()
     M.state.debounce_timer = nil
   end
 end
@@ -108,13 +109,21 @@ end
 ---its execution.
 function M.debounce_update()
   if M.state.debounce_timer then
-    vim.loop.timer_stop(M.state.debounce_timer)
+    M.state.debounce_timer:stop()
+    M.state.debounce_timer:close()
+    M.state.debounce_timer = nil
   end
 
-  M.state.debounce_timer = vim.defer_fn(function()
+  local timer = vim.uv.new_timer()
+  M.state.debounce_timer = timer
+  timer:start(M.state.debounce_ms, 0, vim.schedule_wrap(function()
+    if M.state.debounce_timer == timer then
+      M.state.debounce_timer = nil
+    end
+    timer:stop()
+    timer:close()
     M.update_selection()
-    M.state.debounce_timer = nil
-  end, M.state.debounce_ms)
+  end))
 end

Found while debugging an unrelated Neovim crash issue.
** This issue was detected and opened by 🤖 Claude Opus

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions