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.

406 lines
13 KiB
Lua

local M = {}
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local config = nil
-- attempt to load the current config
-- should fail if we're running headless
do
local ok, module = pcall(require, "fzf-lua.config")
if ok then config = module end
end
-- These globals are set by spawn.fn_transform loadstring
---@diagnostic disable-next-line: undefined-field
M._fzf_lua_server = _G._fzf_lua_server
---@diagnostic disable-next-line: undefined-field
M._devicons_path = _G._devicons_path
---@diagnostic disable-next-line: undefined-field
M._devicons_setup = _G._devicons_setup
local function load_config_section(s, datatype, optional)
if config then
local keys = utils.strsplit(s, '.')
local iter, sect = config, nil
for i=1,#keys do
iter = iter[keys[i]]
if not iter then break end
if i == #keys and type(iter) == datatype then
sect = iter
end
end
return sect
elseif M._fzf_lua_server then
-- load config from our running instance
local res = nil
local is_bytecode = false
local exec_str, exec_opts = nil, nil
if datatype == 'function' then
is_bytecode = true
exec_opts = { s, datatype }
exec_str = ("return require'fzf-lua'.config.bytecode(...)"):format(s)
else
exec_opts = {}
exec_str = ("return require'fzf-lua'.config.%s"):format(s)
end
local ok, errmsg = pcall(function()
local chan_id = vim.fn.sockconnect("pipe", M._fzf_lua_server, { rpc = true })
res = vim.rpcrequest(chan_id, "nvim_exec_lua", exec_str, exec_opts)
vim.fn.chanclose(chan_id)
end)
if ok and is_bytecode then
ok, res = pcall(loadstring, res)
end
if not ok and not optional then
io.stderr:write(("Error loading remote config section '%s': %s\n")
:format(s, errmsg))
elseif ok and type(res) == datatype then
return res
end
end
end
local function set_config_section(s, data)
if M._fzf_lua_server then
-- save config in our running instance
local ok, errmsg = pcall(function()
local chan_id = vim.fn.sockconnect("pipe", M._fzf_lua_server, { rpc = true })
vim.rpcrequest(chan_id, "nvim_exec_lua", ([[
local data = select(1, ...)
require'fzf-lua'.config.%s = data
]]):format(s), { data })
vim.fn.chanclose(chan_id)
end)
if not ok then
io.stderr:write(("Error setting remote config section '%s': %s\n")
:format(s, errmsg))
end
return ok
elseif config then
local keys = utils.strsplit(s, '.')
local iter = config
for i=1,#keys do
iter = iter[keys[i]]
if not iter then break end
if i == #keys-1 then
iter[keys[i+1]] = data
return iter
end
end
end
end
-- Setup the terminal colors codes for nvim-web-devicons colors
local setup_devicon_term_hls = function()
local function hex(hexstr)
local r,g,b = hexstr:match('.(..)(..)(..)')
r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
return r, g, b
end
for _, info in pairs(M._devicons.get_icons()) do
local r, g, b = hex(info.color)
utils.add_ansi_code('DevIcon' .. info.name, string.format('[38;2;%s;%s;%sm', r, g, b))
end
end
local function load_devicons()
if config and config._has_devicons then
-- file was called from the primary instance
-- acquire nvim-web-devicons from config
M._devicons = config._devicons
elseif M._devicons_path and vim.loop.fs_stat(M._devicons_path) then
-- file was called from a headless instance
-- load nvim-web-devicons manually
-- add nvim-web-devicons path to `package.path`
-- so `require("nvim-web-devicons")` can find it
package.path = (";%s/?.lua;"):format(vim.fn.fnamemodify(M._devicons_path, ':h'))
.. package.path
M._devicons = require("nvim-web-devicons")
-- WE NO LONGER USE THIS, LEFT FOR DOCUMENTATION
-- loading with 'require' is needed, 'loadfile'
-- cannot load a custom setup function as it's
-- considered a separate instance and the inner
-- 'require' in the setup fill will create an
-- additional 'nvim-web-devicons' instance
--[[ local file = loadfile(M._devicons_path)
M._devicons = file and file() ]]
-- did caller specify a custom setup function?
-- must be called before the next step as `setup`
-- is ignored when called the second time
M._devicons_setup = M._devicons_setup and vim.fn.expand(M._devicons_setup)
if M._devicons and M._devicons_setup and vim.loop.fs_stat(M._devicons_setup) then
local file = loadfile(M._devicons_setup)
if file then file() end
end
end
if M._devicons and M._devicons.setup and not M._devicons.has_loaded() then
-- if the caller has devicons lazy loaded
-- running without calling setup will generate an error:
-- nvim-web-devicons.lua:972: E5560:
-- nvim_command must not be called in a lua loop callback
M._devicons.setup()
end
if M._devicons and M._devicons.has_loaded() then
-- Setup devicon terminal ansi color codes
setup_devicon_term_hls()
end
end
-- Load remote config and devicons
pcall(load_devicons)
if not config then
local _config = { globals = { git = {}, files = {}, grep = {} } }
_config.globals.git.icons = load_config_section('globals.git.icons', 'table') or {}
_config.globals.file_icon_colors = load_config_section('globals.file_icon_colors', 'table') or {}
_config.globals.file_icon_padding = load_config_section('globals.file_icon_padding', 'string')
_config.globals.files.git_status_cmd = load_config_section('globals.files.git_status_cmd', 'table')
_config.globals.grep.rg_glob_fn = load_config_section('globals.grep.rg_glob_fn', 'function', true)
_config.globals.nbsp = load_config_section('globals.nbsp', 'string')
if _config.globals.nbsp then utils.nbsp = _config.globals.nbsp end
config = _config
end
M.get_devicon = function(file, ext)
local icon, hl
if M._devicons then
icon, hl = M._devicons.get_icon(file, ext:lower(), {default = true})
else
icon, hl = '', 'dark_grey'
end
-- allow user override of the color
local override = config.globals.file_icon_colors
and config.globals.file_icon_colors[ext]
if override then
hl = override
end
if config.globals.file_icon_padding and
#config.globals.file_icon_padding>0 then
icon = icon .. config.globals.file_icon_padding
end
return icon, hl
end
M.get_diff_files = function(opts)
local diff_files = {}
local cmd = opts.git_status_cmd or config.globals.files.git_status_cmd
if not cmd then return {} end
local ok, status, err = pcall(utils.io_systemlist, path.git_cwd(cmd, opts))
if ok and err == 0 then
for i = 1, #status do
local icon = status[i]:match("[MUDARCT?]+")
local file = status[i]:match("[^ ]*$")
if icon and file then
diff_files[file] = icon:gsub("%?%?", "?")
end
end
end
return diff_files
end
M.glob_parse = function(query, opts)
if not query or not query:find(opts.glob_separator) then
return query, nil
end
if config.globals.grep.rg_glob_fn then
return config.globals.grep.rg_glob_fn(query, opts)
end
local glob_args = ""
local search_query, glob_str = query:match("(.*)"..opts.glob_separator.."(.*)")
for _, s in ipairs(utils.strsplit(glob_str, "%s")) do
glob_args = glob_args .. ("%s %s ")
:format(opts.glob_flag, vim.fn.shellescape(s))
end
return search_query, glob_args
end
M.preprocess = function(opts)
if opts.cwd_only and not opts.cwd then
opts.cwd = vim.loop.cwd()
end
if opts.git_icons then
opts.diff_files = M.get_diff_files(opts)
end
local argv = function(i, debug)
-- argv1 is actually the 7th argument if we count
-- arguments already supplied by 'wrap_spawn_stdio'
-- if no index was supplied use the last argument
local idx = tonumber(i) and tonumber(i)+6 or #vim.v.argv
if debug then
io.stdout:write(("[DEBUG]: argv(%d) = %s\n")
:format(idx, vim.fn.shellescape(vim.v.argv[idx])))
end
return vim.v.argv[idx]
end
-- live_grep replace pattern with last argument
local argvz = "{argvz}"
local has_argvz = opts.cmd and opts.cmd:match(argvz)
-- save our last search argument for resume
if opts.argv_expr and has_argvz then
local query = argv(nil, opts.debug)
set_config_section('__resume_data.last_query', query)
if opts.__module__ then
set_config_section(("globals.%s._last_search"):format(opts.__module__),
{ query = query, no_esc = true })
end
end
-- did the caller request rg with glob support?
-- mannipulation needs to be done before the argv hack
if opts.rg_glob and has_argvz then
local query = argv()
local search_query, glob_args = M.glob_parse(query, opts)
if glob_args then
-- gsub doesn't like single % on rhs
search_query = search_query:gsub("%%", "%%%%")
-- reset argvz so it doesn't get replaced again below
opts.cmd = opts.cmd:gsub(argvz,
glob_args .. vim.fn.shellescape(search_query))
end
end
-- nifty hack to avoid having to double escape quotations
-- see my comment inside 'live_grep' initial_command code
if opts.argv_expr then
opts.cmd = opts.cmd:gsub("{argv.*}",
function(x)
local idx = x:match("{argv(.*)}")
return vim.fn.shellescape(argv(idx))
end)
end
return opts
end
M.lcol = function(entry, opts)
if not entry then return nil end
local filename = entry.filename or vim.api.nvim_buf_get_name(entry.bufnr)
return string.format("%s:%s:%s:%s%s",
-- uncomment to test URIs
-- "file://" .. filename,
filename, --utils.ansi_codes.magenta(filename),
utils.ansi_codes.green(tostring(entry.lnum)),
utils.ansi_codes.blue(tostring(entry.col)),
entry.text and #entry.text>0 and " " or "",
not entry.text and "" or
(opts and opts.trim_entry and vim.trim(entry.text)) or entry.text)
end
local COLON_BYTE = string.byte(":")
M.file = function(x, opts)
opts = opts or {}
local ret = {}
local icon, hl
local colon_idx = utils.find_next_char(x, COLON_BYTE) or 0
local file_part = colon_idx>1 and x:sub(1, colon_idx-1) or x
local rest_of_line = colon_idx>1 and x:sub(colon_idx) or nil
-- strip ansi coloring from path so we can use filters
-- otherwise the ANSI escape sequnce will get in the way
-- TODO: we only support path modfication without ANSI
-- escape sequences, it becomes too expensive to modify
-- and restore the path with escape sequences
local filepath, file_is_ansi = utils.strip_ansi_coloring(file_part)
-- fd v8.3 requires adding '--strip-cwd-prefix' to remove
-- the './' prefix, will not work with '--color=always'
-- https://github.com/sharkdp/fd/blob/master/CHANGELOG.md
if not (opts.strip_cwd_prefix == false) and path.starts_with_cwd(filepath) then
filepath = path.strip_cwd_prefix(filepath)
end
-- make path relative
if opts.cwd and #opts.cwd>0 then
filepath = path.relative(filepath, opts.cwd)
end
if path.starts_with_separator(filepath) then
-- filter for cwd only
if opts.cwd_only then
local cwd = opts.cwd or vim.loop.cwd()
if not path.is_relative(filepath, cwd) then
return nil
end
end
-- replace $HOME with ~
filepath = path.HOME_to_tilde(filepath)
end
-- only check for ignored patterns after './' was
-- stripped and path was transformed to relative
if opts.file_ignore_patterns then
for _, pattern in ipairs(opts.file_ignore_patterns) do
if #pattern>0 and filepath:match(pattern) then
return nil
end
end
end
-- only shorten after we're done with all the filtering
-- save a copy for git indicator and icon lookups
local origpath = filepath
if opts.path_shorten then
filepath = path.shorten(filepath, tonumber(opts.path_shorten))
end
if opts.git_icons then
local indicators = opts.diff_files and opts.diff_files[origpath] or utils.nbsp
for i=1,#indicators do
icon = indicators:sub(i,i)
local git_icon = config.globals.git.icons[icon]
if git_icon then
icon = git_icon.icon
if opts.color_icons then
icon = utils.ansi_codes[git_icon.color or "dark_grey"](icon)
end
end
ret[#ret+1] = icon
end
ret[#ret+1] = utils.nbsp
end
if opts.file_icons then
local filename = path.tail(origpath)
local ext = path.extension(filename)
icon, hl = M.get_devicon(filename, ext)
if opts.color_icons then
-- extra workaround for issue #119 (or similars)
-- use default if we can't find the highlight ansi
local fn = utils.ansi_codes[hl] or utils.ansi_codes['dark_grey']
icon = fn(icon)
end
ret[#ret+1] = icon
ret[#ret+1] = utils.nbsp
end
ret[#ret+1] = file_is_ansi>0 and file_part or filepath
ret[#ret+1] = rest_of_line
return table.concat(ret)
end
M.tag = function(x, opts)
local name, file, text = x:match("([^\t]+)\t([^\t]+)\t(.*)")
if not file or not name or not text then return x end
text = text:match('(.*);"') or text -- remove ctag comments
-- unescape ctags special chars
-- '\/' -> '/'
-- '\\' -> '\'
for _, s in ipairs({ '/', '\\' }) do
text = text:gsub([[\]]..s, s)
end
local line, tag = text:match("(%d-);?(/.*/)")
line = line and #line>0 and tonumber(line)
return ("%s%s: %s %s"):format(
M.file(file, opts),
not line and "" or ":"..utils.ansi_codes.green(tostring(line)),
utils.ansi_codes.magenta(name),
utils.ansi_codes.green(tag))
, line
end
return M