Skip to content
bhagwan edited this page Jan 24, 2023 · 15 revisions

Navigation

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 type string, 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 failed rg 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 a neovim --headless wrapper, set this to false 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 type function(query) and return a value of type:
    • string: reload with shell command output
    • table : reload from table
    • function: reload from function
  • opts: optional settings, see fzf_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 to true when it's desirable to call contents when opening the interface (without supplying opts.query)
    • silent_fail [default: true]: when a shell command exist with an error code (e.g. a failed rg 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 a neovim --headless wrapper, set this to false to display the failed message
    • stderr_to_stdout [default: true]: by default fzf is finicky about displaying error messages that are sent to stderr, set to true this option appends 2>&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 a function 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) or vim.api. Set to false 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 or exec_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 rev-list --all | xargs git grep --line-number --column --color=always <query>",
  {
    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 to opts.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 of fzf_opts) use the non-shellescaped version shell.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 of fzf_opts) use the non-shellescaped version shell.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 a bit of lua.

The builtin previewer comes with 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" })

Anything else can be achieved by using the previewer API, below is a minimal example how to extend the "buffer or file" previewer for custom entry parsing:

local fzf_lua = require("fzf-lua")
local builtin = require("fzf-lua.previewer.builtin")

-- Inherit from the "buffer_or_file" previewer
local MyPreviewer = builtin.buffer_or_file:extend()

function MyPreviewer:new(o, opts, fzf_win)
  MyPreviewer.super.new(self, o, opts, fzf_win)
  setmetatable(self, MyPreviewer)
  return self
end

function MyPreviewer:parse_entry(entry_str)
  -- Assume an arbitrary entry in the format of 'file:line'
  local path, line = entry_str:match("([^:]+):?(.*)")
  return {
    path = path,
    line = tonumber(line) or 1,
    col = 1,
  }
end

fzf_lua.fzf_exec("rg --files", {
  previewer = MyPreviewer,
  prompt = "Select file> ",
})

We can also populate the preview buffer with whatever content we wish by overwriting the populate_preview_buf function:

local fzf_lua = require("fzf-lua")
local builtin = require("fzf-lua.previewer.builtin")

-- Inherit from "base" instead of "buffer_or_file"
local MyPreviewer = builtin.base:extend()

function MyPreviewer:new(o, opts, fzf_win)
  MyPreviewer.super.new(self, o, opts, fzf_win)
  setmetatable(self, MyPreviewer)
  return self
end

function MyPreviewer:populate_preview_buf(entry_str)
  local tmpbuf = self:get_tmp_buffer()
  vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, {
    string.format("SELECTED FILE: %s", entry_str)
  })
  self:set_preview_buf(tmpbuf)
  self.win:update_scrollbar()
end

-- Disable line numbering and word wrap
function MyPreviewer:gen_winopts()
  local new_winopts = {
    wrap    = false,
    number  = false
  }
  return vim.tbl_extend("force", self.winopts, new_winopts)
end

fzf_lua.fzf_exec("rg --files", {
  previewer = MyPreviewer,
  prompt = "Select file> ",
})

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,
  })

Credit to @acro5piano for the original idea, described in more detail in #459.

When working on a large codebase it can be desirable to prioritize the current working directory when opening git_files, this can be done by pre-filling the query prompt with the current working directory therefore "boosting" the results prefixed by cwd:

-- The reason I added  'opts' as a parameter is so you can
-- call this function with your own parameters / customizations
-- for example: 'git_files_cwd_aware({ cwd = <another git repo> })'
function M.git_files_cwd_aware(opts)
  opts = opts or {}
  local fzf_lua = require('fzf-lua')
  -- git_root() will warn us if we're not inside a git repo
  -- so we don't have to add another warning here, if
  -- you want to avoid the error message change it to:
  -- local git_root = fzf_lua.path.git_root(opts, true)
  local git_root = fzf_lua.path.git_root(opts)
  if not git_root then return end
  local relative = fzf_lua.path.relative(vim.loop.cwd(), git_root)
  opts.fzf_opts = { ['--query'] = git_root ~= relative and relative or nil }
  return fzf_lua.git_files(opts)
end

Clone this wiki locally