-
Notifications
You must be signed in to change notification settings - Fork 152
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 failedrg
search), 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 --headless
wrapper, set this tofalse
to 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_exec
for 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 totrue
when it's desirable to callcontents
when opening the interface (without supplyingopts.query
) -
silent_fail
[default:true
]: when a shell command exist with an error code (e.g. a failedrg
search), 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 --headless
wrapper, set this tofalse
to display the failed message -
stderr_to_stdout
[default:true
]: by default fzf is finicky about displaying error messages that are sent tostderr
, set totrue
this option appends2>&1
to 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 afunction
for 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 tofalse
to 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) ... end
The 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
query
orexec_empty_query
as 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_glob
searches 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" })