Optimize Snacks.util.bo with buffer option caching to improve file picker performance

Co-authored-by: spenrose <481185+spenrose@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-06-22 17:05:17 +00:00
parent e270b8c003
commit cc21f180b0
2 changed files with 154 additions and 2 deletions

View file

@ -11,6 +11,10 @@ local uv = vim.uv or vim.loop
local key_cache = {} ---@type table<string, string>
local langs = {} ---@type table<string, boolean>
-- Cache for buffer options to avoid redundant API calls
local buf_options_cache = {} ---@type table<number, table<string, any>>
local buf_cache_autocmd_id = nil
---@alias snacks.util.hl table<string, string|vim.api.keyset.highlight>
local hl_groups = {} ---@type table<string, vim.api.keyset.highlight>
@ -89,8 +93,49 @@ end
---@param buf number
---@param bo vim.bo|{}
function M.bo(buf, bo)
for k, v in pairs(bo or {}) do
vim.api.nvim_set_option_value(k, v, { buf = buf })
if not bo or not next(bo) then
return
end
-- Ensure buffer is valid
if not vim.api.nvim_buf_is_valid(buf) then
-- Clean up cache for invalid buffer
buf_options_cache[buf] = nil
return
end
-- Initialize cache for this buffer if not exists
if not buf_options_cache[buf] then
buf_options_cache[buf] = {}
end
local cache = buf_options_cache[buf]
for k, v in pairs(bo) do
-- Only set option if value has changed
if cache[k] ~= v then
local ok, err = pcall(vim.api.nvim_set_option_value, k, v, { buf = buf })
if ok then
cache[k] = v
else
-- If setting option failed, don't cache the value
-- This ensures we'll try again next time
cache[k] = nil
end
end
end
-- Setup autocmd to clean up cache when buffers are deleted (only once)
if not buf_cache_autocmd_id then
buf_cache_autocmd_id = vim.api.nvim_create_autocmd("BufDelete", {
group = vim.api.nvim_create_augroup("snacks_util_buf_cache", { clear = true }),
callback = function(args)
local deleted_buf = args.buf
if buf_options_cache[deleted_buf] then
buf_options_cache[deleted_buf] = nil
end
end,
})
end
end

View file

@ -37,3 +37,110 @@ describe("util.normkey", function()
end)
end
end)
describe("util.bo", function()
local util = require("snacks.util")
-- Helper to count actual API calls by mocking vim.api.nvim_set_option_value
local api_call_count = 0
local original_set_option = vim.api.nvim_set_option_value
before_each(function()
api_call_count = 0
vim.api.nvim_set_option_value = function(...)
api_call_count = api_call_count + 1
return original_set_option(...)
end
end)
after_each(function()
vim.api.nvim_set_option_value = original_set_option
end)
it("should set buffer options correctly", function()
local buf = vim.api.nvim_create_buf(false, true)
util.bo(buf, { buftype = "nofile", filetype = "lua" })
-- Verify options were set
assert.are.equal("nofile", vim.api.nvim_get_option_value("buftype", { buf = buf }))
assert.are.equal("lua", vim.api.nvim_get_option_value("filetype", { buf = buf }))
assert.are.equal(2, api_call_count)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it("should cache options and avoid redundant API calls", function()
local buf = vim.api.nvim_create_buf(false, true)
-- First call should set options
util.bo(buf, { buftype = "nofile", filetype = "lua" })
assert.are.equal(2, api_call_count)
-- Second call with same options should not call API
api_call_count = 0
util.bo(buf, { buftype = "nofile", filetype = "lua" })
assert.are.equal(0, api_call_count)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it("should detect changes and update only changed options", function()
local buf = vim.api.nvim_create_buf(false, true)
-- Set initial options
util.bo(buf, { buftype = "nofile", filetype = "lua" })
assert.are.equal(2, api_call_count)
-- Change only one option
api_call_count = 0
util.bo(buf, { buftype = "nofile", filetype = "javascript" })
assert.are.equal(1, api_call_count) -- Only filetype should be updated
vim.api.nvim_buf_delete(buf, { force = true })
end)
it("should handle empty or nil options", function()
local buf = vim.api.nvim_create_buf(false, true)
-- Should handle nil options
util.bo(buf, nil)
assert.are.equal(0, api_call_count)
-- Should handle empty table
util.bo(buf, {})
assert.are.equal(0, api_call_count)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it("should handle invalid buffers gracefully", function()
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_delete(buf, { force = true })
-- Should not error with invalid buffer
util.bo(buf, { buftype = "nofile" })
assert.are.equal(0, api_call_count)
end)
it("should clean up cache when buffer is deleted", function()
local buf = vim.api.nvim_create_buf(false, true)
-- Set options to populate cache
util.bo(buf, { buftype = "nofile" })
assert.are.equal(1, api_call_count)
-- Delete buffer (this should trigger cache cleanup via autocmd)
vim.api.nvim_buf_delete(buf, { force = true })
-- Processing autocmds
vim.api.nvim_exec_autocmds("BufDelete", { buffer = buf })
-- Create new buffer with same ID shouldn't use old cache
local new_buf = vim.api.nvim_create_buf(false, true)
api_call_count = 0
util.bo(new_buf, { buftype = "nofile" })
assert.are.equal(1, api_call_count) -- Should call API, not use cache
vim.api.nvim_buf_delete(new_buf, { force = true })
end)
end)