Skip to content

Commit

Permalink
feat(lsp_code_actions): fzf native and builtin previewers (#944)
Browse files Browse the repository at this point in the history
2 new previewers for code actions of type `edit` (diff):
- "codeaction" is the default which displays the diff in the
  "builtin" neovim previewer
- "codeaction_native" uses fzf's native preview ability and is
  recommended when git-delta is installed, by default will
  auto-detect the `delta` binary and set as the preview pager
  Try with `:FzfLua lsp_code_actions previewer=codeaction_native`

Special thanks to @aznhe21 for the diff generation code which
I borrowed from `actions-preview.nvim` plugin:
https://github.com/aznhe21/actions-preview.nvim
  • Loading branch information
ibhagwan committed Dec 7, 2023
1 parent a1a2d0f commit b3b05f9
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 31 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,17 @@ require'fzf-lua'.setup {
-- the preview buffer, define them here instead
-- ext_ft_override = { ["ksql"] = "sql", ... },
},
-- Code Action previewers, default is "codeaction" (set via `lsp.code_actions.previewer`)
-- "codeaction_native" uses fzf's native previewer, recommended when combined with git-delta
codeaction = {
-- options for vim.diff(): https://neovim.io/doc/user/lua.html#vim.diff()
diff_opts = { ctxlen = 3 },
},
codeaction_native = {
diff_opts = { ctxlen = 3 },
pager = vim.fn.executable("delta") == 1
and "delta --width=$FZF_PREVIEW_COLUMNS" or nil,
},
},
-- PROVIDERS SETUP
-- use `defaults` (table or function) if you wish to set "global-provider" defaults
Expand Down Expand Up @@ -1143,11 +1154,10 @@ require'fzf-lua'.setup {
code_actions = {
prompt = 'Code Actions> ',
async_or_timeout = 5000,
winopts = {
row = 0.40,
height = 0.35,
width = 0.60,
},
-- when git-delta is installed use "codeaction_native" for beautiful diffs
-- try it out with `:FzfLua lsp_code_actions previewer=codeaction_native`
-- scroll up to `previewers.codeaction{_native}` for more previewer options
previewer = "codeaction",
},
finder = {
prompt = "LSP Finder> ",
Expand Down
17 changes: 12 additions & 5 deletions lua/fzf-lua/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@ M.defaults = {
title_fnamemodify = function(s) return path.tail(s) end,
_ctor = previewers.builtin.buffer_or_file,
},
codeaction = {
_ctor = previewers.builtin.codeaction,
diff_opts = { ctxlen = 3 },
},
codeaction_native = {
_ctor = previewers.fzf.codeaction,
diff_opts = { ctxlen = 3 },
pager = vim.fn.executable("delta") == 1
and "delta --width=$FZF_PREVIEW_COLUMNS" or nil,
},
},
}

Expand Down Expand Up @@ -687,11 +697,8 @@ M.defaults.lsp.finder = {
M.defaults.lsp.code_actions = {
prompt = "Code Actions> ",
async_or_timeout = 5000,
winopts = {
row = 0.40,
height = 0.35,
width = 0.60,
},
previewer = "codeaction",
-- previewer = "codeaction_native",
}

M.defaults.diagnostics = {
Expand Down
198 changes: 198 additions & 0 deletions lua/fzf-lua/previewer/codeaction.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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.api.nvim_buf_get_option(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)
vim.lsp.util.apply_text_edits(text_edits, tmpbuf, offset_encoding)
local new_lines = get_lines(tmpbuf)
vim.api.nvim_buf_delete(tmpbuf, { force = true })
local diff = vim.diff(
table.concat(orig_lines, eol) .. eol,
table.concat(new_lines, eol) .. eol,
diff_opts)
return utils.strsplit(vim.trim(diff), eol)
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))
table.insert(diff, "new file")
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 vim.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

