current commit
261101.1 · fix(git): fix fish preview quoting for special char paths
---@diagnostic disable: param-type-mismatch
local utils = require "fzf-lua.utils"
local shell = require "fzf-lua.shell"
local native = require("fzf-lua.previewer.fzf")
local builtin = require("fzf-lua.previewer.builtin")

local M = {}

-- Thanks to @aznhe21's `actions-preview.nvim` for the diff generation code
-- https://github.com/aznhe21/actions-preview.nvim/blob/master/lua/actions-preview/action.lua
local function get_lines(bufnr)
  vim.fn.bufload(bufnr)
  return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
end

local function get_eol(bufnr)
  local ff = vim.bo[bufnr].fileformat
  if ff == "dos" then
    return "\r\n"
  elseif ff == "unix" then
    return "\n"
  elseif ff == "mac" then
    return "\r"
  else
    error("invalid fileformat")
  end
end

local function diff_text_edits(text_edits, bufnr, offset_encoding, diff_opts)
  local eol = get_eol(bufnr)
  local orig_lines = get_lines(bufnr)
  local tmpbuf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, orig_lines)
  -- deepcopy is necessary due to the apply_text_edits's logic to change the passed edits.
  vim.lsp.util.apply_text_edits(utils.deepcopy(text_edits), tmpbuf, offset_encoding)
  local new_lines = get_lines(tmpbuf)
  vim.api.nvim_buf_delete(tmpbuf, { force = true })
  ---@diagnostic disable-next-line: deprecated
  local diff = (vim.diff or vim.text.diff)(
    table.concat(orig_lines, eol) .. eol,
    table.concat(new_lines, eol) .. eol,
    diff_opts)
  ---@cast diff string
  -- Windows: some LSPs use "\n" for EOL (e.g clangd)
  -- remove both "\n" and "\r\n" (#1172)
  return utils.strsplit(vim.trim(diff), "\r?\n")
end

-- based on `vim.lsp.util.apply_text_document_edit`
-- https://github.com/neovim/neovim/blob/v0.9.2/runtime/lua/vim/lsp/util.lua#L576
local function diff_text_document_edit(text_document_edit, offset_encoding, diff_opts)
  local text_document = text_document_edit.textDocument
  local bufnr = vim.uri_to_bufnr(text_document.uri)

  return diff_text_edits(text_document_edit.edits, bufnr, offset_encoding, diff_opts)
end

-- based on `vim.lsp.util.apply_workspace_edit`
-- https://github.com/neovim/neovim/blob/v0.9.4/runtime/lua/vim/lsp/util.lua#L848
local function diff_workspace_edit(workspace_edit, offset_encoding, diff_opts)
  local diff = {}
  if workspace_edit.documentChanges then
    for _, change in ipairs(workspace_edit.documentChanges) do
      -- imitate git diff
      if change.kind == "rename" then
        local old_path = vim.fn.fnamemodify(vim.uri_to_fname(change.oldUri), ":.")
        local new_path = vim.fn.fnamemodify(vim.uri_to_fname(change.newUri), ":.")

        table.insert(diff, string.format("diff --code-actions a/%s b/%s", old_path, new_path))
        table.insert(diff, string.format("rename from %s", old_path))
        table.insert(diff, string.format("rename to %s", new_path))
        table.insert(diff, "")
      elseif change.kind == "create" then
        local path = vim.fn.fnamemodify(vim.uri_to_fname(change.uri), ":.")

        table.insert(diff, string.format("diff --code-actions a/%s b/%s", path, path))
        -- delta needs file mode
        table.insert(diff, "new file mode 100644")
        -- diff-so-fancy needs index
        table.insert(diff, "index 0000000..fffffff")
        table.insert(diff, "")
      elseif change.kind == "delete" then
        local path = vim.fn.fnamemodify(vim.uri_to_fname(change.uri), ":.")

        table.insert(diff, string.format("diff --code-actions a/%s b/%s", path, path))
        table.insert(diff, string.format("--- a/%s", path))
        table.insert(diff, "+++ /dev/null")
        table.insert(diff, "")
      elseif change.kind then
        -- do nothing
      else
        local path = vim.fn.fnamemodify(vim.uri_to_fname(change.textDocument.uri), ":.")

        table.insert(diff, string.format("diff --code-actions a/%s b/%s", path, path))
        table.insert(diff, string.format("--- a/%s", path))
        table.insert(diff, string.format("+++ b/%s", path))
        for _, l in ipairs(diff_text_document_edit(change, offset_encoding, diff_opts) or {}) do
          table.insert(diff, l)
        end
        table.insert(diff, "")
        table.insert(diff, "")
      end
    end

    return diff
  end

  local all_changes = workspace_edit.changes
  if all_changes and not utils.tbl_isempty(all_changes) then
    for uri, changes in pairs(all_changes) do
      local path = vim.fn.fnamemodify(vim.uri_to_fname(uri), ":.")
      local bufnr = vim.uri_to_bufnr(uri)

      table.insert(diff, string.format("diff --code-actions a/%s b/%s", path, path))
      table.insert(diff, string.format("--- a/%s", path))
      table.insert(diff, string.format("+++ b/%s", path))
      for _, l in ipairs(diff_text_edits(changes, bufnr, offset_encoding, diff_opts) or {}) do
        table.insert(diff, l)
      end
      table.insert(diff, "")
      table.insert(diff, "")
    end
  end

  return diff
end

---@param err? { code: integer, message: string }?
---@param tuple? [integer, lsp.CodeAction]
---@param diff_opts vim.text.diff.Opts
---@return string[]
local function diff_tuple(err, tuple, diff_opts)
  if err then
    return {
      string.format('"codeAction/resolve" failed with error %d: %s', err.code, err.message)
    }
  end ---@cast tuple [integer, lsp.CodeAction]
  local action = tuple[2]
  if action.edit then
    local client = assert(utils.lsp_get_clients({ id = tuple[1] })[1])
    return diff_workspace_edit(action.edit, client.offset_encoding, diff_opts)
  else
    local command = type(action.command) == "table" and action.command or action
    return {
      string.format(
        "Code action preview is only available for document/workspace edits (%s).",
        command and type(command.command) == "string"
        and string.format("command:%s", command.command)
        or string.format("kind:%s", action.kind))
    }
  end
end

-- https://github.com/neovim/neovim/blob/v0.9.4/runtime/lua/vim/lsp/buf.lua#L666
---@param self fzf-lua.previewer.CodeActionBuiltin
---@param idx integer
---@param callback? function
---@return string[]
local function preview_action_tuple(self, idx, callback)
  local tuple = self.opts._items[idx]
  -- neovim changed the ui.select params with 0.10.0 (#947)
  -- { client_id, action } ==> { ctx = <LSP context>, action = <action> }
  if tuple.ctx then
    tuple = { tuple.ctx.client_id, tuple.action }
  end
  -- First check our resolved action cache, if "codeAction/resolve" failed, ignore
  -- the error (we already alerted the user about it in `handle_resolved_response`)
  -- and display the default "unsupported" message from the original action
  if self._resolved_actions[idx] then
    local resolved = self._resolved_actions[idx]
    return diff_tuple(nil, not resolved.err and resolved.tuple or tuple, self.diff_opts)
  end
  -- Not found in cache, check if the client supports code action resolving
  local client_id = tuple[1]
  local client = assert(utils.lsp_get_clients({ id = client_id })[1])
  local action = tuple[2]
  local supports_resolve = utils.__HAS_NVIM_010
      -- runtime/lua/lsp/buf.lua:on_user_choice
      and (function()
        ---@var choice {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext}
        ---@diagnostic disable-next-line: deprecated
        local ms = vim.lsp.protocol.Methods or require("vim.lsp.protocol").Methods
        local choice = self.opts._items[idx]
        local bufnr = assert(choice.ctx.bufnr, "Must have buffer number")
        local reg = client.dynamic_capabilities:get(ms.textDocument_codeAction, { bufnr = bufnr })
        return utils.tbl_get(reg or {}, "registerOptions", "resolveProvider")
            or client:supports_method(ms.codeAction_resolve)
      end)()
      -- prior to nvim 0.10 we could check `client.server_capabilities`
      or utils.tbl_get(client.server_capabilities, "codeActionProvider", "resolveProvider")
  if not action.edit and client and supports_resolve then
    -- Action is not a workspace edit, attempt to resolve the code action
    -- in case it resolves to a workspace edit
    local function handle_resolved_response(err, resolved_action)
      if err then
        -- alert the user "codeAction/resolve" request  failed
        utils.warn(diff_tuple(err, nil, self.diff_opts)[1])
      end
      local resolved = {
        err = err,
        -- Due to a bug in `typescript-tools.nvim` only the first call to `codeAction/resolve`
        -- returns a valid action (non-nil), return nil tuple if the action is nil (#949)
        tuple = resolved_action and { client_id, resolved_action } or nil
      }
      self._resolved_actions[idx] = resolved
      -- HACK: due to upstream bug with jdtls calling resolve messes
      -- errs the workspace edit with "-32603: Internal error." (#1007)
      if not err and client.name == "jdtls" then
        if utils.__HAS_NVIM_010 then
          self.opts._items[idx].action = resolved_action
        else
          self.opts._items[idx][2] = resolved_action
        end
      end
      return resolved.tuple
    end
    if callback then
      client:request("codeAction/resolve", action, function(err, resolved_action)
        local resolved_tuple = handle_resolved_response(err, resolved_action)
        callback(nil, not err and resolved_tuple or tuple)
      end)
      return { string.format("Resolving action (%s)...", action.kind) }
    else
      local res = client:request_sync("codeAction/resolve", action)
      local err, resolved_action = res and res.err, res and res.result
      local resolved_tuple = handle_resolved_response(err, resolved_action)
      return diff_tuple(nil, not err and resolved_tuple or tuple, self.diff_opts)
    end
  else
    return diff_tuple(nil, tuple, self.diff_opts)
  end
end

---@class fzf-lua.previewer.CodeActionBuiltin: fzf-lua.previewer.Builtin,{}
---@field super fzf-lua.previewer.Builtin
---@field diff_opts vim.text.diff.Opts
---@field _resolved_actions table<integer, (false|{ err: any, tuple: any? })?>
M.builtin = builtin.base:extend()
M.builtin.preview_action_tuple = preview_action_tuple

function M.builtin:new(o, opts, fzf_win)
  assert(opts._ui_select and opts._ui_select.kind == "codeaction")
  M.builtin.super.new(self, o, opts, fzf_win)
  setmetatable(self, M.builtin)
  self.diff_opts = o.diff_opts
  self._resolved_actions = {}
  for i, _ in ipairs(self.opts._items) do
    self._resolved_actions[i] = false
  end
  return self
end

function M.builtin:gen_winopts()
  local winopts = {
    wrap       = false,
    cursorline = false,
    number     = false
  }
  return vim.tbl_extend("keep", winopts, self.winopts)
end

function M.builtin:populate_preview_buf(entry_str)
  if not self.win or not self.win:validate_preview() then return end
  local idx = tonumber(entry_str:match("^%s*(%d+)%."))
  assert(type(idx) == "number")
  local lines = self:preview_action_tuple(idx,
    -- use the async version for "codeAction/resolve"
    function(err, resolved_tuple)
      if vim.api.nvim_buf_is_valid(self.tmpbuf) then
        vim.api.nvim_buf_set_lines(self.tmpbuf, 0, -1, false,
          diff_tuple(err, resolved_tuple, self.diff_opts))
      end
    end)
  self.tmpbuf = self:get_tmp_buffer()
  vim.api.nvim_buf_set_lines(self.tmpbuf, 0, -1, false, lines)
  vim.bo[self.tmpbuf].filetype = "git"
  self:set_preview_buf(self.tmpbuf)
  self.win:update_preview_title(string.format(" Action #%d ", idx))
  self.win:update_preview_scrollbar()
end

---@class fzf-lua.previewer.CodeActionNative: fzf-lua.previewer.Fzf,{}
---@field super fzf-lua.previewer.Fzf
M.native = native.base:extend()
M.native.preview_action_tuple = preview_action_tuple

---@param o fzf-lua.config.CodeActionPreviewer
---@param opts fzf-lua.config.LspCodeActions
---@return fzf-lua.previewer.CodeActionNative
function M.native:new(o, opts)
  assert(opts._ui_select and opts._ui_select.kind == "codeaction")
  M.native.super.new(self, o, opts)
  setmetatable(self, M.native)
  local pager = opts.preview_pager == nil and o.pager or opts.preview_pager
  if type(pager) == "function" then pager = pager(opts) end
  ---@cast pager string
  local cmd = pager and pager:match("[^%s]+") or nil
  if cmd and vim.fn.executable(cmd) == 1 then self.pager = pager end
  self.diff_opts = o.diff_opts
  self._resolved_actions = {}
  ---@diagnostic disable-next-line: undefined-field
  for i, _ in ipairs(self.opts._items) do
    self._resolved_actions[i] = false
  end
  return self
end

function M.native:cmdline(o)
  o = o or {}
  local act = shell.stringify_data(function(entries, _, _)
    ---@diagnostic disable-next-line: undefined-field
    if not entries[1] then return shell.nop() end
    local idx = utils.tointeger(entries[1]:match("^%s*%d+%."))
    assert(idx)
    local lines = self:preview_action_tuple(idx)
    return table.concat(lines, "\r\n")
  end, self.opts, "{}")
  if self.pager then
    act = act .. " | " .. utils._if_win_normalize_vars(self.pager)
  end
  return act
end

return M