vijaymarupudi / nvim-fzf

A Lua API for using fzf in neovim.
MIT License
340 stars 13 forks source link
asynchronous fuzzy-search fzf lua neovim plugin vim

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

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

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')

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])

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])

require("fzf.actions").raw_action(fn, [fzf_field_expression])

require("fzf.actions").async_action(fn, [fzf_field_expression])

require("fzf.actions").raw_async_action(fn, [fzf_field_expression])

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)

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])

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