-- https://github.com/neovim/neovim/blob/v0.9.4/runtime/lua/vim/lsp/buf.lua#L666
local function preview_from_tuple(tuple, diff_opts)
assert(type(tuple) == "table" and #tuple > 1)
local action = tuple[2]
if action.edit then
local client = assert(vim.lsp.get_client_by_id(tuple[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/worksapce edits (%s).",
command and type(command.command) == "string"
and string.format("command:%s", command.command)
or string.format("kind:%s", action.kind))
}
end
end


M.builtin = builtin.base:extend()

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
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("^%d+%."))
assert(type(idx) == "number")
local tuple = self.opts._items[idx]
local lines = preview_from_tuple(tuple, self.diff_opts)
self.tmpbuf = self:get_tmp_buffer()
vim.api.nvim_buf_set_lines(self.tmpbuf, 0, -1, false, lines)
vim.api.nvim_buf_set_option(self.tmpbuf, "filetype", "git")
self:set_preview_buf(self.tmpbuf)
self.win:update_title(string.format(" Action #%d ", idx))
self.win:update_scrollbar()
end

M.native = native.base:extend()

function M.native:new(o, opts, fzf_win)
assert(opts._ui_select and opts._ui_select.kind == "codeaction")
M.native.super.new(self, o, opts, fzf_win)
setmetatable(self, M.native)
self.pager = o.pager or opts.preview_pager
self.diff_opts = o.diff_opts
return self
end

function M.native:cmdline(o)
o = o or {}
local act = shell.raw_action(function(entries, _, _)
local idx = tonumber(entries[1]:match("^%d+%."))
assert(type(idx) == "number")
local tuple = self.opts._items[idx]
local lines = preview_from_tuple(tuple, self.diff_opts)
return table.concat(lines, "\r\n")
end, "{}", self.opts.debug)
if self.pager and #self.pager > 0 and vim.fn.executable(self.pager:match("[^%s]+")) == 1 then
act = act .. " | " .. self.pager
end
return act
end

return M
2 changes: 2 additions & 0 deletions lua/fzf-lua/previewer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Previewer.fzf.bat_async = function() return require "fzf-lua.previewer.fzf".bat_
Previewer.fzf.git_diff = function() return require "fzf-lua.previewer.fzf".git_diff end
Previewer.fzf.man_pages = function() return require "fzf-lua.previewer.fzf".man_pages end
Previewer.fzf.help_tags = function() return require "fzf-lua.previewer.fzf".help_tags end
Previewer.fzf.codeaction = function() return require "fzf-lua.previewer.codeaction".native end

Previewer.builtin = {}
Previewer.builtin.buffer_or_file = function()
Expand All @@ -23,5 +24,6 @@ Previewer.builtin.quickfix = function() return require "fzf-lua.previewer.builti
Previewer.builtin.highlights = function() return require "fzf-lua.previewer.builtin".highlights end
Previewer.builtin.autocmds = function() return require "fzf-lua.previewer.builtin".autocmds end
Previewer.builtin.keymaps = function() return require "fzf-lua.previewer.builtin".keymaps end
Previewer.builtin.codeaction = function() return require "fzf-lua.previewer.codeaction".builtin end

return Previewer
1 change: 0 additions & 1 deletion lua/fzf-lua/providers/lsp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,6 @@ M.code_actions = function(opts)
if not has_code_actions then return end
end

opts.previewer = false
opts.actions = opts.actions or {}
opts.actions.default = nil
-- only dereg if we aren't registered
Expand Down
47 changes: 27 additions & 20 deletions lua/fzf-lua/providers/ui_select.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ M.register = function(opts, silent, opts_once)
end

M.accept_item = function(selected, o)
local idx = selected and tonumber(selected[1]:match("^(%d+).")) or nil
local idx = selected and tonumber(selected[1]:match("^(%d+)%.")) or nil
o._on_choice(idx and o._items[idx] or nil, idx)
end

Expand Down Expand Up @@ -86,59 +86,66 @@ M.ui_select = function(items, ui_opts, on_choice)
ui_opts.format_item and ui_opts.format_item(e) or tostring(e)))
end

local _opts = _OPTS or {}
local opts = _OPTS or {}

-- enables customization per kind (#755)
if type(_opts) == "function" then
_opts = _opts(ui_opts, items)
if type(opts) == "function" then
opts = opts(ui_opts, items)
end

_opts.fzf_opts = vim.tbl_extend("keep", _opts.fzf_opts or {}, {
opts.fzf_opts = vim.tbl_extend("keep", opts.fzf_opts or {}, {
["--no-multi"] = "",
["--preview-window"] = "hidden:right:0",
})

-- Force override prompt or it stays cached (#786)
local prompt = ui_opts.prompt or "Select one of:"
_opts.fzf_opts["--prompt"] = prompt:gsub(":%s?$", "> ")
opts.fzf_opts["--prompt"] = 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
opts._items = items
opts._on_choice = on_choice
opts._ui_select = ui_opts

_opts.actions = vim.tbl_deep_extend("keep",
_opts.actions or {}, { ["default"] = M.accept_item })
opts.actions = vim.tbl_deep_extend("keep",
opts.actions or {}, { ["default"] = M.accept_item })

config.set_action_helpstr(M.accept_item, "accept-item")

_opts.fn_selected = function(selected, o)
config.set_action_helpstr(_opts.actions["default"], nil)
opts.fn_selected = function(selected, o)
config.set_action_helpstr(opts.actions["default"], nil)

if not selected then
on_choice(nil, nil)
else
actions.act(o.actions, selected, o)
end

if _opts.post_action_cb then
_opts.post_action_cb()
if opts.post_action_cb then
opts.post_action_cb()
end
end

-- was this triggered by lsp_code_actions?
local opts_once = _OPTS_ONCE

-- ui.select is code actions
-- inherit from defaults if not triggered by lsp_code_actions
if not _OPTS_ONCE and ui_opts.kind == "codeaction" then
_OPTS_ONCE = config.normalize_opts({}, "lsp.code_actions")
end
if _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
opts_once = vim.tbl_deep_extend("keep", _OPTS_ONCE, _opts)
opts_once.actions = _opts.actions
local previewer = _OPTS_ONCE.previewer
_OPTS_ONCE.previewer = nil -- can't copy the previewer object
opts = vim.tbl_deep_extend("keep", _OPTS_ONCE, opts)
opts.actions = { ["default"] = opts.actions["default"] }
opts.previewer = previewer
_OPTS_ONCE = nil
end

core.fzf_exec(entries, opts_once or _opts)
core.fzf_exec(entries, opts)
end

return M

0 comments on commit b3b05f9

Please sign in to comment.