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 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 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 "") end return filepath, entry, errcmd end function Previewer.cmd_async:cmdline(o) o = o or {} local act = shell.raw_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.raw_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.raw_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.raw_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