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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions