Skip to content

Commit 3b90af5

Browse files
committed
fix(timing): matching and timing were overwhelming main loop
1 parent 9176fba commit 3b90af5

13 files changed

Lines changed: 469 additions & 129 deletions

README.md

Lines changed: 239 additions & 99 deletions
Large diffs are not rendered by default.

lua/fuzzy/init.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
local Pool = require("fuzzy.pool")
22
local Registry = require("fuzzy.registry")
33
local Scheduler = require("fuzzy.scheduler")
4+
local group = vim.api.nvim_create_augroup("FUZZYMATCH", { clear = true })
45

56
local M = {
67
config = {},
78
}
89

10+
function M.teardown()
11+
if Scheduler and Scheduler.close then
12+
Scheduler.close()
13+
end
14+
if Registry and Registry.close then
15+
Registry.close()
16+
end
17+
if Pool and Pool.close then
18+
Pool.close()
19+
end
20+
end
21+
922
function M.setup(opts)
1023
M.config = vim.tbl_deep_extend("keep", opts or {}, {
1124
general = {
@@ -63,6 +76,8 @@ function M.setup(opts)
6376

6477
vim.api.nvim_set_hl(0, "SelectLineHighlight", { link = "Normal", default = false })
6578
vim.api.nvim_set_hl(0, "SelectDecoratorDefault", { link = "Normal", default = false })
79+
80+
vim.api.nvim_create_autocmd("VimLeavePre", { group = group, callback = M.teardown })
6681
end
6782

6883
return M

lua/fuzzy/match.lua

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ function Match:_stop_processing()
103103
-- kill the timer if it is still active, this will stop any further processing, we do not wait for the current processing to finish,
104104
-- since it is expected that the callback will handle nil results as a signal that processing was aborted
105105
if vim.loop.is_closing(self._state.timer) == false then
106-
pcall(vim.loop.stop, self._state.timer)
106+
pcall(self._state.timer.stop, self._state.timer)
107+
pcall(self._state.timer.close, self._state.timer)
107108
end
108109
self._state.timer = nil
109110
end
@@ -179,9 +180,11 @@ function Match:_clean_context()
179180
self.transform = nil
180181
end
181182

182-
function Match:_bind_method(method)
183+
function Match:_bind_guarded(method, token)
183184
return function(...)
184-
return method(self, ...)
185+
if self._state.token == token then
186+
return method(self, ...)
187+
end
185188
end
186189
end
187190

@@ -334,15 +337,12 @@ function Match:match(list, pattern, callback, transform)
334337
end
335338
end
336339

337-
-- ensure we drop the old results first, these will be re-referenced only when the matching process finishes after this matching process
338-
-- that is currently being started now
339-
if self.results then
340-
self.results = nil
341-
end
342-
343-
-- init core match context
340+
-- version the current run, into an incrementing token, that is to ensure that older matching does not start operating on new
341+
-- state or on new starts and ensures older ticks get terminated safely
344342
self._state.token = (self._state.token or 0) + 1
345343
local token = self._state.token
344+
345+
-- init core match context
346346
self.list = assert(list)
347347
self.pattern = assert(pattern)
348348
self.callback = assert(callback)
@@ -370,15 +370,22 @@ function Match:match(list, pattern, callback, transform)
370370
-- ensure offset restored
371371
self._state.offset = 0
372372

373+
if self.results then
374+
-- ensure we drop the old results first, these will be re-referenced only when the matching process finishes after this matching process
375+
-- that is currently being started now
376+
self.results = nil
377+
end
378+
373379
if not self._state.accum then
374-
-- prepare accumulator for results
380+
-- the accumulator is an array of 3 sub-arrays, the first sub-array is the matching elements, the second is the matching
381+
-- positions and the third is the score, the 3 sub-arrays are always guaranteed to be of the same size.
375382
self._state.accum = {}
376383
end
377384

378385
if not self._state.chunks then
379386
-- chunks are reused to avoid frequent allocations, they represent the part of the whole source list currently being processed
380387
-- for matches, the chunks are first filled from the source list and then used in the matchfuzzy call
381-
local size = self._options.step
388+
local size = self._options.step or 0
382389
self._state.chunks = Pool.obtain(size)
383390
end
384391

@@ -400,12 +407,9 @@ function Match:match(list, pattern, callback, transform)
400407
self._state.timer = vim.loop.new_timer()
401408
self._state.timer:start(0,
402409
self._options.timer,
403-
vim.schedule_wrap(function()
404-
if self._state.token ~= token then
405-
return
406-
end
407-
Match._match_worker(self)
408-
end)
410+
vim.schedule_wrap(self:_bind_guarded(
411+
Match._match_worker, token
412+
))
409413
)
410414

411415
-- run one cycle immediately now

lua/fuzzy/picker.lua

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ end
313313
function Picker:_input_prompt()
314314
-- debounce the user input to avoid flooding the matching and rendering logic with too many updates, especially when dealing
315315
-- with large result sets or fast typers
316-
return utils.debounce_callback(self._options.prompt_debounce, function(query)
316+
local debounced = utils.debounce_callback(self._options.prompt_debounce, function(query)
317317
self.select:move_top()
318318
self.select:toggle_clear()
319319

@@ -410,11 +410,18 @@ function Picker:_input_prompt()
410410
self:_running_match()
411411
end
412412
end)
413+
414+
return function(query)
415+
if query ~= nil then
416+
self.match:stop()
417+
end
418+
return debounced(query)
419+
end
413420
end
414421

415422
function Picker:_create_stage()
416423
local function _input_prompt()
417-
return utils.debounce_callback(self._options.prompt_debounce, function(query)
424+
local debounced = utils.debounce_callback(self._options.prompt_debounce, function(query)
418425
local stage = self._state.stage
419426
assert(stage and next(stage))
420427
stage.select:move_top()
@@ -460,6 +467,16 @@ function Picker:_create_stage()
460467
end
461468
end
462469
end)
470+
471+
return function(query)
472+
if query ~= nil then
473+
local state = self._state.stage
474+
if state and state.match then
475+
state.match:stop()
476+
end
477+
end
478+
return debounced(query)
479+
end
463480
end
464481

465482
local function _cancel_prompt()
@@ -575,11 +592,13 @@ end
575592

576593
function Picker:_matching_worker(mode, list, match_query, total, __type)
577594
if __type == 1 then
595+
-- stop any ongoing matcher work so the debounced call can run promptly
578596
if self._matching_worker_debounced == nil then
579597
self._matching_worker_debounced = utils.debounce_callback(
580598
self._options.stream_debounce, self._matching_worker
581599
)
582600
end
601+
self.match:stop()
583602
self._matching_worker_debounced(
584603
self, mode, list,
585604
match_query, total

lua/fuzzy/pool.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,13 @@ function Pool.prune(now)
319319
end
320320
end
321321

322+
--- Stop and close the prune timer.
323+
function Pool.close()
324+
if Pool.prune_timer and not vim.uv.is_closing(Pool.prune_timer) then
325+
pcall(Pool.prune_timer.stop, Pool.prune_timer)
326+
pcall(Pool.prune_timer.close, Pool.prune_timer)
327+
end
328+
Pool.prune_timer = nil
329+
end
330+
322331
return Pool

lua/fuzzy/registry.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,13 @@ function Registry.prune(now)
116116
end
117117
end
118118

119+
--- Stop and close the prune timer.
120+
function Registry.close()
121+
if Registry.prune_timer and not vim.uv.is_closing(Registry.prune_timer) then
122+
pcall(Registry.prune_timer.stop, Registry.prune_timer)
123+
pcall(Registry.prune_timer.close, Registry.prune_timer)
124+
end
125+
Registry.prune_timer = nil
126+
end
127+
119128
return Registry

lua/fuzzy/scheduler.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ end
5454
--- @param async Async Async object to add to the scheduler
5555
function Scheduler.add(async)
5656
assert(async and async._step)
57+
if not Scheduler._executor then
58+
Scheduler.new({})
59+
end
5760
table.insert(Scheduler._queue, async)
5861
if not Scheduler._executor:is_active() then
5962
local wrapped = vim.schedule_wrap(Scheduler.step)
@@ -74,4 +77,15 @@ function Scheduler.get(thread)
7477
return nil
7578
end
7679

80+
--- Stop and close the scheduler executor.
81+
function Scheduler.close()
82+
if Scheduler._executor and not vim.uv.is_closing(Scheduler._executor) then
83+
pcall(Scheduler._executor.stop, Scheduler._executor)
84+
pcall(Scheduler._executor.close, Scheduler._executor)
85+
end
86+
Scheduler._executor = nil
87+
Scheduler._queue = nil
88+
Scheduler._budget = nil
89+
end
90+
7791
return Scheduler

lua/fuzzy/select.lua

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ local detailed_extmark_opts = { limit = 4, type = "highlight", details = true, h
1010
local padding = { " ", "SelectHeaderPadding" }
1111
local spacing = { ",", "SelectHeaderDelimiter" }
1212

13+
local PREVIEW_EVENTIGNORE = table.concat({
14+
"FileType",
15+
"BufEnter",
16+
"BufLeave",
17+
"BufWinEnter",
18+
"BufWinLeave",
19+
"WinEnter",
20+
"WinLeave",
21+
"WinNew",
22+
"WinClosed",
23+
"CursorMoved",
24+
"CursorMovedI",
25+
"CursorHold",
26+
"CursorHoldI",
27+
}, ",")
28+
1329
local utils = require("fuzzy.utils")
1430
local Async = require("fuzzy.async")
1531
local Worker = require("fuzzy.worker")
@@ -850,7 +866,7 @@ local function display_default(content, window, buffer)
850866
end
851867
if buffer ~= nil and vim.api.nvim_buf_is_valid(buffer) then
852868
local old_ignore = vim.o.eventignore
853-
vim.o.eventignore = "all"
869+
vim.o.eventignore = PREVIEW_EVENTIGNORE
854870
pcall(populate_buffer, buffer, content)
855871
vim.api.nvim_win_set_buf(window, buffer)
856872
vim.api.nvim_win_set_cursor(window, { 1, 0 })
@@ -868,7 +884,7 @@ local function display_entry(previewer, entry, window, buffer)
868884
return
869885
end
870886
local old_ignore = vim.o.eventignore
871-
vim.o.eventignore = "all"
887+
vim.o.eventignore = PREVIEW_EVENTIGNORE
872888
local ok, res, msg = pcall(previewer.preview, previewer, entry, window)
873889
if not ok then
874890
display_default({ res or msg or "Unable to preview current entry" }, window, buffer)
@@ -2409,15 +2425,17 @@ end
24092425
--- Checks if the selection interface is currently open, this is determined by checking if both the prompt and list windows are valid.
24102426
--- @return boolean True if the selection interface is open, false otherwise.
24112427
function Select:isopen()
2412-
local prompt = self.prompt_window and vim.api.nvim_win_is_valid(self.prompt_window)
2428+
local prompt = not (self._options.prompt_input ~= false)
2429+
or (self.prompt_window and vim.api.nvim_win_is_valid(self.prompt_window))
24132430
local list = self.list_window and vim.api.nvim_win_is_valid(self.list_window)
24142431
return list ~= nil and prompt ~= nil and list and prompt
24152432
end
24162433

24172434
--- Checks if the selection interface is valid, meaning that it has been initialized/opened at least once and has not been destroyed by calling the `close` method which would invalidate its curent state
24182435
--- @return boolean True if the select interface is still valid, false otherwise.
24192436
function Select:isvalid()
2420-
local prompt = self.prompt_buffer and vim.api.nvim_buf_is_valid(self.prompt_buffer)
2437+
local prompt = not (self._options.prompt_input ~= false)
2438+
or (self.prompt_buffer and vim.api.nvim_buf_is_valid(self.prompt_buffer))
24212439
local list = self.list_buffer and vim.api.nvim_buf_is_valid(self.list_buffer)
24222440
return list ~= nil and prompt ~= nil and list and prompt
24232441
end
@@ -2468,6 +2486,9 @@ function Select:list(entries, positions)
24682486
self._state.positions = positions
24692487
self._state.entries = entries
24702488
self._state.streaming = true
2489+
if not self:isvalid() then
2490+
return
2491+
end
24712492
self:_render_list()
24722493
elseif positions == nil then
24732494
self._state.streaming = false
@@ -2573,8 +2594,14 @@ function Select:open()
25732594
end
25742595

25752596
if not self._state.renderer then
2597+
-- every select has a stateful renderer, in this case this rednerer worker ensures that only the last render job that is
2598+
-- queried is run, that makes sure that intermediate render jobs that would otherwise be overridden immediately by new
2599+
-- ones are discarded. This is the worker.coalesce job type
25762600
self._state.renderer = Worker.coalesce()
25772601
end
2602+
2603+
-- the source window has to be remembered, that source windo is the source from which the selection interface was initiated,
2604+
-- that is then later used for a target for future select actions that are performed by the interface
25782605
self.source_window = vim.api.nvim_get_current_win()
25792606
local factor = (opts.prompt_list and opts.preview) and 2.0 or 1.0
25802607
local size = compute_height(opts.window_ratio, factor)
@@ -2596,7 +2623,7 @@ function Select:open()
25962623
if self._state.query ~= query then
25972624
self:_prompt_input(query, opts.prompt_input)
25982625
if query == nil then self:close() end
2599-
self._state.query = query
2626+
self._state.query = query or ""
26002627
end
26012628
end
26022629
})
@@ -2740,7 +2767,7 @@ function Select:open()
27402767
pattern = "*",
27412768
callback = function()
27422769
if vim.tbl_contains(vim.v.event.windows, self.list_window) and
2743-
not self:_is_rendering() and vim.api.nvim_buf_line_count(list_buffer) >= 1
2770+
self:isvalid() and vim.api.nvim_buf_line_count(list_buffer) >= 1
27442771
then
27452772
utils.timed_call(Select._render_list, self, false)
27462773
end

0 commit comments

Comments
 (0)