260812.1 · 3a91d37663c9e627fd21d0cc30d0dfa67d0d6341
fix(ui_select): merge _OPTS_ONCE before normalize_opts (fix #2795)
Regression from 4a602e1d ("fix(ui_select): add native preview for fzf-native/fzf-tmux"): `M.ui_select` started normalizing the registered opts via `config.normalize_opts(opts, "ui_select")` while the `_OPTS_ONCE` merge (sent from `lsp_code_actions`) still happened afterwards, inside an `elseif` branch of the `ui_opts.preview_item` check.
This caused two breakages with `FzfLua.lsp_code_actions({no_resume=true})`:
1. `no_resume` was invisible to the "hide" profile's `enrich` (which runs
during `normalize_opts` and skips only when `opts.no_resume` or
`opts.no_hide` are set). Esc (and every other non-reuse action) got
wrapped with `fzf.hide()`, so the picker window hid instead of closing
and `resume` brought back a picker the user explicitly asked not to
resume.
2. On Neovim >= 0.12 `vim.lsp.buf.code_action` passes `preview_item` to
`vim.ui.select`, taking the `if ui_opts.preview_item` branch, which
skipped the `elseif _OPTS_ONCE` merge entirely and silently dropped
the once-opts (`no_resume`, etc.) on the floor.
Fix:
- Move the `_OPTS_ONCE` resolution/merge (including the auto-detected
`kind == "codeaction"` path and the non-codeaction fallback
deregister) above `config.normalize_opts`, so merged values such as
`no_resume` are visible to profile `enrich` functions.
- The merge now runs regardless of `ui_opts.preview_item`, so once-opts
apply to both the native and buffer preview variants.
- Limit the "disable hide profile" default (`no_hide`, #2439) to
`ui_opts.kind == "codeaction"` and set it before `normalize_opts`
(post-merge) so the hide `enrich` actually sees it, while an explicit
`no_hide = false` from `lsp_code_actions` opts is still respected.
Non-codeaction `vim.ui.select` kinds are no longer forced to
`no_hide`, i.e. they can be hidden/resumed with the "hide" profile
like any other picker.
Verified with the default profile (includes "hide"):
- `lsp_code_actions({no_resume=true})`: esc/enter close the window,
`FzfLua resume` reports "No resume data available."
- `lsp_code_actions()` without `no_resume` behaves as before.
- Normal `vim.ui.select` (non-codeaction) still hides on esc and
resumes via unhide; selecting an item still calls `on_choice`.
reading declarations
Changes to lua/fzf-lua/providers/ui_select.lua. All files
lua/fzf-lua/providers/ui_select.lua
@@ -112,6 +112,66 @@ M.ui_select = function(items, ui_opts, on_choice) opts = opts(ui_opts, items) end + -- ui.select is code actions+ -- inherit from defaults if not triggered by lsp_code_actions+ ---@type 'error'|'keep'|'force'+ local opts_merge_strategy = "keep"++ -- fix error when vim.lsp.buf.code_action() called but didn't triggers vim.ui.select+ -- _OPTS_ONCE also means pending deregister+ -- since we only use it to custom codeaction preview now+ if _OPTS_ONCE and ui_opts.kind ~= "codeaction" then+ M.deregister({}, true, true)+ _OPTS_ONCE = nil+ return vim.ui.select(items, ui_opts, on_choice)+ end++ if not _OPTS_ONCE and ui_opts.kind == "codeaction" then+ ---@type fzf-lua.config.LspCodeActions+ _OPTS_ONCE = config.normalize_opts({}, "lsp.code_actions")+ if not _OPTS_ONCE then return end+ -- auto-detected code actions, prioritize the ui_select+ -- options over `lsp.code_actions` (#999)+ opts_merge_strategy = "force"+ end++ if _OPTS_ONCE then+ -- merge and clear the once opts sent from lsp_code_actions **before**+ -- normalize so the merge result (e.g. `no_resume`) is visible to the+ -- profile enrich functions (e.g. "hide" profile skips when+ -- `no_resume|no_hide` are set) (#2795)+ local opts_once = _OPTS_ONCE+ local previewer = opts_once.previewer+ opts_once.previewer = nil -- can't copy the previewer object+ ---@diagnostic disable-next-line: param-type-mismatch+ ---@diagnostic disable-next-line: assign-type-mismatch+ ---@diagnostic disable-next-line: generic-constraint-mismatch+ opts = vim.tbl_deep_extend(opts_merge_strategy, opts_once, opts)+ ---@cast opts table+ -- We also override actions to guarantee a single default+ -- action, otherwise selected[1] will be empty due to+ -- multiple keybinds trigger, sending `--expect` to fzf+ opts.actions = vim.tbl_deep_extend("force", opts.actions or {},+ { ["enter"] = opts.actions and opts.actions.enter or nil })+ opts.previewer = previewer+ -- Callback to set the coroutine so we know if the interface+ -- was opened or not (e.g. when no code actions are present)+ opts.cb_co = function(co)+ ---@diagnostic disable-next-line: inject-field+ opts_once._co = co+ end+ _OPTS_ONCE = nil+ end++ -- disable hide profile unless specifically requested, must be set+ -- prior to `normalize_opts` as the "hide" profile's `enrich` needs+ -- this to skip setting `<Esc>` to hide (post-merge so that an+ -- explicit `no_hide=false` sent via `_OPTS_ONCE` is respected)+ if ui_opts.kind == "codeaction" then+ -- casues issues with abort as on_choice(nil) won't be called (#2439)+ opts.no_hide = opts.no_hide == nil and true or opts.no_hide+ end+ opts = config.normalize_opts(opts, "ui_select") if not opts then return end @@ -170,29 +230,6 @@ M.ui_select = function(items, ui_opts, on_choice) end) - -- ui.select is code actions- -- inherit from defaults if not triggered by lsp_code_actions- ---@type 'error'|'keep'|'force'- local opts_merge_strategy = "keep"-- -- fix error when vim.lsp.buf.code_action() called but didn't triggers vim.ui.select- -- _OPTS_ONCE also means pending deregister- -- since we only use it to custom codeaction preview now- if _OPTS_ONCE and ui_opts.kind ~= "codeaction" then- M.deregister({}, true, true)- _OPTS_ONCE = nil- return vim.ui.select(items, ui_opts, on_choice)- end-- if not _OPTS_ONCE and ui_opts.kind == "codeaction" then- ---@type fzf-lua.config.LspCodeActions- _OPTS_ONCE = config.normalize_opts({}, "lsp.code_actions")- if not _OPTS_ONCE then return end- -- auto-detected code actions, prioritize the ui_select- -- options over `lsp.code_actions` (#999)- opts_merge_strategy = "force"- end- if ui_opts.preview_item then local preview_type = resolve_preview_type(ui_opts, opts) if preview_type == "native" then@@ -266,36 +303,8 @@ M.ui_select = function(items, ui_opts, on_choice) end, } end- elseif _OPTS_ONCE then- -- merge and clear the once opts sent from lsp_code_actions.- -- We also override actions to guarantee a single default- -- action, otherwise selected[1] will be empty due to- -- multiple keybinds trigger, sending `--expect` to fzf- local previewer = _OPTS_ONCE.previewer- _OPTS_ONCE.previewer = nil -- can't copy the previewer object- ---@diagnostic disable-next-line: param-type-mismatch- ---@diagnostic disable-next-line: assign-type-mismatch- ---@diagnostic disable-next-line: generic-constraint-mismatch- opts = vim.tbl_deep_extend(opts_merge_strategy, _OPTS_ONCE, opts)- ---@cast opts table- opts.actions = vim.tbl_deep_extend("force", opts.actions or {},- { ["enter"] = opts.actions.enter })- opts.previewer = previewer- -- Callback to set the coroutine so we know if the interface- -- was opened or not (e.g. when no code actions are present)- opts.cb_co = (function()- -- NOTE: use clojure as `_OPTS_ONCE` is otherwise nullified- local opts_once_ref = _OPTS_ONCE- ---@diagnostic disable-next-line: inject-field- return function(co) opts_once_ref._co = co end- end)()- _OPTS_ONCE = nil end - -- disable hide profile unless specifically requested- -- casues issues with abort as on_choice(nil) won't be called (#2439)- opts.no_hide = opts.no_hide == nil and true or opts.no_hide- core.fzf_exec(entries, opts) end