perf(picker): gc optims

This commit is contained in:
Folke Lemaitre 2025-01-21 16:36:11 +01:00
parent c1e2c619b2
commit 3fa2ea3115
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
8 changed files with 76 additions and 29 deletions

View file

@ -87,7 +87,7 @@ local M = {}
---@field confirm? snacks.picker.Action.spec shortcut for confirm action
---@field auto_confirm? boolean automatically confirm if there is only one item
---@field main? snacks.picker.main.Config main editor window config
---@field on_change? fun(picker:snacks.Picker, item:snacks.picker.Item) called when the cursor changes
---@field on_change? fun(picker:snacks.Picker, item?:snacks.picker.Item) called when the cursor changes
---@field on_show? fun(picker:snacks.Picker) called when the picker is shown
---@field jump? snacks.picker.jump.Config|{}
--- Other

View file

@ -34,6 +34,14 @@ function M:count()
return #self.items
end
function M:close()
self.task:abort()
self.task = Async.nop()
self._find = function()
return {}
end
end
---@param search string
function M:changed(search)
search = vim.trim(search)

View file

@ -23,11 +23,11 @@ function M.new(picker)
height = 1,
text = picker.opts.live and self.filter.search or self.filter.pattern,
ft = "regex",
on_win = function()
vim.fn.prompt_setprompt(self.win.buf, "")
self.win:focus()
vim.cmd.startinsert()
vim.api.nvim_win_set_cursor(self.win.win, { 1, #self:get() + 1 })
on_win = function(win)
vim.fn.prompt_setprompt(win.buf, "")
win:focus()
end,
bo = {
filetype = "snacks_picker_input",
@ -40,29 +40,29 @@ function M.new(picker)
},
}))
local ref = picker:ref()
local ref = Snacks.util.ref(self)
self.win:on(
{ "TextChangedI", "TextChanged" },
Snacks.util.throttle(function()
local p = ref()
if not p or not self.win:valid() then
local input = ref()
if not input or not input.win:valid() then
return
end
-- only one line
-- Can happen when someone pastes a multiline string
if vim.api.nvim_buf_line_count(self.win.buf) > 1 then
local line = vim.trim(self.win:text():gsub("\n", " "))
vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, { line })
vim.api.nvim_win_set_cursor(self.win.win, { 1, #line + 1 })
if vim.api.nvim_buf_line_count(input.win.buf) > 1 then
local line = vim.trim(input.win:text():gsub("\n", " "))
vim.api.nvim_buf_set_lines(input.win.buf, 0, -1, false, { line })
vim.api.nvim_win_set_cursor(input.win.win, { 1, #line + 1 })
end
vim.bo[self.win.buf].modified = false
local pattern = self:get()
if p.opts.live then
self.filter.search = pattern
vim.bo[input.win.buf].modified = false
local pattern = input:get()
if input.picker.opts.live then
input.filter.search = pattern
else
self.filter.pattern = pattern
input.filter.pattern = pattern
end
p:match()
input.picker:match()
end, { ms = picker.opts.live and 100 or 30 }),
{ buf = true }
)

View file

@ -132,7 +132,9 @@ function M:count()
end
function M:close()
-- nothing todo. Keep all items so actions can be performed on them,
self.win:destroy()
self.picker = nil
-- Keep all items so actions can be performed on them,
-- even when the picker closed
end

View file

@ -59,6 +59,11 @@ function M:abort()
self.task:abort()
end
function M:close()
self:abort()
self.task = Async.nop()
end
---@param picker snacks.Picker
---@param opts? {prios?: snacks.picker.Item[]}
function M:run(picker, opts)

View file

@ -3,10 +3,12 @@ local Finder = require("snacks.picker.core.finder")
local uv = vim.uv or vim.loop
Async.BUDGET = 10
local _id = 0
---@alias snacks.Picker.ref (fun():snacks.Picker?)|{value?: snacks.Picker}
---@class snacks.Picker
---@field id number
---@field opts snacks.picker.Config
---@field finder snacks.picker.Finder
---@field format snacks.picker.format
@ -38,7 +40,7 @@ M._active = {}
---@class snacks.picker.Last
---@field cursor number
---@field topline number
---@field opts snacks.picker.Config
---@field opts? snacks.picker.Config
---@field selected snacks.picker.Item[]
---@field filter snacks.picker.Filter
@ -51,6 +53,8 @@ M.last = nil
---@param opts? snacks.picker.Config
function M.new(opts)
local self = setmetatable({}, M)
_id = _id + 1
self.id = _id
self.opts = Snacks.picker.config.get(opts)
if self.opts.source == "resume" then
return M.resume()
@ -70,19 +74,23 @@ function M.new(opts)
if picker_count > 0 then
-- clear items from previous pickers for garbage collection
for picker, _ in pairs(M._pickers) do
picker.list.items = {}
picker.list.topk:clear()
picker.finder.items = {}
picker.list.items = {}
picker.list:clear()
picker.list.picker = nil
end
end
if self.opts.debug.leaks then
if self.opts.debug.leaks and picker_count > 0 then
collectgarbage("collect")
picker_count = vim.tbl_count(M._pickers)
if picker_count > 0 then
local pickers = vim.tbl_keys(M._pickers) ---@type snacks.Picker[]
table.sort(pickers, function(a, b)
return a.id < b.id
end)
local lines = { ("# ` %d ` active pickers:"):format(picker_count) }
for picker, _ in pairs(M._pickers) do
for _, picker in ipairs(pickers) do
lines[#lines + 1] = ("- [%s]: **pattern**=%q, **search**=%q"):format(
picker.opts.source or "custom",
picker.input.filter.pattern,
@ -92,7 +100,10 @@ function M.new(opts)
Snacks.notify.error(lines, { title = "Snacks Picker", id = "snacks_picker_leaks" })
Snacks.debug.metrics()
else
Snacks.notifier.hide("snacks_picker_leaks")
Snacks.notify(
"Picker leaks cleared after `collectgarbage`",
{ title = "Snacks Picker", id = "snacks_picker_leaks" }
)
end
end
@ -133,7 +144,7 @@ function M.new(opts)
self.preview = require("snacks.picker.core.preview").new(self.opts, layout.preview == "main" and self.main or nil)
M.last = {
opts = self.opts,
opts = opts,
selected = {},
cursor = self.list.cursor,
filter = self.input.filter,
@ -457,6 +468,15 @@ function M:close()
if self.closed then
return
end
-- FIXME: lsp definitions picker can't be gc-ed without the below,
-- which makes no sense. Need to further investigate.
-- if not self.shown then
-- self.input.win.opts.relative = "editor"
-- self.input.win:show()
-- end
self:hist_record(true)
self.closed = true
M.last.selected = self:selected({ fallback = false })
@ -468,15 +488,22 @@ function M:close()
if is_picker_win and vim.api.nvim_win_is_valid(self.main) then
vim.api.nvim_set_current_win(self.main)
end
self.layout:close()
self.updater:stop()
self.finder:abort()
self.matcher:abort()
M._active[self] = nil
vim.schedule(function()
-- order matters!
self.finder:close()
self.matcher:close()
self.layout:close()
self.list:close()
self.input:close()
self.preview:close()
self.resolved_layout = nil
self.preview = nil
self.matcher = nil
self.updater = nil
self.history = nil
end)
end
@ -487,12 +514,12 @@ end
---@private
function M:progress(ms)
if self.updater:is_active() then
if self.updater:is_active() or self.closed then
return
end
self.updater = vim.defer_fn(function()
self:update()
if self:is_active() then
if not self.closed and self:is_active() then
-- slower progress when we filled topk
local topk, height = self.list.topk:count(), self.list.state.height or 50
self:progress(topk > height and 30 or 10)

View file

@ -140,6 +140,8 @@ function M.request(buf, method, params, cb)
end)
async:suspend()
cancel = {}
async = Async.nop()
end
-- Support for older versions of neovim

View file

@ -489,6 +489,9 @@ end
function M:destroy()
self:close()
self.events = {}
self.keys = {}
self.meta = {}
self.opts = {}
end
---@param opts? { buf: boolean }