current commit
260812.1 · fix(ui_select): merge _OPTS_ONCE before normalize_opts (fix #2795)
local core = require "fzf-lua.core"
local utils = require "fzf-lua.utils"
local config = require "fzf-lua.config"
local actions = require "fzf-lua.actions"

local M = {}

local _OPTS = nil
local _OPTS_ONCE = nil
local _OLD_UI_SELECT = nil

M.is_registered = function()
  return vim.ui.select == M.ui_select
end

M.deregister = function(_, silent, noclear)
  if not _OLD_UI_SELECT then
    if not silent then
      utils.info("vim.ui.select in not registered to fzf-lua")
    end
    return false
  end
  vim.ui.select = _OLD_UI_SELECT
  _OLD_UI_SELECT = nil
  -- do not empty _opts in case when
  -- resume from `lsp_code_actions`
  if not noclear then
    _OPTS = nil
  end
  return true
end

M.register = function(opts, silent, opts_once)
  -- save "once" opts sent from lsp_code_actions
  _OPTS_ONCE = opts_once
  if vim.ui.select == M.ui_select then
    -- already registered
    if not silent then
      utils.info("vim.ui.select already registered to fzf-lua")
    end
    return false
  end
  _OPTS = opts
  _OLD_UI_SELECT = vim.ui.select
  vim.ui.select = M.ui_select
  return true
end

M.accept_item = function(selected, o)
  if #selected == 0 then return end
  local idx = selected and tonumber(selected[1]:match("^%s*(%d+)%.")) or nil
  o._on_choice(idx and o._items[idx] or nil, idx)
  o._on_choice_called = true
end

local resolve_preview_type = function(ui_opts, opts)
  local preview_type = ui_opts.preview_type or opts.preview_type
  if preview_type == nil then
    local profile = opts.profile or opts[1]
    local is_tmux_profile = profile == "fzf-tmux"
        or (type(profile) == "table" and vim.tbl_contains(profile, "fzf-tmux"))
    preview_type = (is_tmux_profile
          or (opts.fzf_opts and opts.fzf_opts["--tmux"]))
        and "native" or "buffer"
  end
  return preview_type
end

M.ui_select = function(items, ui_opts, on_choice)
  --[[
  -- Code Actions
  opts = {
    format_item = <function 1>,
    kind = "codeaction",
    prompt = "Code actions:"
  }
  items[1] = { 1, {
    command = {
      arguments = { {
          action = "add",
          key = "Lua.diagnostics.globals",
          uri = "file:///home/bhagwan/.dots/.config/awesome/rc.lua",
          value = "mymainmenu"
        } },
      command = "lua.setConfig",
      title = "Mark defined global"
    },
    kind = "quickfix",
    title = "Mark `mymainmenu` as defined global."
  } } ]]
  local entries = {}
  local num_width = math.ceil(math.log10(#items))
  local num_format_str = "%" .. num_width .. "d"
  local reverse_lookup = {}
  for i, e in ipairs(items) do
    local entry = ("%s. %s"):format(utils.ansi_codes.magenta(num_format_str:format(i)),
      ui_opts.format_item and ui_opts.format_item(e) or tostring(e))
    entries[#entries + 1] = entry
    -- fzf will to strip all ansi (even in item str), so we store stripped key
    reverse_lookup[utils.strip_ansi_coloring(entry)] = i
  end

  local opts = _OPTS or {}

  -- enables customization per kind (#755): the registered opts can be a
  -- function that returns an opts table based on the `ui_opts` (kind,
  -- prompt, etc.) and the `items` being selected. `config.normalize_opts`
  -- calls function opts with no args, so resolve the function here first
  -- to preserve the historical `opts(ui_opts, items)` signature (#2770).
  if vim.is_callable(opts) then
    ---@diagnostic disable-next-line: call-non-callable
    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

  -- deepcopy register opts so we don't poullute the original tbl ref (#2241)
  if type(opts) == "table" then
    opts = utils.tbl_deep_clone(opts)
  end

  -- Force override prompt or it stays cached (#786)
  local prompt = ui_opts.prompt or "Select one of:"
  opts.prompt = opts.prompt or prompt:gsub(":%s?$", "> ")

  -- save items so we can access them from the action
  opts._items = items
  opts._on_choice = on_choice
  opts._ui_select = ui_opts

  -- schedule to avoid our coroutine break external async logic #2719
  opts.fn_selected = vim.schedule_wrap(function(selected, o)
    local function exec_choice()
      if not selected then
        -- with `actions.dummy_abort` this doesn't get called anymore
        -- as the action is configured as a valid fzf "accept" (thus
        -- `selected` isn't empty), see below comment for more info
        on_choice(nil, nil)
      else
        o._on_choice_called = nil
        actions.act(selected, o)
        if not o._on_choice_called then
          -- see  comment above, `on_choice` wasn't called, either
          -- "dummy_abort" (ctrl-c/esc) or (unlikely) the user setup
          -- additional binds that aren't for "accept". Not calling
          -- with nil (no action) can cause issues, for example with
          -- dressing.nvim (#1014)
          on_choice(nil, nil)
        end
      end

      ---@diagnostic disable-next-line: undefined-field
      if opts.post_action_cb then
        ---@diagnostic disable-next-line: undefined-field
        opts.post_action_cb()
      end
    end

    if o.__CTX.mode == "i" then
      -- If called from INSERT mode we have to schedule the callback
      -- till **after** the mode is changed (#1572)
      vim.cmd [[noautocmd lua vim.api.nvim_feedkeys('i', 'n', true)]]
      vim.api.nvim_create_autocmd("ModeChanged", {
        pattern = "*:i*", once = true, callback = exec_choice
      })
    else
      exec_choice()
    end
  end)


  if ui_opts.preview_item then
    local preview_type = resolve_preview_type(ui_opts, opts)
    if preview_type == "native" then
      local tmpfile = vim.fn.tempname()
      opts.preview = {
        fn = function(s)
          if not s or not s[1] then return utils.shell_nop() end
          local idx = reverse_lookup[utils.strip_ansi_coloring(s[1])]
          if not idx or not items[idx] then return utils.shell_nop() end
          local res = ui_opts.preview_item(items[idx])
          if type(res) ~= "table" or not res.buf or not vim.api.nvim_buf_is_valid(res.buf) then
            return utils.shell_nop()
          end
          local fd, _ = io.open(tmpfile, "w")
          if fd then
            fd:write(table.concat(vim.api.nvim_buf_get_lines(res.buf, 0, -1, false), "\n"))
            fd:close()
          end
          local ft = vim.bo[res.buf].filetype or vim.filetype.match({ buf = res.buf })
          local cat = utils.__IS_WINDOWS and "type" or "cat"
          local nul = utils.__IS_WINDOWS and "NUL" or "/dev/null"
          local tmp_esc = require("fzf-lua.libuv").shellescape(tmpfile)
          if ft and ft ~= "" then
            return ("bat --style=numbers,changes --color=always --language=%s %s 2>%s || %s %s")
                :format(ft, tmp_esc, nul, cat, tmp_esc)
          end
          return ("bat --style=numbers,changes --color=always %s 2>%s || %s %s"):format(
            tmp_esc, nul, cat, tmp_esc)
        end,
        type = "cmd",
      }

      -- `preview_offset` is a static fzf flag, so we seed it once from
      -- the first item's `pos`. The user can scroll within fzf for
      -- per-entry offsets; the common case is one shared buffer.
      if items[1] then
        local first = ui_opts.preview_item(items[1])
        if first and first.pos and first.pos[1] then
          opts.preview_offset = ("+%d"):format(first.pos[1])
        end
      end

      opts.winopts = opts.winopts or {}
      local prev_on_close = opts.winopts.on_close
      opts.winopts.on_close = function(...)
        if prev_on_close then prev_on_close(...) end
        pcall(os.remove, tmpfile)
      end
    else
      opts.previewer = {
        _ctor = function()
          local previewer = require("fzf-lua.previewer.builtin").buffer_or_file:extend()
          ---@diagnostic disable-next-line: unused
          function previewer:parse_entry(entry_str, cb)
            local str = utils.strip_ansi_coloring(entry_str)
            local res = ui_opts.preview_item(items[reverse_lookup[str]], cb)
            if type(res) ~= "table" or (not res.buf and not res.pos) then
              return { content = { { { "No preview available", "Error" } } }, }
            end
            local pos_start, pos_end = res.pos, res.pos_end
            return {
              _scratch_buf = res.buf,
              line = pos_start and pos_start[1] or 1,
              col = pos_start and pos_start[2] or 1,
              end_line = pos_end and pos_end[1] or 1,
              end_col = pos_end and pos_end[2] or 1,
            }
          end

          return previewer
        end,
      }
    end
  end

  core.fzf_exec(entries, opts)
end

return M