-
Notifications
You must be signed in to change notification settings - Fork 233
Advanced
Fzf-lua is highly customizable, almost anything can be changed and almost any conceivable use-case is possible with varying effort/complexity.
Most simple customizations can be achieved using the fzf_exec function:
fzf_exec = function(contents, [opts])-
contents: contents of the fzf interface, depending on the argument type can behave differently:-
string: piped shell command -
table: array of strings (lines) -
function: function with data callback
-
-
opts: optional table containing all possible fzf-lua settings (prompt,winopts,fzf_opts,keymap, etc), consult README.md#customization for all possible options, partial list below:-
cwd: working directory context for shell commands -
prompt: fzf's prompt -
actions: map of keybinds to function handlers -
fn_transform: only called when content is of typestring, a function that transforms each output line, can be used to add coloring, text manipulation, etc. -
silent_fail[default:true]: when a shell command exist with an error code (e.g. a failedrgsearch), fzf will print[Command failed: <command>]to the info line, this can sometimes be confusing with fzf-lua as some commands are used inside aneovim --headlesswrapper, set this tofalseto display the failed message
-
The most basic example is feeding an array of strings into fzf:
:lua require'fzf-lua'.fzf_exec({ "line1", "line2" })For more complex use-cases one can also use a function with a data feed
callback, each call to fzf_cb below results in a line added to fzf.
Don't forget to call
fzf_cb(nil)to close the fzf named pipe, this signals EOF and terminates the fzf "loading" indicator.
require'fzf-lua'.fzf_exec(function(fzf_cb)
for i=1,10 do
fzf_cb(i)
end
fzf_cb() -- EOF
end)If our function takes a long time to process you'd have a noticeable delay before the fzf interface opens, fortunately, this is quite easy to solve using lua coroutines:
require'fzf-lua'.fzf_exec(function(fzf_cb)
coroutine.wrap(function()
local co = coroutine.running()
for i=1,1234567 do
-- coroutine.resume only gets called once uv.write completes
fzf_cb(i, function() coroutine.resume(co) end)
-- wait here until 'coroutine.resume' is called which only happens
-- once 'uv.write' completes (i.e. the line was written into fzf)
-- this frees neovim to respond and open the UI
coroutine.yield()
end
-- signal EOF to fzf and close the named pipe
-- this also stops the fzf "loading" indicator
fzf_cb()
end)()
end)Note: that due to coroutine.yield(), any call inside the loop to
vim.fn.xxx or vim.api.xxx (and potentially neovim APIs) will fail with:
E5560: vimL function must not be called in a lua loop callback
However, we can use vim.schedule as a workaround to wrap the contents inside
our loop. The example below lists all buffers (prepended with their buffer
number):
require'fzf-lua'.fzf_exec(function(fzf_cb)
coroutine.wrap(function()
local co = coroutine.running()
for _, b in ipairs(vim.api.nvim_list_bufs()) do
vim.schedule(function()
local name = vim.api.nvim_buf_get_name(b)
name = #name>0 and name or "[No Name]"
fzf_cb(b..":"..name, function() coroutine.resume(co) end)
end)
coroutine.yield()
end
fzf_cb()
end)()
end)Fzf's most common usage is piping a shell command, we do so by sending a
string argument as contents, below is the equivalent of running ls | fzf
in the shell:
:lua require'fzf-lua'.fzf_exec("ls")Sometimes we need to transform the output lines, for example, the below prepends the current working directory and colors the command output purple:
require'fzf-lua'.fzf_exec("ls", {
fn_transform = function(x)
return vim.loop.cwd().."/"..require'fzf-lua'.utils.ansi_codes.magenta(x)
end
})We can also use fzf-lua's make_entry to add colorful icons to the output:
require'fzf-lua'.fzf_exec("rg --files", {
fn_transform = function(x)
return require'fzf-lua'.make_entry.file(x, {file_icons=true, color_icons=true})
end
})For a full list of options consult README.md#customization
Any fzf-lua configuration option can be sent along the command:
:lua require'fzf-lua'.fzf_exec("ls", { prompt="LS> ", cwd="~/<folder>" })Or:
:lua require'fzf-lua'.fzf_exec("ls", { winopts = { height=0.33, width=0.66 } })Once items(s) are selected fzf-lua will run an action from the actions table
mapped to the pressed keybind (default gets called on <CR>):
require'fzf-lua'.fzf_exec("rg --files", {
actions = {
-- Use fzf-lua builtin actions or your own handler
['default'] = require'fzf-lua'.actions.file_edit,
['ctrl-y'] = function(selected, opts)
print("selected item:", selected[1])
end
}
})Alternatively, we can use fzf-lua default actions:
require'fzf-lua'.fzf_exec("rg --files", { actions = require'fzf-lua'.defaults.actions.files })Sometimes it is desirable to perform an action and resume immediately, an example use case would be an action that deletes a file and then refreshes the interface.
Although we can call require'fzf-lua'.resume() (:FzfLua resume) in our
action handler it is not optimal as it will "flash" the fzf window (close
followed by open), we can avoid that by supplying a table as our action
handler, this signals fzf-lua to not close the window and wait for a resume:
require'fzf-lua'.fzf_exec("ls", {
actions = {
['ctrl-x'] = {
function(selected)
print("deleting:", selected[1])
-- uncomment to enable deletion
-- vim.fn.delete(selected[1])
end,
require'fzf-lua'.actions.resume
}
}
})Tying it all together, let's create a colored directory switcher, our provider
should recursively search all subdirectories using fd and cd into the
selected directory:
_G.fzf_dirs = function(opts)
local fzf_lua = require'fzf-lua'
opts = opts or {}
opts.prompt = "Directories> "
opts.fn_transform = function(x)
return fzf_lua.utils.ansi_codes.magenta(x)
end
opts.actions = {
['default'] = function(selected)
vim.cmd("cd " .. selected[1])
end
}
fzf_lua.fzf_exec("fd --type d", opts)
end
-- map our provider to a user command ':Directories'
vim.cmd([[command! -nargs=* Directories lua _G.fzf_dirs()]])
-- or to a keybind, both below are (sort of) equal
vim.keymap.set('n', '<C-k>', _G.fzf_dirs)
vim.keymap.set('n', '<C-k>', '<cmd>lua _G.fzf_dirs()<CR>')
-- We can also send call options directly
:lua _G.fzf_dirs({ cwd = <other directory> })With fzf_exec, fzf contents is populated once and the query prompt is used
to fuzzy match on top of the results, but what if we wanted to change the
contents based on the typed query?
fzf-live utilizes fzf's change:reload mechanism (or skim's "interactive"
mode) to generate dynamic contents that changes with each keystrokes:
You can read more about the way this works in fzf#1750 and in Using fzf as interative Ripgrep launcher
fzf_live = function(contents, [opts])-
contents: must be of typefunction(query)and return a value of type:-
string: reload with shell command output -
table: reload from table -
function: reload from function
-
-
opts: optional settings, seefzf_execfor more info, important for this mode are:-
query[default:nil]: initial query -
exec_empty_query[default:false]: determines if the contents function is called with empty queries or only once the user starts typing, set totruewhen it's desirable to callcontentswhen opening the interface (without supplyingopts.query) -
silent_fail[default:true]: when a shell command exist with an error code (e.g. a failedrgsearch), fzf will print[Command failed: <command>]to the info line, this can sometimes be confusing with fzf-lua as some commands are used inside aneovim --headlesswrapper, set this tofalseto display the failed message -
stderr_to_stdout[default:true]: by default fzf is finicky about displaying error messages that are sent tostderr, set totruethis option appends2>&1to the shell command so error messages are treated as entries by fzf making sure a clear error message is displayed. -
func_async_callback[default:true]: when using afunctionfor cotents, fzf-lua will "coroutinify" the callbacks to prevent UI hickups while reducing mental overhead of having to write the coroutine code. That's not always desireable as it can cause error neovim error E5560 when calling vimL functions (vim.fn) orvim.api. Set tofalseto prevent fzf-lua from using coroutines, see Lua function as contents for more info.
-
The contents argument is non-optional with the signature:
function(query) ... endThe function is then called each time fzf contents needs to be reloaded which
happens with every user keystroke (also when opening the interface with no
query if exec_empty_query is set).
The way the contents is reloaded differs based on the value type returned by
calling contents(), see the examples below for more details.
Similar to fzf_exec, when returning a table each item corresponds to an fzf
line. In the below example fzf will be populated with X number of lines based
on the number the user types:
require'fzf-lua'.fzf_live(
function(q)
local lines = {}
if tonumber(q) then
for i=1,q do
table.insert(lines, i)
end
else
table.insert(lines, "Invalid number: " .. q)
end
return lines
end,
{
prompt = 'Live> ',
exec_empty_query = true,
}
)In the above example, if you tried typing 12345678 you'd notice the UI
becomes less responsive the larger the number becomes, that's because the
table has to be filled with items before fzf-lua starts feeding fzf,
instead we can use a function argument that utilizes a lua coroutine
behind the scenes making sure the UI is free to respond in between inserts:
require'fzf-lua'.fzf_live(
function(q)
return function(fzf_cb)
if tonumber(q) then
for i=1,q do
fzf_cb(i)
end
else
fzf_cb("Invalid number: " .. q)
end
-- signal EOF to close the named pipe
-- and stop fzf's loading indicator
fzf_cb()
end
end,
{
prompt = 'Live> ',
exec_empty_query = true,
}
)Note regarding func_async_callback: by default fzf-lua will
"coroutinify" the callback, this might cause error E5560 when calling certain
neovim APIs / vimL functions which requires wrapping these calls with
vim.schedule as explained in fzf_exec: Lua as a
function. We can disable the coroutine by sending
func_async_callback = false as part of our options.
Below is an example of using neovim API inside a reload function:
Admittedly this is a useless example but I couldn't think of anything better that required both "live" content and the user of an API
require'fzf-lua'.fzf_live(
function(q)
return function(fzf_cb)
coroutine.wrap(function()
local co = coroutine.running()
if tonumber(q) then
for i=1,q do
-- append our buffer name to the entry
-- wrap in vim.schedule to avoid error E5560
vim.schedule(function()
local bufname = vim.api.nvim_buf_get_name(0)
fzf_cb(i..":"..bufname, function() coroutine.resume(co) end)
end)
-- wait here until coroutine.resume is called
coroutine.yield()
end
end
fzf_cb() -- EOF
end)()
end
end,
{
prompt = 'Live> ',
func_async_callback = false,
}
)If you've used fzf.vim or telescope.nvim you're probably familiar with the
concept of "live grep", where instead of feeding all lines of a project into
fzf, rg process is restarted with every keystroke, although fzf is very
performant the latter will be much more optimized when dealing with a large
code base.
fzf_live makes it super easy to run the equivalent of "live grep":
Note you will not see any results until you start typing unless you supplied
queryorexec_empty_queryas options.
:lua require'fzf-lua'.fzf_live("rg --column --line-number --no-heading --color=always --smart-case")By default the query is appended (with a space) after the command string, if
you wish to have the query somewhere before EOL use <query> as placeholder,
the example below redirects stderr to /dev/null to prevent fzf from
displaying the rg error messages:
:lua require'fzf-lua'.fzf_live("rg --column --color=always -- <query> 2>/dev/null")Similar to fzf_exec we can supply fn_transform to modify the command
output, the exmaple below utilizes fzf-lua's make_entry to add colored file
icons to the output:
require'fzf-lua'.fzf_live("rg --column --color=always -- <query>", {
fn_transform = function(x)
return require'fzf-lua'.make_entry.file(x, {file_icons=true, color_icons=true})
end,
})Note that the above command will fail when the query creates an invalid shell
command, for example when typing ' or " the generated command will have an
"unmatched" quote and would fail. For finer control over how the new command
is formed use a function that returns a string representing the new
command:
require'fzf-lua'.fzf_live(
function(q)
return "rg --column --color=always -- " .. vim.fn.shellescape(q or '')
end,
{
fn_transform = function(x)
return require'fzf-lua'.make_entry.file(x, {file_icons=true, color_icons=true})
end,
exec_empty_query = true,
}
)I'll leave it up to the reader to get creative with this as you can do all kinds of useful commands, for example
live_grep_globsearches for--in the query, any argument found past the separator is then reconstructed into the command as--iglob=...flags.
Tying it all together let's create our own version of "live grep" with file icons and git indicators:
_G.live_grep = function(opts)
local fzf_lua = require'fzf-lua'
opts = opts or {}
opts.prompt = "rg> "
opts.git_icons = true
opts.file_icons = true
opts.color_icons = true
-- setup default actions for edit, quickfix, etc
opts.actions = fzf_lua.defaults.actions.files
-- see preview overview for more info on previewers
opts.previewer = "builtin"
opts.fn_transform = function(x)
return fzf_lua.make_entry.file(x, opts)
end
-- we only need 'fn_preprocess' in order to display 'git_icons'
-- it runs once before the actual command to get modified files
-- 'make_entry.file' uses 'opts.diff_files' to detect modified files
-- will probaly make this more straight forward in the future
opts.fn_preprocess = function(o)
opts.diff_files = fzf_lua.make_entry.preprocess(o).diff_files
return opts
end
return fzf_lua.fzf_live(function(q)
return "rg --column --color=always -- " .. vim.fn.shellescape(q or '')
end, opts)
end
-- We can use our new function on any folder or
-- with any other fzf-lua options ('winopts', etc)
_G.live_grep({ cwd = "<my folder>" })Another useful example would be an interactive git grep interface that
searches across the entire git history, this way we can find deleted code
which no longer exists in the working tree:
See fzf#ADVANCED to understand the breakdown of the delimiter and preview parameters
require'fzf-lua'.fzf_live(
"git grep --line-number --column --color=always <query> $(git rev-list --all)",
{
fzf_opts = {
['--delimiter'] = ':',
['--preview-window'] = 'nohidden,down,60%,border-top,+{3}+3/3,~3',
},
preview = "git show {1}:{2} | " ..
"bat --style=default --color=always --file-name={2} --highlight-line={3}",
}
)Fzf-lua supports two types of previewers, fzf native and "builtin", fzf native
previewer as its name suggests utilizes fzf's own preview window via the
--preview flag which runs a shell command for each item and previews the
output in the preview window.
The "builtin" previewer uses a neovim buffer inside floating window created
with the nvim_open_win API.
Both previewers are fundamentally different, fzf native uses fzf keybinds and
neovim previewer (you guessed it) uses neovim style binds hence the different
mappings in defaults.keymap.builtin and default.keymap.fzf.
If you're familiar with fzf using the native previewer is pretty straight forward and similar to the way you'd setup fzf in the shell.
Pretty much anything that's possible with fzf can be achieved with fzf-lua, see fzf#ADVANCED for more info on how to use the preview option.
The example below lists files in the current directory and uses cat for
preview:
Note that when supplying a preview command through
fzf_opts(as opposed toopts.preview) we need to shell escape the command
-- both examples are equal
require'fzf-lua'.fzf_exec("rg --files", {
preview = "cat {}",
fzf_opts = { ['--preview-window'] = 'nohidden,down,50%' },
})
-- when using fzf_opts we need to shellescape the command
require'fzf-lua'.fzf_exec("rg --files", {
fzf_opts = {
['--preview'] = vim.fn.shellescape("cat {}"),
['--preview-window'] = 'nohidden,down,50%',
},
})What if we wanted to have a different shell command depending on the selected
item? The example below uses shell.preview_action_cmd to build a previewer
that previews media files using viu and
all other files using bat:
If you wish to use
opts.preview(instead offzf_opts) use the non-shellescaped versionshell.raw_preview_action_cmd
require'fzf-lua'.fzf_exec("rg --files", {
fzf_opts = {
['--preview-window'] = 'nohidden,down,50%',
['--preview'] = require'fzf-lua'.shell.preview_action_cmd(function(items)
local ext = vim.fn.fnamemodify(items[1], ':e')
if vim.tbl_contains({ "png", "jpg", "jpeg" }, ext) then
return "viu -b " .. items[1]
end
return string.format("bat --style=default --color=always %s", items[1])
end)
},
})It's also possible to populate fzf's previewer with contents generated by a
lua function using the shell.action helper, the below example populates the
preview with the filename but you can creative with it (retrieve unsaved buffer
contents using the neovim API, etc):
If you wish to use
opts.preview(instead offzf_opts) use the non-shellescaped versionshell.raw_action
require'fzf-lua'.fzf_exec("rg --files", {
fzf_opts = {
['--preview-window'] = 'nohidden,down,50%',
['--preview'] = require'fzf-lua'.shell.action(function(items)
local contents = {}
vim.tbl_map(function(x)
table.insert(contents, "selected item: " .. x)
end, items)
return contents
end)
},
})Neovim's "builtin" previewer (which is used by default) is a neovim buffer inside a floating window, it's very versatile as almost anything is possible with it.
The interface for the previewer requires more work to be exposed to the public as an API so for now I'll avoid writing examples for it as the code is likely to be changed in the future.
However, there are pre-configured builtin previewers that you can use if your
entires follow a specific format, the most common previewer, known as
"builtin" is capable of displaying neovim buffers, text files and even media
files (using ueberzug or viu), using any of the pre-configured previewers
is as easy as supplying the previewer option, the list of available
previewer names can be found under defaults.previewers, see
README.md#customization
for the full list.
The example below uses the "builtin" previewer to display files in the current directory:
:lua require'fzf-lua'.fzf_exec("rg --files", { previewer = "builtin" })Another example, live rg matches with lines and columns:
:lua require'fzf-lua'.fzf_live("rg --column --color=always", { previewer = "builtin" })Credit to @pure-bliss who came up with the idea, described in more detail in #471.
A user command, :ListFilesFromBranch that autocompletes branch names from
the current repo, opening the interface lists all files from a specific branch
with a preview showing the diff against HEAD (powered by
dandavison/delta), pressing ctrl-v
on a file will open a diff using
vim-fugitve's :Gvsplit:
vim.api.nvim_create_user_command(
'ListFilesFromBranch',
function(opts)
require 'fzf-lua'.files({
cmd = "git ls-tree -r --name-only " .. opts.args,
prompt = opts.args .. "> ",
actions = {
['default'] = false,
['ctrl-s'] = false,
['ctrl-v'] = function(selected, o)
local file = require'fzf-lua'.path.entry_to_file(selected[1], o)
local cmd = string.format("Gvsplit %s:%s", opts.args, file.path)
vim.cmd(cmd)
end,
},
previewer = false,
preview = require'fzf-lua'.shell.raw_preview_action_cmd(function(items)
local file = require'fzf-lua'.path.entry_to_file(items[1])
return string.format("git diff %s HEAD -- %s | delta", opts.args, file.path)
end)
})
end,
{
nargs = 1,
force = true,
complete = function()
local branches = vim.fn.systemlist("git branch --all --sort=-committerdate")
if vim.v.shell_error == 0 then
return vim.tbl_map(function(x)
return x:match("[^%s%*]+"):gsub("^remotes/", "")
end, branches)
end
end,
})