diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index d9cb5f8..d03158a 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -22,6 +22,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 @@ -70,6 +71,7 @@ local defaultHintConfig = { ---@type Precognition.Config local default = { + debounce = 0, startVisible = true, showBlankVirtLine = true, highlightColor = { link = "Comment" }, @@ -279,21 +281,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) @@ -302,6 +289,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 @@ -333,29 +338,44 @@ function M.show() end visible = true + local prev_line + local draw = display_marks + if config.debounce > 0 then + local debounced = utils.debounce_trailing(display_marks, config.debounce) + draw = function(...) + local line = vim.api.nvim_win_get_cursor(0)[1] + if line == prev_line then + display_marks() + else + prev_line = line + end + debounced(...) + end + end + + -- clear and redraw the hints when the cursor moves + vim.api.nvim_create_autocmd("CursorMoved", { + group = au, + callback = cursor_moved_handler(draw), + }) + -- clear the extmark entirely when leaving a buffer (hints should only show in current buffer) vim.api.nvim_create_autocmd("BufLeave", { group = au, callback = on_buf_leave, }) + -- clear the extmark when the cursor moves in insert mode vim.api.nvim_create_autocmd("CursorMovedI", { 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, - }) - vim.api.nvim_create_autocmd("InsertEnter", { group = au, callback = on_insert_enter, }) - display_marks() + draw() end --- Disable automatic showing of hints @@ -411,7 +431,22 @@ local state = { return build_gutter_hints end, on_cursor_moved = function() - return on_cursor_moved + local prev_line + local draw = display_marks + if config.debounce > 0 then + local debounced = utils.debounce_trailing(display_marks, config.debounce) + draw = function(...) + local line = vim.api.nvim_win_get_cursor(0)[1] + if line == prev_line then + display_marks() + else + prev_line = line + end + debounced(...) + end + end + + return cursor_moved_handler(draw) end, extmark = function() return extmark diff --git a/lua/precognition/utils.lua b/lua/precognition/utils.lua index 256ee9a..2bd129d 100644 --- a/lua/precognition/utils.lua +++ b/lua/precognition/utils.lua @@ -69,4 +69,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 diff --git a/tests/precognition/e2e_spec.lua b/tests/precognition/e2e_spec.lua index 242839e..a214509 100644 --- a/tests/precognition/e2e_spec.lua +++ b/tests/precognition/e2e_spec.lua @@ -146,6 +146,47 @@ describe("e2e tests", function() end end) + it("supports debounce", function() + precognition.setup({ + debounce = 200, + }) + + local buffer = vim.api.nvim_create_buf(true, false) + vim.api.nvim_set_current_buf(buffer) + vim.api.nvim_buf_set_lines( + buffer, + 0, + -1, + false, + { "Hello World this is a test", "line 2", "", "line 4", "", "line 6" } + ) + + precognition.on_cursor_moved() + + assert.is_nil(precognition.extmark) + + eq( + {}, + vim.api.nvim_buf_get_extmarks(buffer, precognition.ns, 0, -1, { + details = true, + }) + ) + + local co = coroutine.running() + coroutine.yield(vim.defer_fn(function() + coroutine.resume(co) + end, 210)) + + assert.not_nil(precognition.extmark) + + local extmarks = vim.api.nvim_buf_get_extmark_by_id(buffer, precognition.ns, precognition.extmark, { + details = true, + }) + + eq(vim.api.nvim_win_get_cursor(0)[1] - 1, extmarks[1]) + eq("^ e w $", extmarks[3].virt_lines[1][1][1]) + end) + it("virtual line text color can be customised", function() precognition.setup({ highlightColor = { link = "Function" } }) local buffer = vim.api.nvim_create_buf(true, false)