nvim-fzf
An asynchronous Lua API for using fzf
in Neovim (>= 0.5). Allows for full asynchronicity for UI speed and usability.
Preview:
Note how in the example above, information is passed freely between
neovim and fzf
. Neovim is previewing the buffer in a split that you have
selected in fzf
. Using this library, you can perform anything in
response to fzf
events and keybindings.
Some handcrafted useful commands at
Tested on Linux, MacOS, and Windows.
Requirements
fzf
binary
Usage
local fzf = require("fzf")
coroutine.wrap(function()
local result = fzf.fzf({"choice 1", "choice 2"}, "--ansi")
-- result is a list of lines that fzf returns, if the user has chosen
if result then
print(result[1])
end
end)()
Table of contents
- Usage
- Installation
- Important information
- API Functions
- Main API
- Action API
- Helpers
- Examples
- How it works
- FAQ
Installation
Plug 'vijaymarupudi/nvim-fzf'
Important information
All fzf
functions should be run in a coroutine.
Example:
local fzf = require("fzf")
coroutine.wrap(function()
local result = fzf.fzf({"choice 1", "choice 2"})
if result then
print(result[1])
end
end)()
API Functions
Require this plugin using local fzf = require('fzf')
-
fzf.fzf(contents, [fzf_cli_args], [options])
An fzf function that opens a centered floating window and closes it after the user has chosen.
Example:
local results = fzf.fzf({"Option 1", "Option 2"}, "--nth 1") if results then -- do something end
options
: an optional table, taking optional settings. You can use this to change the default floating window behavior or the fzf binary.options.width
(number): width of the windowoptions.height
(number): height of the windowoptions.row
(number): row from top where window startsoptions.col
(number): column from left where window startsoptions.relative
('win', 'editor', 'cursor'): window position relative tooptions.border
(boolean | string | table, default: true): whether to display a border- if
border
isfalse
, a border won't be shown - if
border
istrue
, a rounded border will be shown - if
border
is anything else, it is passed directly tonvim_open_win
- if
options.window_on_create
(function): a function that's called after the window is created. Use this function to configure the various properties of the window such as background highlight group.options.fzf_binary
(string): The name (or path) of thefzf
(orskim
) executable.options.fzf_cwd
(string): The path of the working directory to run the fzf command in.options.fzf_cli_args
(string): Additional fzf command line arguments to prepend to the arguments supplied to the fzf functions. This is only useful when used in conjunction withfzf.default_options
.
NOTE:
options
inherits its properties fromfzf.default_options
. If you'd like to change the defaults for all nvim-fzf functions, modify this table e.g.require("fzf").default_options = { border = false }
Example:
local results = fzf.fzf({"Option 1", "Option 2"}, "--nth 1", { width = 30, height = 10, border = false }) if results then -- do something end
-
fzf.fzf_relative(contents, [fzf_cli_args], [options])
An fzf function that opens a centered floating window relative to the current split and closes it after the user has chosen. (Same as setting
options.relative = 'win'
)Example:
local results = fzf.fzf_relative({"Option 1", "Option 2"}, "--nth 1") if results then -- do something end
options
: an optional table taking optional settings. Seefzf.fzf
for information on settings. -
fzf.provided_win_fzf(contents, [fzf_cli_args], [options])
Runs fzf in the current window, and closes it after the user has chosen. Allows for the user to provide the fzf window.
-- for a vertical fzf vim.cmd [[ vertical new ]] fzf.provided_win_fzf(contents, fzf_cli_args)
-
fzf.raw_fzf(contents, [fzf_cli_args], [options])
An fzf function that runs fzf in the current window. See
Main API
for more details about the general API.
NOTE: nvim-fzf inherits nvim's environmental variables. This means
that options in $FZF_DEFAULT_OPTS
and other environment variables are
respected. You can override them using command line switches or
:let-environment
.
Main API
fzf(contents, [fzf_cli_args])
-
contents
-
if string: a shell command
local result = fzf("fd")
-
if table: a list of strings or string convertibles
local result = fzf({1, 2, "item"})
-
if function:
nvim-fzf
calls the function with a callback function to write vals to the fzf pipe. This api is asynchronous, making it possible to use fzf for long running applications and making the user interface snappy. Callbacks can be concurrent.-
cb(value, finished_cb)
value
: A value to write to fzffinished_cb(err)
: A callback called with an err if there is an error writing the value to fzf. This can occur if the user has already picked a value in fzf.
local result = fzf(function(cb) cb("value_1", function(err) -- this error can happen if the user has already chosen a value -- before the information was sent to fzf if err then return end cb("value_2", function(err) if err then return end cb(nil) -- to close the pipe to fzf, this removes the loading -- indicator in fzf end) end) end)
The function is also called with two other optional arguments for more advanced usage.
-
The 2nd argument is a variant of the callback function (which is passed as the 1st argument), but it does not add newlines to the elements. This is useful to pass through information directly to the pipe.
-
The 3rd argument is the
vim.loop
/luv
pipe to FZF. Use as you see fit!
-
-
-
fzf_cli_args
: string, A list of command line arguments for fzf.Can use to expect different key bindings (e.g.
--expect ctrl-t,ctrl-v
), previews, and coloring. -
return values
-
table, the lines that fzf returns in the shell as a table. If not lines are returned by fzf, the function returns nil for an easy conditional check.
local result = fzf("fd") if result then -- do something with result[1] end
local result = fzf("fd", "--multi") if result then -- do something with result[1] to result[#result] end
local result = fzf("fd", "--expect=ctrl-t") if result then if result[1] == "ctrl-t" then -- do something with result[2] else -- do something with result[2] end end
-
number: Representing fzf's exit code.
-
Action API (fzf Previews, Bindings, Actions in Lua)
Sometimes you want to use neovim information in fzf (such as previews of non file buffers, bindings to delete buffers, or change colorschemes). fzf expects a shell command for these parameters. Making your own shell command and setting up RPC can be cumbersome. This plugin provides an easy API to run a lua function / closure in response to these actions.
local fzf = require "fzf".fzf
local action = require "fzf.actions".action
coroutine.wrap(function()
-- items is a table of selected or hovered fzf items
local shell = action(function(items, fzf_lines, fzf_cols)
-- only one item will be hovered at any time, so get the selection
-- out and convert it to a number
local buf = tonumber(items[1])
-- you can return either a string or a table to show in the preview
-- window
return vim.api.nvim_buf_get_lines(buf, 0, -1, false)
end)
fzf(vim.api.nvim_list_bufs(), "--preview " .. shell)
end)()
require("fzf.actions").action(fn, [fzf_field_expression])
-
fn(selections, fzf_lines, fzf_cols)
: A function that takes a selection, performs an action, and optionally returns either atable
orstring
to print to stdout. This command is shell-escaped, so that you can easily append it to the--preview
fzf cli argument.selections
: atable
of strings selected infzf
fzf_lines
: number of lines in the preview window i.e.$FZF_PREVIEW_LINES
fzf_cols
: number of cols in the preview window i.e.$FZF_PREVIEW_COLS
-
fzf_field_expression
(string, optional, default:"{+}"
): This fzf field expression determines what items are sent to the action function. -
return value: a shell-escaped string to append to the
fzf
command line arguments (fzf_cli_args
) forfzf
to run.
require("fzf.actions").raw_action(fn, [fzf_field_expression])
-
Same as above, except it is not shell-escaped, so you can use it for complicated
--bind
functions. Take care to escape the result of this function before using it, as it contains spaces and quotes.local fzf = require("fzf").fzf local raw_action = require("fzf.actions").raw_action local raw_act_string = raw_action(function(args) -- do something with the args end) local bind_string = vim.fn.shellescape(string.format("--bind=ctrl-r:reload(%s)", raw_act_string)) coroutine.wrap(function() fzf({1, 2, 3, 4}, "--multi " .. bind_string) end)()
require("fzf.actions").async_action(fn, [fzf_field_expression])
-
fn(pipe, selections, fzf_lines, fzf_cols)
: Similar toaction(...)
, butfn
is passed an additional argument, thelibuv
/vim.loop
pipe tofzf
, as the first argument. Users can write to this pipe usinguv.write(pipe, data, callback)
and are expected to close the pipe usinguv.close(pipe)
.This function can be used for previews that take a long time to render and calculate from neovim.
require("fzf.actions").raw_async_action(fn, [fzf_field_expression])
- Same as above, except it is not shell-escaped, so you can use it for
complicated
--bind
functions. Take care to escape the result of this function before using it, as it contains spaces and quotes.
Helpers
Asynchronous programming is hard. For the case when you want to accept a
shell command, and simply transform each line into another line,
nvim-fzf
has a helper function that returns a function that
asynchronously applies the transformation, which can be passed right
into fzf
.
require("fzf.helpers").cmd_line_transformer(cmd, fn)
cmd
- if string: the shell command to transform
- if table: a table taking the following properties
cmd.cmd
(string): the shell command to transformcmd.cwd
(string, optional): the working directory to run the shell script in.cmd.pid_cb
(function, optional): a callback called with the pid of the shell command when available.
fn
(function): a function that takes as input a line from the shell command (string) and returns a new line to be sent tofzf
(string).
local fzf = require("fzf")
local fzf_helpers = require("fzf.helpers")
coroutine.wrap(function()
-- the transformation function runs for each line in the command
local fzf_fn = fzf_helpers.cmd_line_transformer("seq 1000", function(x)
local n = tonumber(x)
return tostring(n * n)
end)
local choices = fzf.fzf(fzf_fn)
end)()
require("fzf.helpers").choices_to_shell_cmd_previewer(fn, [fzf_field_expression])
-
fn(items, fzf_lines, fzf_cols)
: A function that is expected to return a shell cmd string to run asynchronously and feed tofzf
. This allows the user to use Lua to parse the input from fzf before performantly using an external process to preview the output.local fzf = require("fzf") local helpers = require("fzf.helpers") coroutine.wrap(function () local action = helpers.choices_to_shell_cmd_previewer(function(items) return "seq " .. vim.fn.shellescape(tostring(items[1])) end) fzf.fzf("seq 1 1000", "--preview=" .. action) end)()
-
fzf_field_expression
: See above
Examples
Filetype picker
local fts = {
"typescript",
"javascript",
"lua",
"python",
"vim",
"markdown",
"sh"
}
coroutine.wrap(function()
local choice = require "fzf".fzf(fts)
if choice then
vim.cmd(string.format("set ft=%s", choice[1]))
end
end)()
Colorscheme picker
This example provides a live preview of the colorscheme while the user
is choosing between them. An example showing the advantages of nvim-fzf
and the --preview
fzf cli arg.
local action = require("fzf.actions").action
local function get_colorschemes()
local colorscheme_vim_files = vim.fn.globpath(vim.o.rtp, "colors/*.vim", true, true)
local colorschemes = {}
for _, colorscheme_file in ipairs(colorscheme_vim_files) do
local colorscheme = vim.fn.fnamemodify(colorscheme_file, ":t:r")
table.insert(colorschemes, colorscheme)
end
return colorschemes
end
local function get_current_colorscheme()
if vim.g.colors_name then
return vim.g.colors_name
else
return 'default'
end
end
coroutine.wrap(function ()
local preview_function = action(function (args)
if args then
local colorscheme = args[1]
vim.cmd("colorscheme " .. colorscheme)
end
end)
local current_colorscheme = get_current_colorscheme()
local choices = fzf(get_colorschemes(), "--preview=" .. preview_function .. " --preview-window right:0")
if not choices then
vim.cmd("colorscheme " .. current_colorscheme)
else
vim.cmd("colorscheme " .. choices[1])
end
end)()
Helptags picker
This is a bit complex example that is completely asynchronous for
performance reasons. It also uses the fzf
--expect
command line flag.
local runtimepaths = vim.api.nvim_list_runtime_paths()
local uv = vim.loop
local fzf = require('fzf').fzf
local function readfilecb(path, callback)
uv.fs_open(path, "r", 438, function(err, fd)
if err then
callback(err)
return
end
uv.fs_fstat(fd, function(err, stat)
if err then
callback(err)
return
end
uv.fs_read(fd, stat.size, 0, function(err, data)
if err then
callback(err)
return
end
uv.fs_close(fd, function(err)
if err then
callback(err)
return
end
return callback(nil, data)
end)
end)
end)
end)
end
local function readfile(name)
local co = coroutine.running()
readfilecb(name, function (err, data)
coroutine.resume(co, err, data)
end)
local err, data = coroutine.yield()
if err then error(err) end
return data
end
local function deal_with_tags(tagfile, cb)
local co = coroutine.running()
coroutine.wrap(function ()
local success, data = pcall(readfile, tagfile)
if success then
for i, line in ipairs(vim.split(data, "\n")) do
local items = vim.split(line, "\t")
-- escape codes for grey
local tag = string.format("%s\t\27[0;37m%s\27[0m", items[1], items[2])
local co = coroutine.running()
cb(tag, function ()
coroutine.resume(co)
end)
coroutine.yield()
end
end
coroutine.resume(co)
end)()
coroutine.yield()
end
local fzf_function = function (cb)
local total_done = 0
for i, rtp in ipairs(runtimepaths) do
local tagfile = table.concat({rtp, "doc", "tags"}, "/")
-- wrapping to make all the file reading concurrent
coroutine.wrap(function ()
deal_with_tags(tagfile, cb)
total_done = total_done + 1
if total_done == #runtimepaths then
cb(nil)
end
end)()
end
end
coroutine.wrap(function ()
local result = fzf(fzf_function, "--nth 1 --ansi --expect=ctrl-t,ctrl-s,ctrl-v")
if not result then
return
end
local choice = vim.split(result[2], "\t")[1]
local key = result[1]
local windowcmd
if key == "" or key == "ctrl-s" then
windowcmd = ""
elseif key == "ctrl-v" then
windowcmd = "vertical"
elseif key == "ctrl-t" then
windowcmd = "tab"
else
print("Not implemented!")
error("Not implemented!")
end
vim.cmd(string.format("%s h %s", windowcmd, choice))
end)()
How it works
This plugin uses a temporary named pipe, and uses it to communicate to
fzf
.
FAQ
-
Does this conflict with
fzf.vim
?This library does not conflict with
fzf.vim
or the fzf vim API. -
How do I change the color of the default floating window spawned by
fzf.fzf
?You need to set the
winhl
option for the default window. You can do this for each command or globally by using thewindow_on_create
option.This makes the background of the popup window the same color of the backgrounds of normal windows. Example:
require("fzf").default_options = { window_on_create = function() vim.cmd("set winhl=Normal:Normal") end }