You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 lines
10 KiB
Lua

local path = require "fzf-lua.path"
local libuv = require "fzf-lua.libuv"
local shell = require "fzf-lua.shell"
local utils = require "fzf-lua.utils"
local Object = require "fzf-lua.class"
local Previewer = {}
Previewer.base = Object:extend()
-- Previewer base object
function Previewer.base:new(o, opts)
o = o or {}
self.type = "cmd";
self.cmd = o.cmd;
self.args = o.args or "";
self.opts = opts;
return self
end
function Previewer.base:preview_window(_)
return nil
end
function Previewer.base:preview_offset()
--[[
#
# Explanation of the fzf preview offset options:
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
'--preview-window '~3:+{2}+3/2''
]]
if self.opts.line_field_index then
return ("+{%d}-/2"):format(self.opts.line_field_index)
end
end
function Previewer.base:fzf_delimiter()
if not self.opts.line_field_index then return end
-- set delimiter to ':'
-- entry format is 'file:line:col: text'
local delim = self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"]
if not delim then
delim = '[:]'
elseif not delim:match(":") then
if delim:match("%[.*%]")then
delim = delim:match("(%[.*)%]") .. ':]'
else
-- remove surrounding quotes
delim = delim:match("^'?(.*)'$?") or delim
delim = '[' .. utils.rg_escape(delim):gsub("%]", "\\]") .. ':]'
end
end
return delim
end
-- Generic shell command previewer
Previewer.cmd = Previewer.base:extend()
function Previewer.cmd:new(o, opts)
Previewer.cmd.super.new(self, o, opts)
return self
end
function Previewer.cmd:sh_wrap(cmd, args, action, extra_args)
return "sh -c " .. libuv.shellescape(("%s %s %s `%s`"):format(
cmd, args or "", extra_args or "", action))
end
function Previewer.cmd:cmdline(o)
o = o or {}
o.action = o.action or self:action(o)
return vim.fn.shellescape(self:sh_wrap(self.cmd, self.args, o.action))
end
function Previewer.cmd:action(o)
o = o or {}
local act = shell.raw_action(function (items, _, _)
-- only preview first item
local entry = path.entry_to_file(items[1], self.opts)
return entry.bufname or entry.path
end, self.opts.field_index_expr or "{}", self.opts.debug)
return act
end
-- Specialized bat previewer
Previewer.bat = Previewer.cmd:extend()
function Previewer.bat:new(o, opts)
Previewer.bat.super.new(self, o, opts)
self.theme = o.theme
return self
end
function Previewer.bat:cmdline(o)
o = o or {}
o.action = o.action or self:action(o)
local highlight_line = ""
if self.opts.line_field_index then
highlight_line = string.format("--highlight-line={%d}", self.opts.line_field_index)
end
return vim.fn.shellescape(self:sh_wrap(self.cmd, self.args, o.action, highlight_line))
end
-- Specialized head previewer
Previewer.head = Previewer.cmd:extend()
function Previewer.head:new(o, opts)
Previewer.head.super.new(self, o, opts)
return self
end
function Previewer.head:cmdline(o)
o = o or {}
o.action = o.action or self:action(o)
local lines = "--lines=-0"
-- print all lines instead
-- if self.opts.line_field_index then
-- lines = string.format("--lines={%d}", self.opts.line_field_index)
-- end
return vim.fn.shellescape(self:sh_wrap(self.cmd, self.args, o.action, lines))
end
-- new async_action from nvim-fzf
Previewer.cmd_async = Previewer.base:extend()
function Previewer.cmd_async:new(o, opts)
Previewer.cmd_async.super.new(self, o, opts)
return self
end
local grep_tag = function(file, tag)
local line = 1
local filepath = file
local pattern = utils.rg_escape(tag)
if not pattern or not filepath then return line end
local grep_cmd = vim.fn.executable("rg") == 1
and {"rg", "--line-number"}
or {"grep", "-n", "-P"}
-- ctags uses '$' at the end of short patterns
-- 'rg|grep' does not match these properly when
-- 'fileformat' isn't set to 'unix', when set to
-- 'dos' we need to prepend '$' with '\r$' with 'rg'
-- it is simpler to just ignore it compleley.
--[[ local ff = fileformat(filepath)
if ff == 'dos' then
pattern = pattern:gsub("\\%$$", "\\r%$")
else
pattern = pattern:gsub("\\%$$", "%$")
end --]]
-- equivalent pattern to `rg --crlf`
-- see discussion in #219
pattern = pattern:gsub("\\%$$", "\\r??%$")
local cmd = utils.tbl_deep_clone(grep_cmd)
table.insert(cmd, pattern)
table.insert(cmd, filepath)
local out = utils.io_system(cmd)
if not utils.shell_error() then
line = tonumber(out:match("[^:]+")) or 1
else
utils.warn(("previewer: unable to find pattern '%s' in file '%s'"):format(pattern, file))
end
return line
end
function Previewer.cmd_async:parse_entry_and_verify(entrystr)
local entry = path.entry_to_file(entrystr, self.opts)
local filepath = entry.bufname or entry.path or ''
if self.opts._ctag and entry.line<=1 then
-- tags without line numbers
-- make sure we don't already have line #
-- (in the case the line no. is actually 1)
local line = entry.stripped:match("[^:]+(%d+):")
local ctag = path.entry_to_ctag(entry.stripped, true)
if not line and ctag then
entry.ctag = ctag
entry.line = grep_tag(filepath, entry.ctag)
end
end
local errcmd = nil
-- verify the file exists on disk and is accessible
if #filepath==0 or not vim.loop.fs_stat(filepath) then
errcmd = ('echo "%s: NO SUCH FILE OR ACCESS DENIED"'):format(
filepath and #filepath>0 and vim.fn.shellescape(filepath) or "<null>")
end
return filepath, entry, errcmd
end
function Previewer.cmd_async:cmdline(o)
o = o or {}
local act = shell.preview_action_cmd(function(items)
local filepath, _, errcmd = self:parse_entry_and_verify(items[1])
local cmd = errcmd or ('%s %s %s'):format(
self.cmd, self.args, vim.fn.shellescape(filepath))
-- uncomment to see the command in the preview window
-- cmd = vim.fn.shellescape(cmd)
return cmd
end, "{}", self.opts.debug)
return act
end
Previewer.bat_async = Previewer.cmd_async:extend()
function Previewer.bat_async:new(o, opts)
Previewer.bat_async.super.new(self, o, opts)
self.theme = o.theme
return self
end
function Previewer.bat_async:cmdline(o)
o = o or {}
local act = shell.preview_action_cmd(function(items, fzf_lines)
local filepath, entry, errcmd = self:parse_entry_and_verify(items[1])
local line_range = ''
if entry.ctag then
-- this is a ctag without line numbers, since we can't
-- provide the preview file offset to fzf via the field
-- index expression we use '--line-range' instead
local start_line = math.max(1, entry.line-fzf_lines/2)
local end_line = start_line + fzf_lines-1
line_range = ("--line-range=%d:%d"):format(start_line, end_line)
end
local cmd = errcmd or ('%s %s %s %s %s'):format(
self.cmd, self.args,
self.opts.line_field_index and
("--highlight-line=%d"):format(entry.line) or '',
line_range,
vim.fn.shellescape(filepath))
-- uncomment to see the command in the preview window
-- cmd = vim.fn.shellescape(cmd)
return cmd
end, "{}", self.opts.debug)
return act
end
Previewer.git_diff = Previewer.base:extend()
function Previewer.git_diff:new(o, opts)
Previewer.git_diff.super.new(self, o, opts)
self.cmd_deleted = path.git_cwd(o.cmd_deleted, opts)
self.cmd_modified = path.git_cwd(o.cmd_modified, opts)
self.cmd_untracked = path.git_cwd(o.cmd_untracked, opts)
self.pager = o.pager
do
-- populate the icon mappings
local icons_overrides = o._fn_git_icons and o._fn_git_icons()
self.git_icons = {}
for _, i in ipairs({ "D", "M", "R", "A", "C", "T", "?" }) do
self.git_icons[i] =
icons_overrides and icons_overrides[i] and
utils.lua_regex_escape(icons_overrides[i].icon) or i
end
end
return self
end
function Previewer.git_diff:cmdline(o)
o = o or {}
local act = shell.preview_action_cmd(function(items, fzf_lines, fzf_columns)
if not items or vim.tbl_isempty(items) then
utils.warn("shell error while running preview action.")
return
end
local is_deleted = items[1]:match(self.git_icons['D']..utils.nbsp) ~= nil
local is_modified = items[1]:match("[" ..
self.git_icons['M'] ..
self.git_icons['R'] ..
self.git_icons['A'] ..
self.git_icons['T'] ..
"]" ..utils.nbsp) ~= nil
local is_untracked = items[1]:match("[" ..
self.git_icons['?'] ..
self.git_icons['C'] ..
"]"..utils.nbsp) ~= nil
local file = path.entry_to_file(items[1], self.opts)
local cmd = nil
if is_modified then cmd = self.cmd_modified
elseif is_deleted then cmd = self.cmd_deleted
elseif is_untracked then cmd = self.cmd_untracked end
if not cmd then return "" end
local pager = ""
if self.pager and #self.pager>0 and
vim.fn.executable(self.pager:match("[^%s]+")) == 1 then
pager = '| ' .. self.pager
end
-- with default commands we add the filepath at the end
-- if the user configured a more complex command, e.g.:
-- git_diff = {
-- cmd_modified = "git diff --color HEAD %s | less -SEX"
-- }
-- we use ':format' directly on the user's command, see
-- issue #392 for more info (limiting diff output width)
if not cmd:match("%%s") then
cmd = cmd .. " %s"
end
cmd = cmd:format(vim.fn.shellescape(file.path))
cmd = ("FZF_PREVIEW_LINES=%d;FZF_PREVIEW_COLUMNS=%d;%s %s")
:format(fzf_lines, fzf_columns, cmd, pager)
cmd = 'sh -c ' .. vim.fn.shellescape(cmd)
-- uncomment to see the command in the preview window
-- cmd = vim.fn.shellescape(cmd)
return cmd
-- we need to add '--' to mark the end of command options
-- as git icon customization may contain special shell chars
-- which will otherwise choke our preview cmd ('+', '-', etc)
end, "-- {}", self.opts.debug)
return act
end
Previewer.man_pages = Previewer.base:extend()
function Previewer.man_pages:new(o, opts)
Previewer.man_pages.super.new(self, o, opts)
self.cmd = self.cmd or "man"
return self
end
function Previewer.man_pages:cmdline(o)
o = o or {}
local act = shell.preview_action_cmd(function(items)
-- local manpage = require'fzf-lua.providers.manpages'.getmanpage(items[1])
local manpage = items[1]:match("[^[,( ]+")
local cmd = ("%s %s %s"):format(
self.cmd, self.args, vim.fn.shellescape(manpage))
-- uncomment to see the command in the preview window
-- cmd = vim.fn.shellescape(cmd)
return cmd
end, "{}", self.opts.debug)
return act
end
return Previewer