From f7cc9da1e786d4abdbb7ae9b01af4950c7769086 Mon Sep 17 00:00:00 2001
From: Will Hopkins <willothyh@gmail.com>
Date: Sat, 1 Jun 2024 10:51:04 -0700
Subject: [PATCH] feat: add optional debounce

---
 lua/precognition/init.lua  | 48 ++++++++++++++++++++++++--------------
 lua/precognition/utils.lua | 38 ++++++++++++++++++++++++++++++
 2 files changed, 68 insertions(+), 18 deletions(-)

diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua
index 234370c..37d8f9b 100644
--- a/lua/precognition/init.lua
+++ b/lua/precognition/init.lua
@@ -26,6 +26,7 @@ local M = {}
 ---@field NextParagraph Precognition.HintOpts
 
 ---@class Precognition.Config
+---@field debounce integer
 ---@field startVisible boolean
 ---@field showBlankVirtLine boolean
 ---@field highlightColor vim.api.keyset.highlight
@@ -74,6 +75,7 @@ local defaultHintConfig = {
 
 ---@type Precognition.Config
 local default = {
+    debounce = 0,
     startVisible = true,
     showBlankVirtLine = true,
     highlightColor = { link = "Comment" },
@@ -264,21 +266,6 @@ local function display_marks()
     dirty = false
 end
 
-local function on_cursor_moved(ev)
-    local buf = ev and ev.buf or vim.api.nvim_get_current_buf()
-    if extmark then
-        local ext = vim.api.nvim_buf_get_extmark_by_id(buf, ns, extmark, {
-            details = true,
-        })
-        if ext and ext[1] ~= vim.api.nvim_win_get_cursor(0)[1] - 1 then
-            vim.api.nvim_buf_del_extmark(0, ns, extmark)
-            extmark = nil
-        end
-    end
-    dirty = true
-    display_marks()
-end
-
 local function on_insert_enter(ev)
     if extmark then
         vim.api.nvim_buf_del_extmark(ev.buf, ns, extmark)
@@ -287,6 +274,24 @@ local function on_insert_enter(ev)
     dirty = true
 end
 
+---@param draw fun()
+local function cursor_moved_handler(draw)
+    return function(ev)
+        local buf = ev and ev.buf or vim.api.nvim_get_current_buf()
+        if extmark then
+            local ext = vim.api.nvim_buf_get_extmark_by_id(buf, ns, extmark, {
+                details = true,
+            })
+            if ext and ext[1] ~= vim.api.nvim_win_get_cursor(0)[1] - 1 then
+                vim.api.nvim_buf_del_extmark(0, ns, extmark)
+                extmark = nil
+            end
+        end
+        dirty = true
+        draw()
+    end
+end
+
 local function on_buf_edit()
     apply_gutter_hints(build_gutter_hints())
 end
@@ -318,6 +323,11 @@ function M.show()
     end
     visible = true
 
+    local draw = display_marks
+    if config.debounce > 0 then
+        draw = utils.debounce_trailing(display_marks, config.debounce)
+    end
+
     -- clear the extmark entirely when leaving a buffer (hints should only show in current buffer)
     vim.api.nvim_create_autocmd("BufLeave", {
         group = au,
@@ -328,11 +338,13 @@ function M.show()
         group = au,
         callback = on_buf_edit,
     })
+
     -- clear the extmark when the cursor moves, or when insert mode is entered
     --
     vim.api.nvim_create_autocmd("CursorMoved", {
         group = au,
-        callback = on_cursor_moved,
+        -- callback = on_cursor_moved,
+        callback = cursor_moved_handler(draw),
     })
 
     vim.api.nvim_create_autocmd("InsertEnter", {
@@ -340,7 +352,7 @@ function M.show()
         callback = on_insert_enter,
     })
 
-    display_marks()
+    draw()
 end
 
 --- Disable automatic showing of hints
@@ -396,7 +408,7 @@ local state = {
         return build_gutter_hints
     end,
     on_cursor_moved = function()
-        return on_cursor_moved
+        return cursor_moved_handler(display_marks)
     end,
     extmark = function()
         return extmark
diff --git a/lua/precognition/utils.lua b/lua/precognition/utils.lua
index c01d5fc..2b10e0d 100644
--- a/lua/precognition/utils.lua
+++ b/lua/precognition/utils.lua
@@ -65,4 +65,42 @@ function M.add_multibyte_padding(cur_line, extra_padding, line_len)
     end
 end
 
+---Debounces calls to a function, and ensures it only runs once per delay
+---even if called repeatedly.
+---@param fn fun(...: any)
+---@param delay integer
+function M.debounce_trailing(fn, delay)
+    local running = false
+    local timer = assert(vim.uv.new_timer())
+
+    -- Ugly hack to ensure timer is closed when the function is garbage collected
+    -- unfortunate but necessary to avoid creating a new timer for each call.
+    --
+    -- In LuaJIT, only userdata can have finalizers. `newproxy` creates an opaque userdata
+    -- which we can attach a finalizer to and use as a "canary."
+    local proxy = newproxy(true)
+    getmetatable(proxy).__gc = function()
+        if not timer:is_closing() then
+            timer:close()
+        end
+    end
+
+    return function(...)
+        local _ = proxy
+        if running then
+            return
+        end
+        running = true
+        local args = { ... }
+        timer:start(
+            delay,
+            0,
+            vim.schedule_wrap(function()
+                fn(unpack(args))
+                running = false
+            end)
+        )
+    end
+end
+
 return M