├── .gitignore ├── plugin └── fzf-lua.vim ├── .github └── ISSUE_TEMPLATE.md ├── lua └── fzf-lua │ ├── previewer │ ├── init.lua │ └── fzf.lua │ ├── providers │ ├── module.lua │ ├── manpages.lua │ ├── quickfix.lua │ ├── colorschemes.lua │ ├── oldfiles.lua │ ├── files.lua │ ├── ui_select.lua │ ├── helptags.lua │ ├── tags.lua │ ├── git.lua │ ├── dap.lua │ ├── buffers.lua │ ├── nvim.lua │ └── grep.lua │ ├── class.lua │ ├── shell_helper.lua │ ├── cmd.lua │ ├── shell.lua │ ├── fzf.lua │ ├── path.lua │ ├── init.lua │ ├── make_entry.lua │ ├── libuv.lua │ ├── actions.lua │ ├── utils.lua │ └── core.lua └── minimal_init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /plugin/fzf-lua.vim: -------------------------------------------------------------------------------- 1 | if !has('nvim-0.5') 2 | echohl Error 3 | echomsg "Fzf-lua is only available for Neovim versions 0.5 and above" 4 | echohl clear 5 | finish 6 | endif 7 | 8 | if exists('g:loaded_fzf_lua') | finish | endif 9 | let g:loaded_fzf_lua = 1 10 | 11 | " FzfLua builtin lists 12 | function! s:fzflua_complete(arg,line,pos) 13 | let l:builtin_list = luaeval('vim.tbl_filter( 14 | \ function(k) 15 | \ if require("fzf-lua")._excluded_metamap[k] then 16 | \ return false 17 | \ end 18 | \ return true 19 | \ end, 20 | \ vim.tbl_keys(require("fzf-lua")))') 21 | 22 | let list = [l:builtin_list] 23 | let l = split(a:line[:a:pos-1], '\%(\%(\%(^\|[^\\]\)\\\)\@) 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Info 5 | 6 | - Operating System: 7 | - Shell: 8 | - Terminal: 9 | - `nvim --version`: 10 | - `fzf --version`: 11 | 12 | 19 | - [ ] The issue is reproducible with `minimal_init.lua` 20 | 21 | 22 |
23 | fzf-lua configuration 24 | 25 | 26 | 27 | ```lua 28 | require('fzf-lua').setup({ 29 | }) 30 | ``` 31 |
32 | 33 | ### Description 34 | -------------------------------------------------------------------------------- /lua/fzf-lua/previewer/init.lua: -------------------------------------------------------------------------------- 1 | local Previewer = {} 2 | 3 | Previewer.fzf = {} 4 | Previewer.fzf.cmd = function() return require 'fzf-lua.previewer.fzf'.cmd end 5 | Previewer.fzf.bat = function() return require 'fzf-lua.previewer.fzf'.bat end 6 | Previewer.fzf.head = function() return require 'fzf-lua.previewer.fzf'.head end 7 | Previewer.fzf.cmd_async = function() return require 'fzf-lua.previewer.fzf'.cmd_async end 8 | Previewer.fzf.bat_async = function() return require 'fzf-lua.previewer.fzf'.bat_async end 9 | Previewer.fzf.git_diff = function() return require 'fzf-lua.previewer.fzf'.git_diff end 10 | Previewer.fzf.man_pages = function() return require 'fzf-lua.previewer.fzf'.man_pages end 11 | 12 | Previewer.builtin = {} 13 | Previewer.builtin.buffer_or_file = function() return require 'fzf-lua.previewer.builtin'.buffer_or_file end 14 | Previewer.builtin.help_tags = function() return require 'fzf-lua.previewer.builtin'.help_tags end 15 | Previewer.builtin.man_pages = function() return require 'fzf-lua.previewer.builtin'.man_pages end 16 | Previewer.builtin.marks = function() return require 'fzf-lua.previewer.builtin'.marks end 17 | Previewer.builtin.jumps = function() return require 'fzf-lua.previewer.builtin'.jumps end 18 | Previewer.builtin.tags = function() return require 'fzf-lua.previewer.builtin'.tags end 19 | 20 | return Previewer 21 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/module.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local shell = require "fzf-lua.shell" 3 | local config = require "fzf-lua.config" 4 | local actions = require "fzf-lua.actions" 5 | 6 | local M = {} 7 | 8 | M.metatable = function(opts) 9 | 10 | opts = config.normalize_opts(opts, config.globals.builtin) 11 | if not opts then return end 12 | 13 | if not opts.metatable then opts.metatable = getmetatable('').__index end 14 | 15 | local prev_act = shell.action(function (args) 16 | -- TODO: retreive method help 17 | local help = '' 18 | return string.format("%s:%s", args[1], help) 19 | end) 20 | 21 | local methods = {} 22 | for k, _ in pairs(opts.metatable) do 23 | if not opts.metatable_exclude or opts.metatable_exclude[k] == nil then 24 | table.insert(methods, k) 25 | end 26 | end 27 | 28 | table.sort(methods, function(a, b) return a 1 then 49 | for i = 2, #selected do 50 | selected[i] = M.getmanpage(selected[i]) 51 | end 52 | end 53 | 54 | actions.act(opts.actions, selected) 55 | 56 | end)() 57 | 58 | end 59 | 60 | return M 61 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/quickfix.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local utils = require "fzf-lua.utils" 3 | local config = require "fzf-lua.config" 4 | 5 | local M = {} 6 | 7 | local quickfix_run = function(opts, cfg, locations) 8 | if not locations then return {} end 9 | local results = {} 10 | for _, entry in ipairs(locations) do 11 | table.insert(results, core.make_entry_lcol(opts, entry)) 12 | end 13 | 14 | opts = config.normalize_opts(opts, cfg) 15 | if not opts then return end 16 | 17 | if not opts.cwd then opts.cwd = vim.loop.cwd() end 18 | 19 | local contents = function (cb) 20 | for _, x in ipairs(results) do 21 | x = core.make_entry_file(opts, x) 22 | if x then 23 | cb(x, function(err) 24 | if err then return end 25 | -- close the pipe to fzf, this 26 | -- removes the loading indicator in fzf 27 | cb(nil, function() end) 28 | end) 29 | end 30 | end 31 | utils.delayed_cb(cb) 32 | end 33 | 34 | opts = core.set_fzf_field_index(opts) 35 | return core.fzf_files(opts, contents) 36 | end 37 | 38 | M.quickfix = function(opts) 39 | local locations = vim.fn.getqflist() 40 | if vim.tbl_isempty(locations) then 41 | utils.info("Quickfix list is empty.") 42 | return 43 | end 44 | 45 | return quickfix_run(opts, config.globals.quickfix, locations) 46 | end 47 | 48 | M.loclist = function(opts) 49 | local locations = vim.fn.getloclist(0) 50 | 51 | for _, value in pairs(locations) do 52 | value.filename = vim.api.nvim_buf_get_name(value.bufnr) 53 | end 54 | 55 | if vim.tbl_isempty(locations) then 56 | utils.info("Location list is empty.") 57 | return 58 | end 59 | 60 | return quickfix_run(opts, config.globals.loclist, locations) 61 | end 62 | 63 | return M 64 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/colorschemes.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local shell = require "fzf-lua.shell" 3 | local config = require "fzf-lua.config" 4 | local actions = require "fzf-lua.actions" 5 | 6 | local function get_current_colorscheme() 7 | if vim.g.colors_name then 8 | return vim.g.colors_name 9 | else 10 | return 'default' 11 | end 12 | end 13 | 14 | local M = {} 15 | 16 | M.colorschemes = function(opts) 17 | 18 | opts = config.normalize_opts(opts, config.globals.colorschemes) 19 | if not opts then return end 20 | 21 | local prev_act = shell.action(function (args) 22 | if opts.live_preview and args then 23 | local colorscheme = args[1] 24 | vim.cmd("colorscheme " .. colorscheme) 25 | end 26 | end) 27 | 28 | local current_colorscheme = get_current_colorscheme() 29 | local current_background = vim.o.background 30 | local colors = vim.list_extend(opts.colors or {}, vim.fn.getcompletion('', 'color')) 31 | 32 | -- must add ':nohidden' or fzf ignore the preview action 33 | -- disabling our live preview of colorschemes 34 | opts.fzf_opts['--preview'] = prev_act 35 | opts.fzf_opts['--no-multi'] = '' 36 | opts.fzf_opts['--preview-window'] = 'nohidden:right:0' 37 | 38 | core.fzf_wrap(opts, colors, function(selected) 39 | 40 | -- reset color scheme if live_preview is enabled 41 | -- and nothing or non-default action was selected 42 | if opts.live_preview and (not selected or #selected[1]>0) then 43 | vim.o.background = current_background 44 | vim.cmd("colorscheme " .. current_colorscheme) 45 | vim.o.background = current_background 46 | end 47 | 48 | if selected then 49 | actions.act(opts.actions, selected) 50 | end 51 | 52 | if opts.post_reset_cb then 53 | opts.post_reset_cb() 54 | end 55 | 56 | end)() 57 | 58 | end 59 | 60 | return M 61 | -------------------------------------------------------------------------------- /minimal_init.lua: -------------------------------------------------------------------------------- 1 | -- Download this file and run `nvim -u /path/to/minimal_init.lua` or exec directly with: 2 | -- nvim -u <((echo "lua << EOF") && (curl -s https://raw.githubusercontent.com/ibhagwan/fzf-lua/main/minimal_init.lua) && (echo "EOF")) 3 | if vim.api.nvim_call_function('has', {'nvim-0.5'}) ~= 1 then 4 | vim.api.nvim_command('echohl WarningMsg | echom "Fzf-lua requires neovim > v0.5 | echohl None"') 5 | return 6 | end 7 | 8 | local res, packer = pcall(require, "packer") 9 | local install_suffix = "/site/pack/packer/%s/packer.nvim" 10 | local install_path = vim.fn.stdpath("data") .. string.format(install_suffix, "opt") 11 | local is_installed = vim.loop.fs_stat(install_path) ~= nil 12 | 13 | if not res and is_installed then 14 | vim.cmd("packadd packer.nvim") 15 | res, packer = pcall(require, "packer") 16 | end 17 | 18 | if not res then 19 | print("Downloading packer.nvim...\n") 20 | vim.fn.system({ 21 | "git", "clone", '--depth', '1', 22 | "https://github.com/wbthomason/packer.nvim", 23 | install_path, 24 | }) 25 | vim.cmd("packadd packer.nvim") 26 | res, packer = pcall(require, "packer") 27 | if res then 28 | vim.fn.delete(packer.config.compile_path, "rf") 29 | print("Successfully installed packer.nvim.") 30 | else 31 | print(("Error installing packer.nvim\nPath: %s"):format(install_path)) 32 | return 33 | end 34 | end 35 | 36 | packer.startup({ 37 | function(use) 38 | use { 'wbthomason/packer.nvim', opt = true } 39 | use { 'ibhagwan/fzf-lua', 40 | setup = [[ vim.api.nvim_set_keymap('n', '', 41 | 'lua require"fzf-lua".files()', {}) ]], 42 | config = 'require"fzf-lua".setup({})', 43 | event = 'VimEnter', 44 | opt = true, 45 | } 46 | end, 47 | -- do not remove installed plugins (when running 'vim -u') 48 | config = { auto_clean = false } 49 | }) 50 | 51 | packer.on_compile_done = function() 52 | packer.loader('fzf-lua') 53 | end 54 | 55 | if not vim.loop.fs_stat(packer.config.compile_path) then 56 | packer.sync() 57 | else 58 | packer.compile() 59 | end 60 | -------------------------------------------------------------------------------- /lua/fzf-lua/shell_helper.lua: -------------------------------------------------------------------------------- 1 | -- modified version of: 2 | -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/action_helper.lua 3 | local uv = vim.loop 4 | 5 | local function get_preview_socket() 6 | local tmp = vim.fn.tempname() 7 | local socket = uv.new_pipe(false) 8 | uv.pipe_bind(socket, tmp) 9 | return socket, tmp 10 | end 11 | 12 | local preview_socket, preview_socket_path = get_preview_socket() 13 | 14 | uv.listen(preview_socket, 100, function(_) 15 | local preview_receive_socket = uv.new_pipe(false) 16 | -- start listening 17 | uv.accept(preview_socket, preview_receive_socket) 18 | preview_receive_socket:read_start(function(err, data) 19 | assert(not err) 20 | if not data then 21 | uv.close(preview_receive_socket) 22 | uv.close(preview_socket) 23 | vim.schedule(function() 24 | vim.cmd[[qall]] 25 | end) 26 | return 27 | end 28 | io.write(data) 29 | end) 30 | end) 31 | 32 | 33 | local function_id = tonumber(vim.fn.argv(1)) 34 | local success, errmsg = pcall(function () 35 | local nargs = vim.fn.argc() 36 | local args = {} 37 | -- this is guaranteed to be 2 or more, we are interested in those greater than 2 38 | for i=3,nargs do 39 | -- vim uses zero indexing 40 | table.insert(args, vim.fn.argv(i - 1)) 41 | end 42 | local environ = vim.fn.environ() 43 | local chan_id = vim.fn.sockconnect("pipe", vim.fn.argv(0), { rpc = true }) 44 | -- for skim compatibility 45 | local preview_lines = environ.FZF_PREVIEW_LINES or environ.LINES 46 | local preview_cols = environ.FZF_PREVIEW_COLUMNS or environ.COLUMNS 47 | vim.rpcrequest(chan_id, "nvim_exec_lua", [[ 48 | local luaargs = {...} 49 | local function_id = luaargs[1] 50 | local preview_socket_path = luaargs[2] 51 | local fzf_selection = luaargs[3] 52 | local fzf_lines = luaargs[4] 53 | local fzf_columns = luaargs[5] 54 | local usr_func = require"fzf-lua.shell".get_func(function_id) 55 | return usr_func(preview_socket_path, fzf_selection, fzf_lines, fzf_columns) 56 | ]], { 57 | function_id, 58 | preview_socket_path, 59 | args, 60 | tonumber(preview_lines), 61 | tonumber(preview_cols) 62 | }) 63 | vim.fn.chanclose(chan_id) 64 | end) 65 | 66 | if not success then 67 | io.stderr:write("FzfLua Error:\n\n" .. errmsg .. "\n") 68 | vim.cmd [[qall]] 69 | end 70 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/oldfiles.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local config = require "fzf-lua.config" 3 | 4 | local M = {} 5 | 6 | M.oldfiles = function(opts) 7 | opts = config.normalize_opts(opts, config.globals.oldfiles) 8 | if not opts then return end 9 | 10 | local current_buffer = vim.api.nvim_get_current_buf() 11 | local current_file = vim.api.nvim_buf_get_name(current_buffer) 12 | local sess_tbl = {} 13 | local sess_map = {} 14 | 15 | if opts.include_current_session then 16 | for _, buffer in ipairs(vim.split(vim.fn.execute(':buffers! t'), "\n")) do 17 | local bufnr = tonumber(buffer:match('%s*(%d+)')) 18 | if bufnr then 19 | local file = vim.api.nvim_buf_get_name(bufnr) 20 | local fs_stat = not opts.stat_file and true or vim.loop.fs_stat(file) 21 | if #file>0 and fs_stat and bufnr ~= current_buffer then 22 | sess_map[file] = true 23 | table.insert(sess_tbl, file) 24 | end 25 | end 26 | end 27 | end 28 | 29 | local contents = function (cb) 30 | 31 | local function add_entry(x, co) 32 | x = core.make_entry_file(opts, x) 33 | if not x then return end 34 | cb(x, function(err) 35 | coroutine.resume(co) 36 | if err then 37 | -- close the pipe to fzf, this 38 | -- removes the loading indicator in fzf 39 | cb(nil, function() end) 40 | end 41 | end) 42 | coroutine.yield() 43 | end 44 | 45 | -- run in a coroutine for async progress indication 46 | coroutine.wrap(function() 47 | local co = coroutine.running() 48 | 49 | for _, file in ipairs(sess_tbl) do 50 | add_entry(file, co) 51 | end 52 | 53 | -- local start = os.time(); for _ = 1,10000,1 do 54 | for _, file in ipairs(vim.v.oldfiles) do 55 | local fs_stat = not opts.stat_file and true or vim.loop.fs_stat(file) 56 | if fs_stat and file ~= current_file and not sess_map[file] then 57 | add_entry(file, co) 58 | end 59 | end 60 | -- end; print("took", os.time()-start, "seconds.") 61 | 62 | -- done 63 | cb(nil, function() coroutine.resume(co) end) 64 | coroutine.yield() 65 | end)() 66 | 67 | end 68 | 69 | opts = core.set_header(opts, 2) 70 | return core.fzf_files(opts, contents) 71 | end 72 | 73 | return M 74 | -------------------------------------------------------------------------------- /lua/fzf-lua/cmd.lua: -------------------------------------------------------------------------------- 1 | -- Modified from Telescope 'command.lua' 2 | local builtin = require "fzf-lua" 3 | local utils = require "fzf-lua.utils" 4 | local command = {} 5 | 6 | local arg_value = { 7 | ["nil"] = nil, 8 | ['""'] = "", 9 | ['"'] = "", 10 | } 11 | 12 | local bool_type = { 13 | ["false"] = false, 14 | ["true"] = true, 15 | } 16 | 17 | -- convert command line string arguments to 18 | -- lua number boolean type and nil value 19 | local function convert_user_opts(user_opts) 20 | 21 | local _switch = { 22 | ["boolean"] = function(key, val) 23 | if val == "false" then 24 | user_opts[key] = false 25 | return 26 | end 27 | user_opts[key] = true 28 | end, 29 | ["number"] = function(key, val) 30 | user_opts[key] = tonumber(val) 31 | end, 32 | ["string"] = function(key, val) 33 | if arg_value[val] ~= nil then 34 | user_opts[key] = arg_value[val] 35 | return 36 | end 37 | 38 | if bool_type[val] ~= nil then 39 | user_opts[key] = bool_type[val] 40 | end 41 | end, 42 | } 43 | 44 | local _switch_metatable = { 45 | __index = function(_, k) 46 | utils.info(string.format("Type of %s does not match", k)) 47 | end, 48 | } 49 | 50 | setmetatable(_switch, _switch_metatable) 51 | 52 | for key, val in pairs(user_opts) do 53 | _switch["string"](key, val) 54 | end 55 | end 56 | 57 | -- receive the viml command args 58 | -- it should output a table value like 59 | -- { 60 | -- cmd = 'files', 61 | -- opts = { 62 | -- cwd = '***', 63 | -- } 64 | local function run_command(args) 65 | local user_opts = args or {} 66 | if next(user_opts) == nil and not user_opts.cmd then 67 | utils.info("missing command args") 68 | return 69 | end 70 | 71 | local cmd = user_opts.cmd 72 | local opts = user_opts.opts or {} 73 | 74 | if next(opts) ~= nil then 75 | convert_user_opts(opts) 76 | end 77 | 78 | if builtin[cmd] then 79 | builtin[cmd](opts) 80 | return 81 | end 82 | end 83 | 84 | function command.load_command(cmd, ...) 85 | local args = { ... } 86 | if cmd == nil then 87 | run_command { cmd = "builtin" } 88 | return 89 | end 90 | 91 | local user_opts = {} 92 | user_opts["cmd"] = cmd 93 | user_opts.opts = {} 94 | 95 | for _, arg in ipairs(args) do 96 | local param = vim.split(arg, "=") 97 | user_opts.opts[param[1]] = param[2] 98 | end 99 | 100 | run_command(user_opts) 101 | end 102 | 103 | return command 104 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/files.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local utils = require "fzf-lua.utils" 3 | local config = require "fzf-lua.config" 4 | 5 | local M = {} 6 | 7 | local function POSIX_find_compat(opts) 8 | local ver = utils.find_version() 9 | -- POSIX find does not have '--version' 10 | -- we assume POSIX when 'ver==nil' 11 | if not ver and opts:match("%-printf") then 12 | utils.warn("POSIX find does not support the '-printf' option." .. 13 | " Install 'fd' or set 'files.find_opts' to '-type f'.") 14 | end 15 | end 16 | 17 | local get_files_cmd = function(opts) 18 | if opts.raw_cmd and #opts.raw_cmd>0 then 19 | return opts.raw_cmd 20 | end 21 | if opts.cmd and #opts.cmd>0 then 22 | return opts.cmd 23 | end 24 | local command = nil 25 | if vim.fn.executable("fd") == 1 then 26 | command = string.format('fd %s', opts.fd_opts) 27 | elseif vim.fn.executable("rg") == 1 then 28 | command = string.format('rg %s', opts.rg_opts) 29 | else 30 | POSIX_find_compat(opts.find_opts) 31 | command = string.format('find -L . %s', opts.find_opts) 32 | end 33 | return command 34 | end 35 | 36 | M.files = function(opts) 37 | opts = config.normalize_opts(opts, config.globals.files) 38 | if not opts then return end 39 | opts.cmd = get_files_cmd(opts) 40 | local contents = core.mt_cmd_wrapper(opts) 41 | opts = core.set_header(opts, 2) 42 | return core.fzf_files(opts, contents) 43 | end 44 | 45 | M.args = function(opts) 46 | opts = config.normalize_opts(opts, config.globals.args) 47 | if not opts then return end 48 | 49 | if opts.fzf_opts['--header'] == nil then 50 | opts.fzf_opts['--header'] = vim.fn.shellescape((':: %s to delete') 51 | :format(utils.ansi_codes.yellow(""))) 52 | end 53 | 54 | local contents = function (cb) 55 | 56 | local function add_entry(x, co) 57 | x = core.make_entry_file(opts, x) 58 | if not x then return end 59 | cb(x, function(err) 60 | coroutine.resume(co) 61 | if err then 62 | -- close the pipe to fzf, this 63 | -- removes the loading indicator in fzf 64 | cb(nil, function() end) 65 | end 66 | end) 67 | coroutine.yield() 68 | end 69 | 70 | -- run in a coroutine for async progress indication 71 | coroutine.wrap(function() 72 | local co = coroutine.running() 73 | 74 | local entries = vim.fn.execute("args") 75 | entries = utils.strsplit(entries, "%s\n") 76 | -- remove the current file indicator 77 | -- remove all non-files 78 | -- local start = os.time(); for _ = 1,10000,1 do 79 | for _, s in ipairs(entries) do 80 | if s:match('^%[') then 81 | s = s:gsub('^%[', ''):gsub('%]$', '') 82 | end 83 | local st = vim.loop.fs_stat(s) 84 | if opts.files_only == false or 85 | st and st.type == 'file' then 86 | add_entry(s, co) 87 | end 88 | end 89 | -- end; print("took", os.time()-start, "seconds.") 90 | 91 | -- done 92 | cb(nil, function() coroutine.resume(co) end) 93 | coroutine.yield() 94 | end)() 95 | 96 | end 97 | 98 | opts = core.set_header(opts, 2) 99 | return core.fzf_files(opts, contents) 100 | end 101 | 102 | return M 103 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/ui_select.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local utils = require "fzf-lua.utils" 3 | local config = require "fzf-lua.config" 4 | local actions = require "fzf-lua.actions" 5 | 6 | local M = {} 7 | 8 | local _opts = nil 9 | local _old_ui_select = nil 10 | 11 | M.is_registered = function() 12 | return vim.ui.select == M.ui_select 13 | end 14 | 15 | M.deregister = function(_, silent, noclear) 16 | if not _old_ui_select then 17 | if not silent then 18 | utils.info("vim.ui.select in not registered to fzf-lua") 19 | end 20 | return false 21 | end 22 | vim.ui.select = _old_ui_select 23 | _old_ui_select = nil 24 | -- do not empty _opts incase when 25 | -- resume from `lsp_code_actions` 26 | if not noclear then 27 | _opts = nil 28 | end 29 | return true 30 | end 31 | 32 | M.register = function(opts, silent) 33 | if vim.ui.select == M.ui_select then 34 | -- already registered 35 | if not silent then 36 | utils.info("vim.ui.select already registered to fzf-lua") 37 | end 38 | return false 39 | end 40 | _opts = opts 41 | _old_ui_select = vim.ui.select 42 | vim.ui.select = M.ui_select 43 | return true 44 | end 45 | 46 | M.ui_select = function(items, opts, on_choice) 47 | --[[ 48 | -- Code Actions 49 | opts = { 50 | format_item = , 51 | kind = "codeaction", 52 | prompt = "Code actions:" 53 | } 54 | items[1] = { 1, { 55 | command = { 56 | arguments = { { 57 | action = "add", 58 | key = "Lua.diagnostics.globals", 59 | uri = "file:///home/bhagwan/.dots/.config/awesome/rc.lua", 60 | value = "mymainmenu" 61 | } }, 62 | command = "lua.setConfig", 63 | title = "Mark defined global" 64 | }, 65 | kind = "quickfix", 66 | title = "Mark `mymainmenu` as defined global." 67 | } } ]] 68 | 69 | -- exit visual mode if needed 70 | local mode = vim.api.nvim_get_mode() 71 | if not mode.mode:match("^n") then 72 | utils.feed_keys_termcodes("") 73 | end 74 | 75 | local entries = {} 76 | for i, e in ipairs(items) do 77 | table.insert(entries, 78 | ("%s. %s"):format(utils.ansi_codes.magenta(tostring(i)), 79 | opts.format_item and opts.format_item(e) or tostring(e))) 80 | end 81 | 82 | local prompt = opts.prompt 83 | if not prompt then 84 | prompt = "Select one of:" 85 | end 86 | 87 | _opts = _opts or {} 88 | _opts.fzf_opts = { 89 | ['--no-multi'] = '', 90 | ['--prompt'] = prompt:gsub(":%s?$", "> "), 91 | ['--preview-window'] = 'hidden:right:0', 92 | } 93 | 94 | -- save items so we can access them from the action 95 | _opts._items = items 96 | _opts._on_choice = on_choice 97 | 98 | _opts.actions = vim.tbl_deep_extend("keep", _opts.actions or {}, 99 | { 100 | ["default"] = function(selected, o) 101 | local idx = selected and tonumber(selected[1]:match("^(%d+).")) or nil 102 | o._on_choice(idx and o._items[idx] or nil, idx) 103 | end 104 | }) 105 | 106 | config.set_action_helpstr(_opts.actions['default'], "accept-item") 107 | 108 | core.fzf_wrap(_opts, entries, function(selected) 109 | 110 | config.set_action_helpstr(_opts.actions['default'], nil) 111 | 112 | if not selected then 113 | on_choice(nil, nil) 114 | else 115 | actions.act(_opts.actions, selected, _opts) 116 | end 117 | 118 | if _opts.post_action_cb then 119 | _opts.post_action_cb() 120 | end 121 | 122 | end)() 123 | 124 | end 125 | 126 | return M 127 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/helptags.lua: -------------------------------------------------------------------------------- 1 | local path = require "fzf-lua.path" 2 | local core = require "fzf-lua.core" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local actions = require "fzf-lua.actions" 6 | 7 | 8 | local M = {} 9 | 10 | local fzf_function = function (cb) 11 | local opts = {} 12 | opts.lang = config.globals.helptags.lang or vim.o.helplang 13 | opts.fallback = utils._if(config.globals.helptags.fallback ~= nil, config.globals.helptags.fallback, true) 14 | 15 | local langs = vim.split(opts.lang, ',', true) 16 | if opts.fallback and not vim.tbl_contains(langs, 'en') then 17 | table.insert(langs, 'en') 18 | end 19 | local langs_map = {} 20 | for _, lang in ipairs(langs) do 21 | langs_map[lang] = true 22 | end 23 | 24 | local tag_files = {} 25 | local function add_tag_file(lang, file) 26 | if langs_map[lang] then 27 | if tag_files[lang] then 28 | table.insert(tag_files[lang], file) 29 | else 30 | tag_files[lang] = {file} 31 | end 32 | end 33 | end 34 | 35 | local help_files = {} 36 | local all_files = vim.fn.globpath(vim.o.runtimepath, 'doc/*', 1, 1) 37 | for _, fullpath in ipairs(all_files) do 38 | local file = path.tail(fullpath) 39 | if file == 'tags' then 40 | add_tag_file('en', fullpath) 41 | elseif file:match('^tags%-..$') then 42 | local lang = file:sub(-2) 43 | add_tag_file(lang, fullpath) 44 | else 45 | help_files[file] = fullpath 46 | end 47 | end 48 | 49 | local add_tag = function(t, fzf_cb, co) 50 | --[[ local tag = string.format("%-58s\t%s", 51 | utils.ansi_codes.blue(t.name), 52 | utils._if(t.name and #t.name>0, path.basename(t.name), '')) ]] 53 | local tag = utils.ansi_codes.magenta(t.name) 54 | fzf_cb(tag, function() 55 | coroutine.resume(co) 56 | end) 57 | end 58 | 59 | coroutine.wrap(function () 60 | local co = coroutine.running() 61 | local tags_map = {} 62 | local delimiter = string.char(9) 63 | for _, lang in ipairs(langs) do 64 | for _, file in ipairs(tag_files[lang] or {}) do 65 | local lines = vim.split(utils.read_file(file), '\n', true) 66 | for _, line in ipairs(lines) do 67 | -- TODO: also ignore tagComment starting with ';' 68 | if not line:match'^!_TAG_' then 69 | local fields = vim.split(line, delimiter, true) 70 | if #fields == 3 and not tags_map[fields[1]] then 71 | add_tag({ 72 | name = fields[1], 73 | filename = help_files[fields[2]], 74 | cmd = fields[3], 75 | lang = lang, 76 | }, cb, co) 77 | tags_map[fields[1]] = true 78 | -- pause here until we call coroutine.resume() 79 | coroutine.yield() 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -- done, we can't call utils.delayed_cb here 86 | -- because sleep() messes up the coroutine 87 | -- cb(nil, function() coroutine.resume(co) end) 88 | utils.delayed_cb(cb, function() coroutine.resume(co) end) 89 | coroutine.yield() 90 | end)() 91 | end 92 | 93 | 94 | M.helptags = function(opts) 95 | 96 | opts = config.normalize_opts(opts, config.globals.helptags) 97 | if not opts then return end 98 | 99 | -- local prev_act = action(function (args) end) 100 | 101 | opts.fzf_opts['--no-multi'] = '' 102 | opts.fzf_opts['--preview-window'] = 'hidden:right:0' 103 | opts.fzf_opts['--nth'] = '1' 104 | 105 | core.fzf_wrap(opts, fzf_function, function(selected) 106 | 107 | if not selected then return end 108 | 109 | actions.act(opts.actions, selected) 110 | 111 | end)() 112 | 113 | end 114 | 115 | return M 116 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/tags.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local path = require "fzf-lua.path" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local make_entry = require "fzf-lua.make_entry" 6 | 7 | local M = {} 8 | 9 | local function get_tags_cmd(opts, flags) 10 | local query = nil 11 | local cmd = "grep" 12 | if vim.fn.executable("rg") == 1 then 13 | cmd = "rg" 14 | end 15 | if opts.search and #opts.search>0 then 16 | query = vim.fn.shellescape(opts.no_esc and opts.search or 17 | utils.rg_escape(opts.search)) 18 | elseif opts._curr_file and #opts._curr_file>0 then 19 | query = vim.fn.shellescape(opts._curr_file) 20 | else 21 | query = "-v '^!_TAG_'" 22 | end 23 | return ("%s %s %s %s"):format(cmd, flags or '', query, 24 | vim.fn.shellescape(opts._ctags_file)) 25 | end 26 | 27 | local function tags(opts) 28 | 29 | -- signal actions this is a ctag 30 | opts._ctag = true 31 | opts.ctags_file = opts.ctags_file and vim.fn.expand(opts.ctags_file) or "tags" 32 | opts._ctags_file = opts.ctags_file 33 | if not path.starts_with_separator(opts._ctags_file) and opts.cwd then 34 | opts._ctags_file = path.join({opts.cwd, opts.ctags_file}) 35 | end 36 | 37 | if not vim.loop.fs_stat(opts._ctags_file) then 38 | utils.info(("Tags file ('%s') does not exists. Create one with ctags -R") 39 | :format(opts._ctags_file)) 40 | return 41 | end 42 | 43 | if opts.line_field_index == nil then 44 | -- if caller did not specify the line field index 45 | -- grep the first tag with '-m 1' and test for line presence 46 | local cmd = get_tags_cmd({ _ctags_file = opts._ctags_file }, "-m 1") 47 | local ok, lines, err = pcall(utils.io_systemlist, cmd) 48 | if ok and err == 0 and lines and not vim.tbl_isempty(lines) then 49 | local tag, line = make_entry.tag(opts, lines[1]) 50 | if tag and not line then 51 | -- tags file does not contain lines 52 | -- remove preview offset field index 53 | opts.line_field_index = 0 54 | end 55 | end 56 | end 57 | 58 | -- prevents 'file|git_icons=false' from overriding processing 59 | opts.requires_processing = true 60 | opts._fn_transform = make_entry.tag -- multiprocess=false 61 | opts._fn_transform_str = [[return require("make_entry").tag]] -- multiprocess=true 62 | 63 | if opts.lgrep then 64 | -- live_grep requested by caller ('tags_live_grep') 65 | opts.prompt = opts.prompt:match("^*") and opts.prompt or '*' .. opts.prompt 66 | opts.filename = opts._ctags_file 67 | if opts.multiprocess then 68 | return require'fzf-lua.providers.grep'.live_grep_mt(opts) 69 | else 70 | -- 'live_grep_st' uses different signature '_fn_transform' 71 | opts._fn_transform = function(x) 72 | return make_entry.tag(opts, x) 73 | end 74 | return require'fzf-lua.providers.grep'.live_grep_st(opts) 75 | end 76 | end 77 | 78 | opts._curr_file = opts._curr_file and 79 | path.relative(opts._curr_file, opts.cwd or vim.loop.cwd()) 80 | opts.cmd = opts.cmd or get_tags_cmd(opts) 81 | local contents = core.mt_cmd_wrapper(opts) 82 | opts = core.set_header(opts) 83 | opts = core.set_fzf_field_index(opts) 84 | return core.fzf_files(opts, contents) 85 | end 86 | 87 | M.tags = function(opts) 88 | opts = config.normalize_opts(opts, config.globals.tags) 89 | if not opts then return end 90 | return tags(opts) 91 | end 92 | 93 | M.btags = function(opts) 94 | opts = config.normalize_opts(opts, config.globals.btags) 95 | if not opts then return end 96 | opts._curr_file = vim.api.nvim_buf_get_name(0) 97 | if not opts._curr_file or #opts._curr_file==0 then 98 | utils.info("'btags' is not available for unnamed buffers.") 99 | return 100 | end 101 | return tags(opts) 102 | end 103 | 104 | M.grep = function(opts) 105 | opts = opts or {} 106 | 107 | if not opts.search then 108 | opts.search = vim.fn.input(opts.input_prompt or 'Grep For> ') 109 | end 110 | 111 | return M.tags(opts) 112 | end 113 | 114 | M.live_grep = function(opts) 115 | opts = config.normalize_opts(opts, config.globals.tags) 116 | if not opts then return end 117 | opts.lgrep = true 118 | opts.__FNCREF__ = utils.__FNCREF__() 119 | return tags(opts) 120 | end 121 | 122 | M.grep_cword = function(opts) 123 | if not opts then opts = {} end 124 | opts.search = vim.fn.expand("") 125 | return M.grep(opts) 126 | end 127 | 128 | M.grep_cWORD = function(opts) 129 | if not opts then opts = {} end 130 | opts.search = vim.fn.expand("") 131 | return M.grep(opts) 132 | end 133 | 134 | M.grep_visual = function(opts) 135 | if not opts then opts = {} end 136 | opts.search = utils.get_visual_selection() 137 | return M.grep(opts) 138 | end 139 | 140 | return M 141 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/git.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local path = require "fzf-lua.path" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local actions = require "fzf-lua.actions" 6 | local libuv = require "fzf-lua.libuv" 7 | local shell = require "fzf-lua.shell" 8 | 9 | local M = {} 10 | 11 | M.files = function(opts) 12 | opts = config.normalize_opts(opts, config.globals.git.files) 13 | if not opts then return end 14 | opts.cwd = path.git_root(opts.cwd) 15 | if not opts.cwd then return end 16 | local contents = core.mt_cmd_wrapper(opts) 17 | opts = core.set_header(opts, 2) 18 | return core.fzf_files(opts, contents) 19 | end 20 | 21 | M.status = function(opts) 22 | opts = config.normalize_opts(opts, config.globals.git.status) 23 | if not opts then return end 24 | opts.cwd = path.git_root(opts.cwd) 25 | if not opts.cwd then return end 26 | if opts.preview then 27 | opts.preview = vim.fn.shellescape(path.git_cwd(opts.preview, opts.cwd)) 28 | end 29 | -- we don't need git icons since we get them 30 | -- as part of our `git status -s` 31 | opts.git_icons = false 32 | if not opts.no_header then 33 | local stage = utils.ansi_codes.yellow("") 34 | local unstage = utils.ansi_codes.yellow("") 35 | opts.fzf_opts['--header'] = vim.fn.shellescape( 36 | ('+ - :: %s to stage, %s to unstage'):format(stage, unstage)) 37 | end 38 | local function git_iconify(x) 39 | local icon = x 40 | local git_icon = config.globals.git.icons[x] 41 | if git_icon then 42 | icon = git_icon.icon 43 | if opts.color_icons then 44 | icon = utils.ansi_codes[git_icon.color or "dark_grey"](icon) 45 | end 46 | end 47 | return icon 48 | end 49 | local contents = libuv.spawn_nvim_fzf_cmd(opts, 50 | function(x) 51 | -- unrecognizable format, return 52 | if not x or #x<4 then return x end 53 | -- `man git-status` 54 | -- we are guaranteed format of: XY 55 | -- spaced files are wrapped with quotes 56 | -- remove both git markers and quotes 57 | local f1, f2 = x:sub(4):gsub('"', ""), nil 58 | -- renames spearate files with '->' 59 | if f1:match("%s%->%s") then 60 | f1, f2 = f1:match("(.*)%s%->%s(.*)") 61 | end 62 | f1 = f1 and core.make_entry_file(opts, f1) 63 | f2 = f2 and core.make_entry_file(opts, f2) 64 | local staged = git_iconify(x:sub(1,1):gsub("?", " ")) 65 | local unstaged = git_iconify(x:sub(2,2)) 66 | local entry = ("%s%s%s%s%s"):format( 67 | staged, utils.nbsp, unstaged, utils.nbsp .. utils.nbsp, 68 | (f2 and ("%s -> %s"):format(f1, f2) or f1)) 69 | return entry 70 | end, 71 | function(o) 72 | return core.make_entry_preprocess(o) 73 | end) 74 | opts = core.set_header(opts, 2) 75 | return core.fzf_files(opts, contents) 76 | end 77 | 78 | local function git_cmd(opts) 79 | opts.cwd = path.git_root(opts.cwd) 80 | if not opts.cwd then return end 81 | opts = core.set_header(opts, 2) 82 | core.fzf_wrap(opts, opts.cmd, function(selected) 83 | if not selected then return end 84 | actions.act(opts.actions, selected, opts) 85 | end)() 86 | end 87 | 88 | M.commits = function(opts) 89 | opts = config.normalize_opts(opts, config.globals.git.commits) 90 | if not opts then return end 91 | opts.preview = vim.fn.shellescape(path.git_cwd(opts.preview, opts.cwd)) 92 | return git_cmd(opts) 93 | end 94 | 95 | M.bcommits = function(opts) 96 | opts = config.normalize_opts(opts, config.globals.git.bcommits) 97 | if not opts then return end 98 | local git_root = path.git_root(opts.cwd) 99 | if not git_root then return end 100 | local file = path.relative(vim.fn.expand("%:p"), git_root) 101 | opts.cmd = opts.cmd .. " " .. file 102 | local git_ver = utils.git_version() 103 | -- rotate-to first appeared with git version 2.31 104 | if git_ver and git_ver >= 2.31 then 105 | opts.preview = opts.preview .. " --rotate-to=" .. vim.fn.shellescape(file) 106 | end 107 | opts.preview = vim.fn.shellescape(path.git_cwd(opts.preview, opts.cwd)) 108 | return git_cmd(opts) 109 | end 110 | 111 | M.branches = function(opts) 112 | opts = config.normalize_opts(opts, config.globals.git.branches) 113 | if not opts then return end 114 | opts.fzf_opts["--no-multi"] = '' 115 | opts._preview = path.git_cwd(opts.preview, opts.cwd) 116 | opts.preview = shell.preview_action_cmd(function(items) 117 | local branch = items[1]:gsub("%*", "") -- remove the * from current branch 118 | if branch:find("%)") ~= nil then 119 | -- (HEAD detached at origin/master) 120 | branch = branch:match(".* ([^%)]+)") or "" 121 | else 122 | -- remove anything past space 123 | branch = branch:match("[^ ]+") 124 | end 125 | return opts._preview:gsub("{.*}", branch) 126 | -- return "echo " .. branch 127 | end) 128 | return git_cmd(opts) 129 | end 130 | 131 | return M 132 | -------------------------------------------------------------------------------- /lua/fzf-lua/shell.lua: -------------------------------------------------------------------------------- 1 | -- modified version of: 2 | -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf/actions.lua 3 | local uv = vim.loop 4 | local path = require "fzf-lua.path" 5 | local libuv = require "fzf-lua.libuv" 6 | 7 | local M = {} 8 | 9 | local _counter = 0 10 | local _registry = {} 11 | 12 | function M.register_func(fn) 13 | _counter = _counter + 1 14 | _registry[_counter] = fn 15 | return _counter 16 | end 17 | 18 | function M.get_func(counter) 19 | return _registry[counter] 20 | end 21 | 22 | -- creates a new address to listen to messages from actions. This is important, 23 | -- if the user is using a custom fixed $NVIM_LISTEN_ADDRESS. Different neovim 24 | -- instances will then use the same path as the address and it causes a mess, 25 | -- i.e. actions stop working on the old instance. So we create our own (random 26 | -- path) RPC server for this instance if it hasn't been started already. 27 | -- NOT USED ANYMORE, we use `vim.g.fzf_lua_server` instead 28 | -- local action_server_address = nil 29 | 30 | function M.raw_async_action(fn, fzf_field_expression) 31 | 32 | if not fzf_field_expression then 33 | fzf_field_expression = "{+}" 34 | end 35 | 36 | local receiving_function = function(pipe_path, ...) 37 | local pipe = uv.new_pipe(false) 38 | local args = {...} 39 | uv.pipe_connect(pipe, pipe_path, function(err) 40 | vim.schedule(function () 41 | fn(pipe, unpack(args)) 42 | end) 43 | end) 44 | end 45 | 46 | local id = M.register_func(receiving_function) 47 | 48 | -- this is for windows WSL and AppImage users, their nvim path isn't just 49 | -- 'nvim', it can be something else 50 | local nvim_command = vim.v.argv[1] 51 | 52 | local action_string = string.format("%s -n --headless --clean --cmd %s %s %s %s", 53 | vim.fn.shellescape(nvim_command), 54 | vim.fn.shellescape("luafile " .. path.join{vim.g.fzf_lua_directory, "shell_helper.lua"}), 55 | vim.fn.shellescape(vim.g.fzf_lua_server), 56 | id, 57 | fzf_field_expression) 58 | return action_string, id 59 | end 60 | 61 | function M.async_action(fn, fzf_field_expression) 62 | local action_string, id = M.raw_async_action(fn, fzf_field_expression) 63 | return vim.fn.shellescape(action_string), id 64 | end 65 | 66 | function M.raw_action(fn, fzf_field_expression) 67 | 68 | local receiving_function = function(pipe, ...) 69 | local ret = fn(...) 70 | 71 | local on_complete = function(_) 72 | -- We are NOT asserting, in case fzf closes 73 | -- the pipe before we can send the preview 74 | -- assert(not err) 75 | uv.close(pipe) 76 | end 77 | 78 | if type(ret) == "string" then 79 | uv.write(pipe, ret, on_complete) 80 | elseif type(ret) == nil then 81 | on_complete() 82 | elseif type(ret) == "table" then 83 | if not vim.tbl_isempty(ret) then 84 | uv.write(pipe, vim.tbl_map(function(x) return x.."\n" end, ret), on_complete) 85 | else 86 | on_complete() 87 | end 88 | else 89 | uv.write(pipe, tostring(ret) .. "\n", on_complete) 90 | end 91 | end 92 | 93 | return M.raw_async_action(receiving_function, fzf_field_expression) 94 | end 95 | 96 | function M.action(fn, fzf_field_expression) 97 | local action_string, id = M.raw_action(fn, fzf_field_expression) 98 | return vim.fn.shellescape(action_string), id 99 | end 100 | 101 | M.preview_action_cmd = function(fn, fzf_field_expression) 102 | 103 | return M.async_action(function(pipe, ...) 104 | 105 | local function on_finish(_, _) 106 | if pipe and not uv.is_closing(pipe) then 107 | uv.close(pipe) 108 | pipe = nil 109 | end 110 | end 111 | 112 | local function on_write(data, cb) 113 | if not pipe then 114 | cb(true) 115 | else 116 | uv.write(pipe, data, cb) 117 | end 118 | end 119 | 120 | return libuv.spawn({ 121 | cmd = fn(...), 122 | cb_finish = on_finish, 123 | cb_write = on_write, 124 | }, false) 125 | 126 | end, fzf_field_expression) 127 | end 128 | 129 | M.reload_action_cmd = function(opts, fzf_field_expression) 130 | 131 | local _pid = nil 132 | 133 | return M.raw_async_action(function(pipe, args) 134 | 135 | local function on_pid(pid) 136 | _pid = pid 137 | if opts.pid_cb then 138 | opts.pid_cb(pid) 139 | end 140 | end 141 | 142 | local function on_finish(_, _) 143 | if pipe and not uv.is_closing(pipe) then 144 | uv.close(pipe) 145 | pipe = nil 146 | end 147 | end 148 | 149 | local function on_write(data, cb) 150 | if not pipe then 151 | cb(true) 152 | else 153 | uv.write(pipe, data, cb) 154 | end 155 | end 156 | 157 | -- terminate previously running commands 158 | libuv.process_kill(_pid) 159 | 160 | -- return libuv.spawn({ 161 | return libuv.async_spawn({ 162 | cwd = opts.cwd, 163 | cmd = opts._reload_command(args[1]), 164 | cb_finish = on_finish, 165 | cb_write = on_write, 166 | cb_pid = on_pid, 167 | -- must send false, 'coroutinify' adds callback as last argument 168 | -- which will conflict with the 'fn_transform' argument 169 | }, opts._fn_transform or false) 170 | 171 | end, fzf_field_expression) 172 | end 173 | 174 | return M 175 | -------------------------------------------------------------------------------- /lua/fzf-lua/fzf.lua: -------------------------------------------------------------------------------- 1 | -- slimmed down version of nvim-fzf's 'raw_fzf', changes include: 2 | -- DOES NOT SUPPORT WINDOWS 3 | -- does not close the pipe before all writes are complete 4 | -- option to not add '\n' on content function callbacks 5 | -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf.lua 6 | local uv = vim.loop 7 | 8 | local M = {} 9 | 10 | local function get_lines_from_file(file) 11 | local t = {} 12 | for v in file:lines() do 13 | table.insert(t, v) 14 | end 15 | return t 16 | end 17 | 18 | 19 | -- workaround to a potential 'tempname' bug? (#222) 20 | -- neovim doesn't guarantee the existence of the 21 | -- parent temp dir potentially failing `mkfifo` 22 | -- https://github.com/neovim/neovim/issues/1432 23 | -- https://github.com/neovim/neovim/pull/11284 24 | local function tempname() 25 | local tmpname = vim.fn.tempname() 26 | local parent = vim.fn.fnamemodify(tmpname, ':h') 27 | -- parent must exist for `mkfifo` to succeed 28 | -- if the neovim temp dir was deleted or the 29 | -- tempname already exists we use 'os.tmpname' 30 | if not uv.fs_stat(parent) or uv.fs_stat(tmpname) then 31 | tmpname = os.tmpname() 32 | -- 'os.tmpname' touches the file which 33 | -- will also fail `mkfifo`, delete it 34 | vim.fn.delete(tmpname) 35 | end 36 | return tmpname 37 | end 38 | 39 | -- contents can be either a table with tostring()able items, or a function that 40 | -- can be called repeatedly for values. the latter can use coroutines for async 41 | -- behavior. 42 | function M.raw_fzf(contents, fzf_cli_args, opts) 43 | if not coroutine.running() then 44 | error("please run function in a coroutine") 45 | end 46 | 47 | if not opts then opts = {} end 48 | local cwd = opts.fzf_cwd or opts.cwd 49 | local cmd = opts.fzf_binary or opts.fzf_bin or 'fzf' 50 | local fifotmpname = tempname() 51 | local outputtmpname = tempname() 52 | 53 | if fzf_cli_args then cmd = cmd .. " " .. fzf_cli_args end 54 | if opts.fzf_cli_args then cmd = cmd .. " " .. opts.fzf_cli_args end 55 | 56 | if contents then 57 | if type(contents) == "string" and #contents>0 then 58 | cmd = ("%s | %s"):format(contents, cmd) 59 | else 60 | cmd = ("%s < %s"):format(cmd, vim.fn.shellescape(fifotmpname)) 61 | end 62 | end 63 | 64 | cmd = ("%s > %s"):format(cmd, vim.fn.shellescape(outputtmpname)) 65 | 66 | local fd, output_pipe = nil, nil 67 | local finish_called = false 68 | local write_cb_count = 0 69 | 70 | -- Create the output pipe 71 | -- We use tbl for perf reasons, from ':help system': 72 | -- If {cmd} is a List it runs directly (no 'shell') 73 | -- If {cmd} is a String it runs in the 'shell' 74 | vim.fn.system({"mkfifo", fifotmpname}) 75 | 76 | local function finish(_) 77 | -- mark finish if once called 78 | finish_called = true 79 | -- close pipe if there are no outstanding writes 80 | if output_pipe and write_cb_count == 0 then 81 | output_pipe:close() 82 | output_pipe = nil 83 | end 84 | end 85 | 86 | local function write_cb(data, cb) 87 | if not output_pipe then return end 88 | write_cb_count = write_cb_count + 1 89 | output_pipe:write(data, function(err) 90 | -- decrement write call count 91 | write_cb_count = write_cb_count - 1 92 | -- this will call the user's cb 93 | if cb then cb(err) end 94 | if err then 95 | -- can fail with premature process kill 96 | finish(2) 97 | elseif finish_called and write_cb_count == 0 then 98 | -- 'termopen.on_exit' already called and did not close the 99 | -- pipe due to write_cb_count>0, since this is the last call 100 | -- we can close the fzf pipe 101 | finish(3) 102 | end 103 | end) 104 | end 105 | 106 | -- nvim-fzf compatibility, builds the user callback functions 107 | -- 1st argument: callback function that adds newline to each write 108 | -- 2nd argument: callback function thhat writes the data as is 109 | -- 3rd argument: direct access to the pipe object 110 | local function usr_write_cb(nl) 111 | local function end_of_data(usrdata, cb) 112 | if usrdata == nil then 113 | if cb then cb(nil) end 114 | finish(5) 115 | return true 116 | end 117 | return false 118 | end 119 | if nl then 120 | return function(usrdata, cb) 121 | if not end_of_data(usrdata, cb) then 122 | write_cb(tostring(usrdata).."\n", cb) 123 | end 124 | end 125 | else 126 | return function(usrdata, cb) 127 | if not end_of_data(usrdata, cb) then 128 | write_cb(usrdata, cb) 129 | end 130 | end 131 | end 132 | end 133 | 134 | local co = coroutine.running() 135 | vim.fn.termopen({"sh", "-c", cmd}, { 136 | cwd = cwd, 137 | env = { ['SHELL'] = 'sh' }, 138 | on_exit = function(_, rc, _) 139 | local f = io.open(outputtmpname) 140 | local output = get_lines_from_file(f) 141 | f:close() 142 | finish(1) 143 | vim.fn.delete(fifotmpname) 144 | vim.fn.delete(outputtmpname) 145 | if #output == 0 then output = nil end 146 | coroutine.resume(co, output, rc) 147 | end 148 | }) 149 | vim.cmd[[set ft=fzf]] 150 | vim.cmd[[startinsert]] 151 | 152 | if not contents or type(contents) == "string" then 153 | goto wait_for_fzf 154 | end 155 | 156 | -- have to open this after there is a reader (termopen) 157 | -- otherwise this will block 158 | fd = uv.fs_open(fifotmpname, "w", -1) 159 | output_pipe = uv.new_pipe(false) 160 | output_pipe:open(fd) 161 | -- print(output_pipe:getpeername()) 162 | 163 | -- this part runs in the background, when the user has selected, it will 164 | -- error out, but that doesn't matter so we just break out of the loop. 165 | if contents then 166 | if type(contents) == "table" then 167 | if not vim.tbl_isempty(contents) then 168 | write_cb(vim.tbl_map(function(x) return x.."\n" end, contents)) 169 | end 170 | finish(4) 171 | else 172 | contents(usr_write_cb(true), usr_write_cb(false), output_pipe) 173 | end 174 | end 175 | 176 | ::wait_for_fzf:: 177 | 178 | return coroutine.yield() 179 | end 180 | 181 | return M 182 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/dap.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local path = require "fzf-lua.path" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local actions = require "fzf-lua.actions" 6 | 7 | local _has_dap, _dap = nil, nil 8 | 9 | local M = {} 10 | 11 | -- attempt to load 'nvim-dap' every call 12 | -- in case the plugin was lazy loaded 13 | local function dap() 14 | if _has_dap and _dap then return _dap end 15 | _has_dap, _dap = pcall(require, 'dap') 16 | if not _has_dap or not _dap then 17 | utils.info("DAP requires 'mfussenegger/nvim-dap'") 18 | return false 19 | end 20 | return true 21 | end 22 | 23 | M.commands = function(opts) 24 | if not dap() then return end 25 | 26 | opts = config.normalize_opts(opts, config.globals.dap.commands) 27 | if not opts then return end 28 | 29 | local entries = {} 30 | for k, v in pairs(_dap) do 31 | if type(v) == "function" then 32 | table.insert(entries, k) 33 | end 34 | end 35 | 36 | opts.actions = { 37 | ["default"] = opts.actions and opts.actions.default or 38 | function(selected, _) 39 | _dap[selected[1]]() 40 | if require'fzf-lua.providers.ui_select'.is_registered() then 41 | -- opening an fzf-lua win from another requires this 42 | actions.ensure_insert_mode() 43 | end 44 | end, 45 | } 46 | 47 | opts.fzf_opts['--no-multi'] = '' 48 | 49 | core.fzf_wrap(opts, entries, function(selected) 50 | 51 | if not selected then return end 52 | actions.act(opts.actions, selected) 53 | 54 | end)() 55 | end 56 | 57 | M.configurations = function(opts) 58 | if not dap() then return end 59 | 60 | opts = config.normalize_opts(opts, config.globals.dap.configurations) 61 | if not opts then return end 62 | 63 | local entries = {} 64 | opts._cfgs = {} 65 | for lang, lang_cfgs in pairs(_dap.configurations) do 66 | for _, cfg in ipairs(lang_cfgs) do 67 | opts._cfgs[#entries+1] = cfg 68 | table.insert(entries, ("[%s] %s. %s"):format( 69 | utils.ansi_codes.green(lang), 70 | utils.ansi_codes.magenta(tostring(#entries+1)), 71 | cfg.name 72 | )) 73 | end 74 | end 75 | 76 | opts.actions = { 77 | ["default"] = opts.actions and opts.actions.default or 78 | function(selected, _) 79 | -- cannot run while in session 80 | if _dap.session() then return end 81 | local idx = selected and tonumber(selected[1]:match("(%d+).")) or nil 82 | if idx and opts._cfgs[idx] then 83 | _dap.run(opts._cfgs[idx]) 84 | end 85 | end, 86 | } 87 | 88 | opts.fzf_opts['--no-multi'] = '' 89 | 90 | core.fzf_wrap(opts, entries, function(selected) 91 | 92 | if not selected then return end 93 | actions.act(opts.actions, selected) 94 | 95 | end)() 96 | end 97 | 98 | M.breakpoints = function(opts) 99 | if not dap() then return end 100 | local dap_bps = require'dap.breakpoints' 101 | 102 | opts = config.normalize_opts(opts, config.globals.dap.breakpoints) 103 | if not opts then return end 104 | 105 | -- so we can have accurate info on resume 106 | opts.fn_pre_fzf = function() 107 | opts._locations = dap_bps.to_qf_list(dap_bps.get()) 108 | end 109 | 110 | -- run once to prevent opening an empty dialog 111 | opts.fn_pre_fzf() 112 | 113 | if vim.tbl_isempty(opts._locations) then 114 | utils.info("Breakpoint list is empty.") 115 | return 116 | end 117 | 118 | if not opts.cwd then opts.cwd = vim.loop.cwd() end 119 | 120 | opts.actions = vim.tbl_deep_extend("keep", opts.actions or {}, 121 | { 122 | ["ctrl-x"] = opts.actions and opts.actions['ctrl-x'] or 123 | { 124 | function(selected, o) 125 | for _, e in ipairs(selected) do 126 | local entry = path.entry_to_file(e, o.cwd) 127 | if entry.bufnr>0 and entry.line then 128 | dap_bps.remove(entry.bufnr, entry.line) 129 | end 130 | end 131 | end, 132 | -- resume after bp deletion 133 | actions.resume 134 | } 135 | }) 136 | 137 | local contents = function (cb) 138 | local entries = {} 139 | for _, entry in ipairs(opts._locations) do 140 | table.insert(entries, core.make_entry_lcol(opts, entry)) 141 | end 142 | 143 | for i, x in ipairs(entries) do 144 | x = ("[%s] %s"):format( 145 | -- tostring(opts._locations[i].bufnr), 146 | utils.ansi_codes.yellow(tostring(opts._locations[i].bufnr)), 147 | core.make_entry_file(opts, x)) 148 | if x then 149 | cb(x, function(err) 150 | if err then return end 151 | -- close the pipe to fzf, this 152 | -- removes the loading indicator in fzf 153 | cb(nil, function() end) 154 | end) 155 | end 156 | end 157 | cb(nil, function() end) 158 | end 159 | 160 | if opts.fzf_opts['--header'] == nil then 161 | opts.fzf_opts['--header'] = vim.fn.shellescape((':: %s to delete a Breakpoint') 162 | :format(utils.ansi_codes.yellow(""))) 163 | end 164 | 165 | opts = core.set_fzf_field_index(opts, 3, opts._is_skim and "{}" or "{..-2}") 166 | 167 | core.fzf_wrap(opts, contents, function(selected) 168 | 169 | if not selected then return end 170 | actions.act(opts.actions, selected, opts) 171 | 172 | end)() 173 | 174 | end 175 | 176 | M.variables = function(opts) 177 | if not dap() then return end 178 | 179 | opts = config.normalize_opts(opts, config.globals.dap.variables) 180 | if not opts then return end 181 | 182 | local session = _dap.session() 183 | if not session then 184 | utils.info("No active DAP session.") 185 | return 186 | end 187 | 188 | local entries = {} 189 | for _, s in pairs(session.current_frame.scopes or {}) do 190 | if s.variables then 191 | for _, v in pairs(s.variables) do 192 | if v.type ~= '' and v.value ~= '' then 193 | table.insert(entries, ("[%s] %s = %s"):format( 194 | utils.ansi_codes.green(v.type), 195 | -- utils.ansi_codes.red(v.name), 196 | v.name, 197 | v.value 198 | )) 199 | end 200 | end 201 | end 202 | end 203 | 204 | core.fzf_wrap(opts, entries, function(selected) 205 | 206 | if not selected then return end 207 | actions.act(opts.actions, selected) 208 | 209 | end)() 210 | 211 | end 212 | 213 | M.frames = function(opts) 214 | if not dap() then return end 215 | 216 | opts = config.normalize_opts(opts, config.globals.dap.frames) 217 | if not opts then return end 218 | 219 | local session = _dap.session() 220 | if not session then 221 | utils.info("No active DAP session.") 222 | return 223 | end 224 | 225 | if not session.stopped_thread_id then 226 | utils.info("Unable to switch frames unless stopped.") 227 | return 228 | end 229 | 230 | opts._frames = session.threads[session.stopped_thread_id].frames 231 | 232 | opts.actions = { 233 | ["default"] = opts.actions and opts.actions.default or 234 | function(selected, o) 235 | local sess = _dap.session() 236 | if not sess or not sess.stopped_thread_id then return end 237 | local idx = selected and tonumber(selected[1]:match("(%d+).")) or nil 238 | if idx and o._frames[idx] then 239 | session:_frame_set(o._frames[idx]) 240 | end 241 | end, 242 | } 243 | 244 | local entries = {} 245 | for i, f in ipairs(opts._frames) do 246 | table.insert(entries, ("%s. [%s] %s%s"):format( 247 | utils.ansi_codes.magenta(tostring(i)), 248 | utils.ansi_codes.green(f.name), 249 | f.source and f.source.name or '' , 250 | f.line and ((":%d"):format(f.line)) or '' 251 | )) 252 | end 253 | 254 | opts.fzf_opts['--no-multi'] = '' 255 | 256 | core.fzf_wrap(opts, entries, function(selected) 257 | 258 | if not selected then return end 259 | actions.act(opts.actions, selected, opts) 260 | 261 | end)() 262 | 263 | end 264 | 265 | return M 266 | -------------------------------------------------------------------------------- /lua/fzf-lua/path.lua: -------------------------------------------------------------------------------- 1 | local utils = require "fzf-lua.utils" 2 | local string_byte = string.byte 3 | 4 | local M = {} 5 | 6 | M.separator = function() 7 | return '/' 8 | end 9 | 10 | M.dot_byte = string_byte('.') 11 | M.separator_byte = string_byte(M.separator()) 12 | 13 | M.starts_with_separator = function(path) 14 | return string_byte(path, 1) == M.separator_byte 15 | -- return path:find("^"..M.separator()) == 1 16 | end 17 | 18 | M.starts_with_cwd = function(path) 19 | return #path>1 20 | and string_byte(path, 1) == M.dot_byte 21 | and string_byte(path, 2) == M.separator_byte 22 | -- return path:match("^."..M.separator()) ~= nil 23 | end 24 | 25 | M.strip_cwd_prefix = function(path) 26 | return #path>2 and path:sub(3) 27 | end 28 | 29 | function M.tail(path) 30 | local os_sep = string_byte(M.separator()) 31 | 32 | for i=#path,1,-1 do 33 | if string_byte(path, i) == os_sep then 34 | return path:sub(i+1) 35 | end 36 | end 37 | return path 38 | end 39 | 40 | function M.extension(path) 41 | for i=#path,1,-1 do 42 | if string_byte(path, i) == 46 then 43 | return path:sub(i+1) 44 | end 45 | end 46 | return path 47 | end 48 | 49 | function M.to_matching_str(path) 50 | -- return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)'):gsub('(%_)', '(%%_)') 51 | -- above is missing other lua special chars like '+' etc (#315) 52 | return utils.lua_regex_escape(path) 53 | end 54 | 55 | function M.join(paths) 56 | -- gsub to remove double separator 57 | return table.concat(paths, M.separator()):gsub( 58 | M.separator()..M.separator(), M.separator()) 59 | end 60 | 61 | function M.split(path) 62 | return path:gmatch('[^'..M.separator()..']+'..M.separator()..'?') 63 | end 64 | 65 | ---Get the basename of the given path. 66 | ---@param path string 67 | ---@return string 68 | function M.basename(path) 69 | path = M.remove_trailing(path) 70 | local i = path:match("^.*()" .. M.separator()) 71 | if not i then return path end 72 | return path:sub(i + 1, #path) 73 | end 74 | 75 | ---Get the path to the parent directory of the given path. Returns `nil` if the 76 | ---path has no parent. 77 | ---@param path string 78 | ---@param remove_trailing boolean 79 | ---@return string|nil 80 | function M.parent(path, remove_trailing) 81 | path = " " .. M.remove_trailing(path) 82 | local i = path:match("^.+()" .. M.separator()) 83 | if not i then return nil end 84 | path = path:sub(2, i) 85 | if remove_trailing then 86 | path = M.remove_trailing(path) 87 | end 88 | return path 89 | end 90 | 91 | ---Get a path relative to another path. 92 | ---@param path string 93 | ---@param relative_to string 94 | ---@return string 95 | function M.relative(path, relative_to) 96 | local p, _ = path:gsub("^" .. M.to_matching_str(M.add_trailing(relative_to)), "") 97 | return p 98 | end 99 | 100 | function M.is_relative(path, relative_to) 101 | local p = path:match("^" .. M.to_matching_str(M.add_trailing(relative_to))) 102 | return p ~= nil 103 | end 104 | 105 | function M.add_trailing(path) 106 | if path:sub(-1) == M.separator() then 107 | return path 108 | end 109 | 110 | return path..M.separator() 111 | end 112 | 113 | function M.remove_trailing(path) 114 | local p, _ = path:gsub(M.separator()..'$', '') 115 | return p 116 | end 117 | 118 | function M.shorten(path, max_length) 119 | if string.len(path) > max_length - 1 then 120 | path = path:sub(string.len(path) - max_length + 1, string.len(path)) 121 | local i = path:match("()" .. M.separator()) 122 | if not i then 123 | return "…" .. path 124 | end 125 | return "…" .. path:sub(i, -1) 126 | else 127 | return path 128 | end 129 | end 130 | 131 | local function lastIndexOf(haystack, needle) 132 | local i=haystack:match(".*"..needle.."()") 133 | if i==nil then return nil else return i-1 end 134 | end 135 | 136 | local function stripBeforeLastOccurrenceOf(str, sep) 137 | local idx = lastIndexOf(str, sep) or 0 138 | return str:sub(idx+1), idx 139 | end 140 | 141 | 142 | function M.entry_to_ctag(entry, noesc) 143 | local scode = entry:match("%:.-/^?\t?(.*)/") 144 | -- if tag name contains a slash we could 145 | -- have the wrong match, most tags start 146 | -- with ^ so try to match based on that 147 | scode = scode and scode:match("/^(.*)") or scode 148 | if scode and not noesc then 149 | -- scode = string.gsub(scode, "[$]$", "") 150 | scode = string.gsub(scode, [[\\]], [[\]]) 151 | scode = string.gsub(scode, [[\/]], [[/]]) 152 | scode = string.gsub(scode, "[*]", [[\*]]) 153 | end 154 | return scode 155 | end 156 | 157 | function M.entry_to_location(entry) 158 | local uri, line, col = entry:match("^(.*://.*):(%d+):(%d+):") 159 | line = line and tonumber(line) or 1 160 | col = col and tonumber(col) or 1 161 | return { 162 | stripped = entry, 163 | line = line, 164 | col = col, 165 | uri = uri, 166 | range = { 167 | start = { 168 | line = line-1, 169 | character = col-1, 170 | } 171 | } 172 | } 173 | end 174 | 175 | function M.entry_to_file(entry, cwd, force_uri) 176 | -- Remove ansi coloring and prefixed icons 177 | entry = utils.strip_ansi_coloring(entry) 178 | local stripped, idx = stripBeforeLastOccurrenceOf(entry, utils.nbsp) 179 | local isURI = stripped:match("^%a+://") 180 | -- Prepend cwd before constructing the URI (#341) 181 | if cwd and #cwd>0 and not isURI and 182 | not M.starts_with_separator(stripped) then 183 | stripped = M.join({cwd, stripped}) 184 | end 185 | -- #336: force LSP jumps using 'vim.lsp.util.jump_to_location' 186 | -- so that LSP entries are added to the tag stack 187 | if not isURI and force_uri then 188 | isURI = true 189 | stripped = "file://" .. stripped 190 | end 191 | -- entries from 'buffers' contain '[]' 192 | -- buffer placeholder always comes before the nbsp 193 | local bufnr = idx>1 and entry:sub(1, idx):match("%[(%d+)") or nil 194 | if isURI and not bufnr then 195 | -- Issue #195, when using nvim-jdtls 196 | -- https://github.com/mfussenegger/nvim-jdtls 197 | -- LSP entries inside .jar files appear as URIs 198 | -- 'jdt://' which can then be opened with 199 | -- 'vim.lsp.util.jump_to_location' or 200 | -- 'lua require('jdtls').open_jdt_link(vim.fn.expand('jdt://...'))' 201 | -- Convert to location item so we can use 'jump_to_location' 202 | -- This can also work with any 'file://' prefixes 203 | return M.entry_to_location(stripped) 204 | end 205 | local s = utils.strsplit(stripped, ":") 206 | if not s[1] then return {} end 207 | local file = s[1] 208 | local line = tonumber(s[2]) 209 | local col = tonumber(s[3]) 210 | local terminal 211 | if bufnr then 212 | terminal = utils.is_term_buffer(bufnr) 213 | if terminal then 214 | file, line = stripped:match("([^:]+):(%d+)") 215 | end 216 | end 217 | return { 218 | stripped = stripped, 219 | bufnr = tonumber(bufnr), 220 | bufname = bufnr and vim.api.nvim_buf_is_valid(tonumber(bufnr)) 221 | and vim.api.nvim_buf_get_name(tonumber(bufnr)), 222 | terminal = terminal, 223 | path = file, 224 | line = tonumber(line) or 1, 225 | col = tonumber(col) or 1, 226 | } 227 | end 228 | 229 | function M.git_cwd(cmd, cwd) 230 | if not cwd then return cmd end 231 | cwd = vim.fn.expand(cwd) 232 | if type(cmd) == 'string' then 233 | local arg_cwd = ("-C %s "):format(vim.fn.shellescape(cwd)) 234 | cmd = cmd:gsub("^git ", "git " .. arg_cwd) 235 | else 236 | cmd = utils.tbl_deep_clone(cmd) 237 | table.insert(cmd, 2, "-C") 238 | table.insert(cmd, 3, cwd) 239 | end 240 | return cmd 241 | end 242 | 243 | function M.is_git_repo(cwd, noerr) 244 | return not not M.git_root(cwd, noerr) 245 | end 246 | 247 | function M.git_root(cwd, noerr) 248 | local cmd = M.git_cwd({"git", "rev-parse", "--show-toplevel"}, cwd) 249 | local output, err = utils.io_systemlist(cmd) 250 | if err ~= 0 then 251 | if not noerr then utils.info(unpack(output)) end 252 | return nil 253 | end 254 | return output[1] 255 | end 256 | 257 | return M 258 | -------------------------------------------------------------------------------- /lua/fzf-lua/init.lua: -------------------------------------------------------------------------------- 1 | local utils = require "fzf-lua.utils" 2 | local config = require "fzf-lua.config" 3 | 4 | do 5 | -- using the latest nightly 'NVIM v0.6.0-dev+569-g2ecf0a4c6' 6 | -- pluging '.vim' initialization sometimes doesn't get called 7 | local path = require "fzf-lua.path" 8 | local currFile = debug.getinfo(1, 'S').source:gsub("^@", "") 9 | vim.g.fzf_lua_directory = path.parent(currFile) 10 | 11 | -- Manually source the vimL script containing ':FzfLua' cmd 12 | if not vim.g.loaded_fzf_lua then 13 | local fzf_lua_vim = path.join({ 14 | path.parent(path.parent(vim.g.fzf_lua_directory)), 15 | "plugin", "fzf-lua.vim" 16 | }) 17 | if vim.loop.fs_stat(fzf_lua_vim) then 18 | vim.cmd(("source %s"):format(fzf_lua_vim)) 19 | -- utils.info(("manually loaded '%s'"):format(fzf_lua_vim)) 20 | end 21 | end 22 | 23 | -- Create a new RPC server (tmp socket) to listen to messages (actions/headless) 24 | -- this is safer than using $NVIM_LISTEN_ADDRESS. If the user is using a custom 25 | -- fixed $NVIM_LISTEN_ADDRESS different neovim instances will use the same path 26 | -- as their address and messages won't be recieved on older instances 27 | if not vim.g.fzf_lua_server then 28 | vim.g.fzf_lua_server = vim.fn.serverstart() 29 | end 30 | 31 | end 32 | 33 | local M = {} 34 | 35 | function M.setup(opts) 36 | local globals = vim.tbl_deep_extend("keep", opts, config.globals) 37 | -- backward compatibility before winopts was it's own struct 38 | for k, _ in pairs(globals.winopts) do 39 | if opts[k] ~= nil then globals.winopts[k] = opts[k] end 40 | end 41 | -- backward compatibility for 'fzf_binds' 42 | if opts.fzf_binds then 43 | utils.warn("'fzf_binds' is deprecated, moved under 'keymap.fzf', see ':help fzf-lua-customization'") 44 | globals.keymap.fzf = opts.fzf_binds 45 | end 46 | -- do not merge, override the bind tables 47 | for t, v in pairs({ 48 | ['keymap'] = { 'fzf', 'builtin' }, 49 | ['actions'] = { 'files', 'buffers' }, 50 | }) do 51 | for _, k in ipairs(v) do 52 | if opts[t] and opts[t][k] then 53 | globals[t][k] = opts[t][k] 54 | end 55 | end 56 | end 57 | -- override BAT_CONFIG_PATH to prevent a 58 | -- conflct with '$XDG_DATA_HOME/bat/config' 59 | local bat_theme = globals.previewers.bat.theme or globals.previewers.bat_native.theme 60 | local bat_config = globals.previewers.bat.config or globals.previewers.bat_native.config 61 | if bat_config then 62 | vim.env.BAT_CONFIG_PATH = vim.fn.expand(bat_config) 63 | end 64 | -- override the bat preview theme if set by caller 65 | if bat_theme and #bat_theme > 0 then 66 | vim.env.BAT_THEME = bat_theme 67 | end 68 | -- set lua_io if caller requested 69 | utils.set_lua_io(globals.lua_io) 70 | -- set custom   if caller requested 71 | if globals.nbsp then utils.nbsp = globals.nbsp end 72 | -- reset our globals based on user opts 73 | -- this doesn't happen automatically 74 | config.globals = globals 75 | globals = nil 76 | end 77 | 78 | M.resume = require'fzf-lua.core'.fzf_resume 79 | 80 | M.files = require'fzf-lua.providers.files'.files 81 | M.args = require'fzf-lua.providers.files'.args 82 | M.grep = require'fzf-lua.providers.grep'.grep 83 | M.live_grep = require'fzf-lua.providers.grep'.live_grep 84 | M.live_grep_native = require'fzf-lua.providers.grep'.live_grep_native 85 | M.live_grep_resume = require'fzf-lua.providers.grep'.live_grep_resume 86 | M.live_grep_glob = require'fzf-lua.providers.grep'.live_grep_glob 87 | M.grep_last = require'fzf-lua.providers.grep'.grep_last 88 | M.grep_cword = require'fzf-lua.providers.grep'.grep_cword 89 | M.grep_cWORD = require'fzf-lua.providers.grep'.grep_cWORD 90 | M.grep_visual = require'fzf-lua.providers.grep'.grep_visual 91 | M.grep_curbuf = require'fzf-lua.providers.grep'.grep_curbuf 92 | M.lgrep_curbuf = require'fzf-lua.providers.grep'.lgrep_curbuf 93 | M.grep_project = require'fzf-lua.providers.grep'.grep_project 94 | M.git_files = require'fzf-lua.providers.git'.files 95 | M.git_status = require'fzf-lua.providers.git'.status 96 | M.git_commits = require'fzf-lua.providers.git'.commits 97 | M.git_bcommits = require'fzf-lua.providers.git'.bcommits 98 | M.git_branches = require'fzf-lua.providers.git'.branches 99 | M.oldfiles = require'fzf-lua.providers.oldfiles'.oldfiles 100 | M.quickfix = require'fzf-lua.providers.quickfix'.quickfix 101 | M.loclist = require'fzf-lua.providers.quickfix'.loclist 102 | M.buffers = require'fzf-lua.providers.buffers'.buffers 103 | M.tabs = require'fzf-lua.providers.buffers'.tabs 104 | M.lines = require'fzf-lua.providers.buffers'.lines 105 | M.blines = require'fzf-lua.providers.buffers'.blines 106 | M.help_tags = require'fzf-lua.providers.helptags'.helptags 107 | M.man_pages = require'fzf-lua.providers.manpages'.manpages 108 | M.colorschemes = require'fzf-lua.providers.colorschemes'.colorschemes 109 | 110 | M.tags = require'fzf-lua.providers.tags'.tags 111 | M.btags = require'fzf-lua.providers.tags'.btags 112 | M.tags_grep = require'fzf-lua.providers.tags'.grep 113 | M.tags_grep_cword = require'fzf-lua.providers.tags'.grep_cword 114 | M.tags_grep_cWORD = require'fzf-lua.providers.tags'.grep_cWORD 115 | M.tags_grep_visual = require'fzf-lua.providers.tags'.grep_visual 116 | M.tags_live_grep = require'fzf-lua.providers.tags'.live_grep 117 | M.jumps = require'fzf-lua.providers.nvim'.jumps 118 | M.changes = require'fzf-lua.providers.nvim'.changes 119 | M.tagstack = require'fzf-lua.providers.nvim'.tagstack 120 | M.marks = require'fzf-lua.providers.nvim'.marks 121 | M.keymaps = require'fzf-lua.providers.nvim'.keymaps 122 | M.registers = require'fzf-lua.providers.nvim'.registers 123 | M.commands = require'fzf-lua.providers.nvim'.commands 124 | M.command_history = require'fzf-lua.providers.nvim'.command_history 125 | M.search_history = require'fzf-lua.providers.nvim'.search_history 126 | M.spell_suggest = require'fzf-lua.providers.nvim'.spell_suggest 127 | M.filetypes = require'fzf-lua.providers.nvim'.filetypes 128 | M.packadd = require'fzf-lua.providers.nvim'.packadd 129 | 130 | M.lsp_typedefs = require'fzf-lua.providers.lsp'.typedefs 131 | M.lsp_references = require'fzf-lua.providers.lsp'.references 132 | M.lsp_definitions = require'fzf-lua.providers.lsp'.definitions 133 | M.lsp_declarations = require'fzf-lua.providers.lsp'.declarations 134 | M.lsp_implementations = require'fzf-lua.providers.lsp'.implementations 135 | M.lsp_document_symbols = require'fzf-lua.providers.lsp'.document_symbols 136 | M.lsp_workspace_symbols = require'fzf-lua.providers.lsp'.workspace_symbols 137 | M.lsp_live_workspace_symbols = require'fzf-lua.providers.lsp'.live_workspace_symbols 138 | M.lsp_code_actions = require'fzf-lua.providers.lsp'.code_actions 139 | M.lsp_document_diagnostics = require'fzf-lua.providers.lsp'.diagnostics 140 | M.lsp_workspace_diagnostics = require'fzf-lua.providers.lsp'.workspace_diagnostics 141 | 142 | M.register_ui_select = require'fzf-lua.providers.ui_select'.register 143 | M.deregister_ui_select = require'fzf-lua.providers.ui_select'.deregister 144 | 145 | M.dap_commands = require'fzf-lua.providers.dap'.commands 146 | M.dap_configurations = require'fzf-lua.providers.dap'.configurations 147 | M.dap_breakpoints = require'fzf-lua.providers.dap'.breakpoints 148 | M.dap_variables = require'fzf-lua.providers.dap'.variables 149 | M.dap_frames = require'fzf-lua.providers.dap'.frames 150 | 151 | -- API shortcuts 152 | M.fzf = require'fzf-lua.core'.fzf 153 | M.fzf_wrap = require'fzf-lua.core'.fzf_wrap 154 | M.raw_fzf = require'fzf-lua.fzf'.raw_fzf 155 | 156 | -- exported modules 157 | M._exported_modules = { 158 | 'win', 159 | 'core', 160 | 'path', 161 | 'utils', 162 | 'libuv', 163 | 'shell', 164 | 'config', 165 | 'actions', 166 | } 167 | 168 | -- excluded from builtin / auto-complete 169 | M._excluded_meta = { 170 | 'setup', 171 | 'fzf', 172 | 'fzf_wrap', 173 | 'raw_fzf', 174 | '_excluded_meta', 175 | '_excluded_metamap', 176 | '_exported_modules', 177 | } 178 | 179 | for _, m in ipairs(M._exported_modules) do 180 | M[m] = require("fzf-lua." .. m) 181 | end 182 | 183 | M._excluded_metamap = {} 184 | for _, t in pairs({ M._excluded_meta, M._exported_modules }) do 185 | for _, m in ipairs(t) do 186 | M._excluded_metamap[m] = true 187 | end 188 | end 189 | 190 | M.builtin = function(opts) 191 | if not opts then opts = {} end 192 | opts.metatable = M 193 | opts.metatable_exclude = M._excluded_metamap 194 | return require'fzf-lua.providers.module'.metatable(opts) 195 | end 196 | 197 | return M 198 | -------------------------------------------------------------------------------- /lua/fzf-lua/previewer/fzf.lua: -------------------------------------------------------------------------------- 1 | local path = require "fzf-lua.path" 2 | local shell = require "fzf-lua.shell" 3 | local utils = require "fzf-lua.utils" 4 | local Object = require "fzf-lua.class" 5 | 6 | local Previewer = {} 7 | 8 | Previewer.base = Object:extend() 9 | 10 | -- Previewer base object 11 | function Previewer.base:new(o, opts) 12 | o = o or {} 13 | self.type = "cmd"; 14 | self.cmd = o.cmd; 15 | self.args = o.args or ""; 16 | self.opts = opts; 17 | return self 18 | end 19 | 20 | function Previewer.base:preview_window(_) 21 | return nil 22 | end 23 | 24 | function Previewer.base:preview_offset() 25 | --[[ 26 | # 27 | # Explanation of the fzf preview offset options: 28 | # 29 | # ~3 Top 3 lines as the fixed header 30 | # +{2} Base scroll offset extracted from the second field 31 | # +3 Extra offset to compensate for the 3-line header 32 | # /2 Put in the middle of the preview area 33 | # 34 | '--preview-window '~3:+{2}+3/2'' 35 | ]] 36 | if self.opts.line_field_index then 37 | return ("+{%d}-/2"):format(self.opts.line_field_index) 38 | end 39 | end 40 | 41 | function Previewer.base:fzf_delimiter() 42 | if not self.opts.line_field_index then return end 43 | -- set delimiter to ':' 44 | -- entry format is 'file:line:col: text' 45 | local delim = self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"] 46 | if not delim then 47 | delim = '[:]' 48 | elseif not delim:match(":") then 49 | if delim:match("%[.*%]")then 50 | delim = delim:match("(%[.*)%]") .. ':]' 51 | else 52 | delim = '[' .. 53 | utils.rg_escape(delim:match("^'?(.*)'$?")):gsub("%]", "\\]") 54 | .. ':]' 55 | end 56 | end 57 | return delim 58 | end 59 | 60 | -- Generic shell command previewer 61 | Previewer.cmd = Previewer.base:extend() 62 | 63 | function Previewer.cmd:new(o, opts) 64 | Previewer.cmd.super.new(self, o, opts) 65 | return self 66 | end 67 | 68 | function Previewer.cmd:cmdline(o) 69 | o = o or {} 70 | o.action = o.action or self:action(o) 71 | return vim.fn.shellescape(string.format('sh -c "%s %s `%s`"', 72 | self.cmd, self.args, o.action)) 73 | end 74 | 75 | function Previewer.cmd:action(o) 76 | o = o or {} 77 | local act = shell.raw_action(function (items, _, _) 78 | -- only preview first item 79 | local entry = path.entry_to_file(items[1], self.opts.cwd) 80 | return entry.bufname or entry.path 81 | end, self.opts.field_index_expr or "{}") 82 | return act 83 | end 84 | 85 | -- Specialized bat previewer 86 | Previewer.bat = Previewer.cmd:extend() 87 | 88 | function Previewer.bat:new(o, opts) 89 | Previewer.bat.super.new(self, o, opts) 90 | self.theme = o.theme 91 | return self 92 | end 93 | 94 | function Previewer.bat:cmdline(o) 95 | o = o or {} 96 | o.action = o.action or self:action(o) 97 | local highlight_line = "" 98 | if self.opts.line_field_index then 99 | highlight_line = string.format("--highlight-line={%d}", self.opts.line_field_index) 100 | end 101 | return vim.fn.shellescape(string.format('sh -c "%s %s %s `%s`"', 102 | self.cmd, self.args, highlight_line, self:action(o))) 103 | end 104 | 105 | -- Specialized head previewer 106 | Previewer.head = Previewer.cmd:extend() 107 | 108 | function Previewer.head:new(o, opts) 109 | Previewer.head.super.new(self, o, opts) 110 | return self 111 | end 112 | 113 | function Previewer.head:cmdline(o) 114 | o = o or {} 115 | o.action = o.action or self:action(o) 116 | local lines = "--lines=-0" 117 | -- print all lines instead 118 | -- if self.opts.line_field_index then 119 | -- lines = string.format("--lines={%d}", self.opts.line_field_index) 120 | -- end 121 | return vim.fn.shellescape(string.format('sh -c "%s %s %s `%s`"', 122 | self.cmd, self.args, lines, self:action(o))) 123 | end 124 | 125 | -- new async_action from nvim-fzf 126 | Previewer.cmd_async = Previewer.base:extend() 127 | 128 | function Previewer.cmd_async:new(o, opts) 129 | Previewer.cmd_async.super.new(self, o, opts) 130 | return self 131 | end 132 | 133 | local grep_tag = function(file, tag) 134 | local line = 1 135 | local filepath = file 136 | local pattern = utils.rg_escape(tag) 137 | if not pattern or not filepath then return line end 138 | local grep_cmd = vim.fn.executable("rg") == 1 139 | and {"rg", "--line-number"} 140 | or {"grep", "-n", "-P"} 141 | -- ctags uses '$' at the end of short patterns 142 | -- 'rg|grep' does not match these properly when 143 | -- 'fileformat' isn't set to 'unix', when set to 144 | -- 'dos' we need to prepend '$' with '\r$' with 'rg' 145 | -- it is simpler to just ignore it compleley. 146 | --[[ local ff = fileformat(filepath) 147 | if ff == 'dos' then 148 | pattern = pattern:gsub("\\%$$", "\\r%$") 149 | else 150 | pattern = pattern:gsub("\\%$$", "%$") 151 | end --]] 152 | -- equivalent pattern to `rg --crlf` 153 | -- see discussion in #219 154 | pattern = pattern:gsub("\\%$$", "\\r??%$") 155 | local cmd = utils.tbl_deep_clone(grep_cmd) 156 | table.insert(cmd, pattern) 157 | table.insert(cmd, filepath) 158 | local out = utils.io_system(cmd) 159 | if not utils.shell_error() then 160 | line = out:match("[^:]+") 161 | else 162 | utils.warn(("Unable to find pattern '%s' in file '%s'"):format(pattern, file)) 163 | end 164 | -- if line == 1 then print(cmd) end 165 | return line 166 | end 167 | 168 | function Previewer.cmd_async:parse_entry_and_verify(entrystr) 169 | local entry = path.entry_to_file(entrystr, self.opts.cwd) 170 | local filepath = entry.bufname or entry.path or '' 171 | if self.opts._ctag and entry.line<=1 then 172 | -- tags without line numbers 173 | -- make sure we don't already have line # 174 | -- (in the case the line no. is actually 1) 175 | local line = entry.stripped:match("[^:]+(%d+):") 176 | local ctag = path.entry_to_ctag(entry.stripped, true) 177 | if not line and ctag then 178 | entry.ctag = ctag 179 | entry.line = grep_tag(filepath, entry.ctag) 180 | end 181 | end 182 | local errcmd = nil 183 | -- verify the file exists on disk and is accessible 184 | if #filepath==0 or not vim.loop.fs_stat(filepath) then 185 | errcmd = ('echo "%s: NO SUCH FILE OR ACCESS DENIED"'):format( 186 | filepath and #filepath>0 and vim.fn.shellescape(filepath) or "") 187 | end 188 | return filepath, entry, errcmd 189 | end 190 | 191 | function Previewer.cmd_async:cmdline(o) 192 | o = o or {} 193 | local act = shell.preview_action_cmd(function(items) 194 | local filepath, _, errcmd = self:parse_entry_and_verify(items[1]) 195 | local cmd = errcmd or ('%s %s %s'):format( 196 | self.cmd, self.args, vim.fn.shellescape(filepath)) 197 | -- uncomment to see the command in the preview window 198 | -- cmd = vim.fn.shellescape(cmd) 199 | return cmd 200 | end, "{}") 201 | return act 202 | end 203 | 204 | Previewer.bat_async = Previewer.cmd_async:extend() 205 | 206 | function Previewer.bat_async:new(o, opts) 207 | Previewer.bat_async.super.new(self, o, opts) 208 | self.theme = o.theme 209 | return self 210 | end 211 | 212 | function Previewer.bat_async:cmdline(o) 213 | o = o or {} 214 | local act = shell.preview_action_cmd(function(items, fzf_lines) 215 | local filepath, entry, errcmd = self:parse_entry_and_verify(items[1]) 216 | local line_range = '' 217 | if entry.ctag then 218 | -- this is a ctag without line numbers, since we can't 219 | -- provide the preview file offset to fzf via the field 220 | -- index expression we use '--line-range' instead 221 | local start_line = math.max(1, entry.line-fzf_lines/2) 222 | local end_line = start_line + fzf_lines-1 223 | line_range = ("--line-range=%d:%d"):format(start_line, end_line) 224 | end 225 | local cmd = errcmd or ('%s %s %s %s %s'):format( 226 | self.cmd, self.args, 227 | self.opts.line_field_index and 228 | ("--highlight-line=%d"):format(entry.line) or '', 229 | line_range, 230 | vim.fn.shellescape(filepath)) 231 | -- uncomment to see the command in the preview window 232 | -- cmd = vim.fn.shellescape(cmd) 233 | return cmd 234 | end, "{}") 235 | return act 236 | end 237 | 238 | Previewer.git_diff = Previewer.base:extend() 239 | 240 | function Previewer.git_diff:new(o, opts) 241 | Previewer.git_diff.super.new(self, o, opts) 242 | self.cmd_deleted = path.git_cwd(o.cmd_deleted, opts.cwd) 243 | self.cmd_modified = path.git_cwd(o.cmd_modified, opts.cwd) 244 | self.cmd_untracked = path.git_cwd(o.cmd_untracked, opts.cwd) 245 | self.pager = o.pager 246 | do 247 | -- populate the icon mappings 248 | local icons_overrides = o._fn_git_icons and o._fn_git_icons() 249 | self.git_icons = {} 250 | for _, i in ipairs({ "D", "M", "R", "A", "C", "?" }) do 251 | self.git_icons[i] = 252 | icons_overrides and icons_overrides[i] and 253 | utils.lua_regex_escape(icons_overrides[i].icon) or i 254 | end 255 | end 256 | return self 257 | end 258 | 259 | function Previewer.git_diff:cmdline(o) 260 | o = o or {} 261 | local act = shell.preview_action_cmd(function(items, fzf_lines, fzf_columns) 262 | if not items or vim.tbl_isempty(items) then 263 | utils.warn("shell error while running preview action.") 264 | return 265 | end 266 | local is_deleted = items[1]:match(self.git_icons['D']..utils.nbsp) ~= nil 267 | local is_modified = items[1]:match("[" .. 268 | self.git_icons['M'] .. 269 | self.git_icons['R'] .. 270 | self.git_icons['A'] .. 271 | "]" ..utils.nbsp) ~= nil 272 | local is_untracked = items[1]:match("[" .. 273 | self.git_icons['?'] .. 274 | self.git_icons['C'] .. 275 | "]"..utils.nbsp) ~= nil 276 | local file = path.entry_to_file(items[1], self.opts.cwd) 277 | local cmd = nil 278 | if is_modified then cmd = self.cmd_modified 279 | elseif is_deleted then cmd = self.cmd_deleted 280 | elseif is_untracked then cmd = self.cmd_untracked end 281 | local pager = "" 282 | if self.pager and #self.pager>0 and 283 | vim.fn.executable(self.pager:match("[^%s]+")) == 1 then 284 | pager = '| ' .. self.pager 285 | end 286 | cmd = string.format('FZF_PREVIEW_LINES=%d;FZF_PREVIEW_COLUMNS=%d;%s %s %s', 287 | fzf_lines, fzf_columns, cmd, vim.fn.shellescape(file.path), pager) 288 | cmd = 'sh -c ' .. vim.fn.shellescape(cmd) 289 | if self.opts.debug then 290 | print("[DEBUG]: "..cmd.."\n") 291 | end 292 | -- uncomment to see the command in the preview window 293 | -- cmd = vim.fn.shellescape(cmd) 294 | return cmd 295 | -- we need to add '--' to mark the end of command options 296 | -- as git icon customization may contain special shell chars 297 | -- which will otherwise choke our preview cmd ('+', '-', etc) 298 | end, "-- {}") 299 | return act 300 | end 301 | 302 | Previewer.man_pages = Previewer.base:extend() 303 | 304 | function Previewer.man_pages:new(o, opts) 305 | Previewer.man_pages.super.new(self, o, opts) 306 | self.cmd = self.cmd or "man" 307 | return self 308 | end 309 | 310 | function Previewer.man_pages:cmdline(o) 311 | o = o or {} 312 | local act = shell.preview_action_cmd(function(items) 313 | -- local manpage = require'fzf-lua.providers.manpages'.getmanpage(items[1]) 314 | local manpage = items[1]:match("[^[,( ]+") 315 | local cmd = ("%s %s %s"):format( 316 | self.cmd, self.args, vim.fn.shellescape(manpage)) 317 | -- uncomment to see the command in the preview window 318 | -- cmd = vim.fn.shellescape(cmd) 319 | return cmd 320 | end, "{}") 321 | return act 322 | end 323 | 324 | return Previewer 325 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/buffers.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local path = require "fzf-lua.path" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local actions = require "fzf-lua.actions" 6 | 7 | local M = {} 8 | 9 | -- will hold current/previous buffer/tab 10 | local __STATE = {} 11 | 12 | local UPDATE_STATE = function() 13 | __STATE = { 14 | curtab = vim.api.nvim_win_get_tabpage(0), 15 | curbuf = vim.api.nvim_get_current_buf(), 16 | prevbuf = vim.fn.bufnr('#'), 17 | buflist = vim.api.nvim_list_bufs(), 18 | bufmap = (function() 19 | local map = {} 20 | for _, b in ipairs(vim.api.nvim_list_bufs()) do 21 | map[b] = true 22 | end 23 | return map 24 | end)() 25 | } 26 | end 27 | 28 | local filter_buffers = function(opts, unfiltered) 29 | 30 | if type(unfiltered) == 'function' then 31 | unfiltered = unfiltered() 32 | end 33 | 34 | local curtab_bufnrs = {} 35 | if opts.current_tab_only then 36 | for _, w in ipairs(vim.api.nvim_tabpage_list_wins(__STATE.curtab)) do 37 | local b = vim.api.nvim_win_get_buf(w) 38 | curtab_bufnrs[b] = true 39 | end 40 | end 41 | 42 | local excluded = {} 43 | local bufnrs = vim.tbl_filter(function(b) 44 | if not opts.show_unlisted and 1 ~= vim.fn.buflisted(b) then 45 | excluded[b] = true 46 | end 47 | -- only hide unloaded buffers if opts.show_all_buffers is false, keep them listed if true or nil 48 | if opts.show_all_buffers == false and not vim.api.nvim_buf_is_loaded(b) then 49 | excluded[b] = true 50 | end 51 | if utils.buf_is_qf(b) then 52 | if opts.show_quickfix then 53 | -- show_quickfix trumps show_unlisted 54 | excluded[b] = nil 55 | else 56 | excluded[b] = true 57 | end 58 | end 59 | if opts.ignore_current_buffer and b == __STATE.curbuf then 60 | excluded[b] = true 61 | end 62 | if opts.current_tab_only and not curtab_bufnrs[b] then 63 | excluded[b] = true 64 | end 65 | if opts.no_term_buffers and utils.is_term_buffer(b) then 66 | excluded[b] = true 67 | end 68 | if opts.cwd_only and not path.is_relative(vim.api.nvim_buf_get_name(b), vim.loop.cwd()) then 69 | excluded[b] = true 70 | end 71 | return not excluded[b] 72 | end, unfiltered) 73 | 74 | return bufnrs, excluded 75 | end 76 | 77 | local populate_buffer_entries = function(opts, bufnrs, tabnr) 78 | local buffers = {} 79 | for _, bufnr in ipairs(bufnrs) do 80 | local flag = (bufnr == __STATE.curbuf and '%') or 81 | (bufnr == __STATE.prevbuf and '#') or ' ' 82 | 83 | local element = { 84 | bufnr = bufnr, 85 | flag = flag, 86 | info = vim.fn.getbufinfo(bufnr)[1], 87 | } 88 | 89 | -- get the correct lnum for tabbed buffers 90 | if tabnr then 91 | local winid = utils.winid_from_tab_buf(tabnr, bufnr) 92 | if winid then 93 | element.info.lnum = vim.api.nvim_win_get_cursor(winid)[1] 94 | end 95 | end 96 | 97 | table.insert(buffers, element) 98 | end 99 | if opts.sort_lastused then 100 | table.sort(buffers, function(a, b) 101 | return a.info.lastused > b.info.lastused 102 | end) 103 | end 104 | return buffers 105 | end 106 | 107 | 108 | local function gen_buffer_entry(opts, buf, hl_curbuf) 109 | -- local hidden = buf.info.hidden == 1 and 'h' or 'a' 110 | local hidden = '' 111 | local readonly = vim.api.nvim_buf_get_option(buf.bufnr, 'readonly') and '=' or ' ' 112 | local changed = buf.info.changed == 1 and '+' or ' ' 113 | local flags = hidden .. readonly .. changed 114 | local leftbr = utils.ansi_codes.clear('[') 115 | local rightbr = utils.ansi_codes.clear(']') 116 | local bufname = string.format("%s:%s", 117 | #buf.info.name>0 and 118 | path.relative(buf.info.name, vim.loop.cwd()) or 119 | utils.nvim_buf_get_name(buf.bufnr, buf.info), 120 | buf.info.lnum>0 and buf.info.lnum or "") 121 | if buf.flag == '%' then 122 | flags = utils.ansi_codes.red(buf.flag) .. flags 123 | if hl_curbuf then 124 | -- no header line, highlight current buffer 125 | leftbr = utils.ansi_codes.green('[') 126 | rightbr = utils.ansi_codes.green(']') 127 | bufname = utils.ansi_codes.green(bufname) 128 | end 129 | elseif buf.flag == '#' then 130 | flags = utils.ansi_codes.cyan(buf.flag) .. flags 131 | else 132 | flags = utils.nbsp .. flags 133 | end 134 | local bufnrstr = string.format("%s%s%s", leftbr, 135 | utils.ansi_codes.yellow(string.format(buf.bufnr)), rightbr) 136 | local buficon = '' 137 | local hl = '' 138 | if opts.file_icons then 139 | if utils.is_term_bufname(buf.info.name) then 140 | -- get shell-like icon for terminal buffers 141 | buficon, hl = core.get_devicon(buf.info.name, "sh") 142 | else 143 | local filename = path.tail(buf.info.name) 144 | local extension = path.extension(filename) 145 | buficon, hl = core.get_devicon(filename, extension) 146 | end 147 | if opts.color_icons then 148 | buficon = utils.ansi_codes[hl](buficon) 149 | end 150 | end 151 | local item_str = string.format("%s%s%s%s%s%s%s%s", 152 | utils._if(opts._prefix, opts._prefix, ''), 153 | string.format("%-32s", bufnrstr), 154 | utils.nbsp, 155 | flags, 156 | utils.nbsp, 157 | buficon, 158 | utils.nbsp, 159 | bufname) 160 | return item_str 161 | end 162 | 163 | 164 | M.buffers = function(opts) 165 | 166 | opts = config.normalize_opts(opts, config.globals.buffers) 167 | if not opts then return end 168 | 169 | -- get current tab/buffer/previos buffer 170 | -- save as a func ref for resume to reuse 171 | opts.fn_pre_fzf = UPDATE_STATE 172 | 173 | local contents = function(cb) 174 | 175 | local filtered = filter_buffers(opts, __STATE.buflist) 176 | 177 | if next(filtered) then 178 | local buffers = populate_buffer_entries(opts, filtered) 179 | for _, bufinfo in pairs(buffers) do 180 | cb(gen_buffer_entry(opts, bufinfo, not opts.sort_lastused)) 181 | end 182 | end 183 | cb(nil) 184 | end 185 | 186 | opts.fzf_opts['--header-lines'] = 187 | (not opts.ignore_current_buffer and opts.sort_lastused) and '1' 188 | 189 | opts = core.set_fzf_field_index(opts) 190 | 191 | core.fzf_wrap(opts, contents, function(selected) 192 | 193 | if not selected then return end 194 | 195 | actions.act(opts.actions, selected, opts) 196 | 197 | end)() 198 | end 199 | 200 | M.lines = function(opts) 201 | opts = config.normalize_opts(opts, config.globals.lines) 202 | M.buffer_lines(opts) 203 | end 204 | 205 | M.blines = function(opts) 206 | opts = config.normalize_opts(opts, config.globals.blines) 207 | opts.current_buffer_only = true 208 | M.buffer_lines(opts) 209 | end 210 | 211 | 212 | M.buffer_lines = function(opts) 213 | if not opts then return end 214 | 215 | opts.fn_pre_fzf = UPDATE_STATE 216 | opts.fn_pre_fzf() 217 | 218 | local buffers = filter_buffers(opts, 219 | opts.current_buffer_only and { __STATE.curbuf } or __STATE.buflist) 220 | 221 | local items = {} 222 | 223 | for _, bufnr in ipairs(buffers) do 224 | local data = {} 225 | local filepath = vim.api.nvim_buf_get_name(bufnr) 226 | if vim.api.nvim_buf_is_loaded(bufnr) then 227 | data = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 228 | elseif vim.fn.filereadable(filepath) ~= 0 then 229 | data = vim.fn.readfile(filepath, "") 230 | end 231 | local bufname = path.basename(filepath) 232 | local buficon, hl 233 | if opts.file_icons then 234 | local filename = path.tail(bufname) 235 | local extension = path.extension(filename) 236 | buficon, hl = core.get_devicon(filename, extension) 237 | if opts.color_icons then 238 | buficon = utils.ansi_codes[hl](buficon) 239 | end 240 | end 241 | if not bufname or #bufname==0 then 242 | bufname = utils.nvim_buf_get_name(bufnr) 243 | end 244 | for l, text in ipairs(data) do 245 | table.insert(items, ("[%s]%s%s%s%s:%s: %s"):format( 246 | utils.ansi_codes.yellow(tostring(bufnr)), 247 | utils.nbsp, 248 | buficon or '', 249 | buficon and utils.nbsp or '', 250 | utils.ansi_codes.magenta(bufname), 251 | utils.ansi_codes.green(tostring(l)), 252 | text)) 253 | end 254 | end 255 | 256 | -- ignore bufnr when searching 257 | -- disable multi-select 258 | opts.fzf_opts["--no-multi"] = '' 259 | opts.fzf_opts["--preview-window"] = 'hidden:right:0' 260 | 261 | if opts.search and #opts.search>0 then 262 | opts.fzf_opts['--query'] = vim.fn.shellescape(opts.search) 263 | end 264 | 265 | opts = core.set_fzf_field_index(opts, 3, opts._is_skim and "{}" or "{..-2}") 266 | 267 | core.fzf_wrap(opts, items, function(selected) 268 | if not selected then return end 269 | 270 | -- get the line number 271 | local line = tonumber(selected[2]:match(":(%d+):")) 272 | 273 | actions.act(opts.actions, selected, opts) 274 | 275 | if line then 276 | -- add current location to jumplist 277 | local is_term = utils.is_term_buffer(0) 278 | if not is_term then vim.cmd("normal! m`") end 279 | vim.api.nvim_win_set_cursor(0, {line, 0}) 280 | if not is_term then vim.cmd("norm! zz") end 281 | end 282 | 283 | end)() 284 | end 285 | 286 | M.tabs = function(opts) 287 | 288 | opts = config.normalize_opts(opts, config.globals.tabs) 289 | if not opts then return end 290 | 291 | opts.fn_pre_fzf = UPDATE_STATE 292 | 293 | opts._list_bufs = function() 294 | local res = {} 295 | for _, t in ipairs(vim.api.nvim_list_tabpages()) do 296 | for _, w in ipairs(vim.api.nvim_tabpage_list_wins(t)) do 297 | local b = vim.api.nvim_win_get_buf(w) 298 | -- since this function is called after fzf window 299 | -- is created, exclude the scratch fzf buffers 300 | if __STATE.bufmap[b] then 301 | opts._tab_to_buf[t] = opts._tab_to_buf[t] or {} 302 | opts._tab_to_buf[t][b] = t 303 | table.insert(res, b) 304 | end 305 | end 306 | end 307 | return res 308 | end 309 | 310 | local contents = function(cb) 311 | 312 | opts._tab_to_buf = {} 313 | 314 | local filtered, excluded = filter_buffers(opts, opts._list_bufs) 315 | if not next(filtered) then return end 316 | 317 | -- remove the filtered-out buffers 318 | for b, _ in pairs(excluded) do 319 | for _, bufnrs in pairs(opts._tab_to_buf) do 320 | bufnrs[b] = nil 321 | end 322 | end 323 | 324 | for t, bufnrs in pairs(opts._tab_to_buf) do 325 | 326 | cb(("%d)%s%s\t%s"):format(t, utils.nbsp, 327 | utils.ansi_codes.blue("%s%s#%d"):format(opts.tab_title, utils.nbsp, t), 328 | (t==__STATE.curtab) and 329 | utils.ansi_codes.blue(utils.ansi_codes.bold(opts.tab_marker)) or '')) 330 | 331 | local bufnrs_flat = {} 332 | for b, _ in pairs(bufnrs) do 333 | table.insert(bufnrs_flat, b) 334 | end 335 | 336 | opts.sort_lastused = false 337 | opts._prefix = ("%d)%s%s%s"):format(t, utils.nbsp, utils.nbsp, utils.nbsp) 338 | local buffers = populate_buffer_entries(opts, bufnrs_flat, t) 339 | for _, bufinfo in pairs(buffers) do 340 | cb(gen_buffer_entry(opts, bufinfo, false)) 341 | end 342 | end 343 | cb(nil) 344 | end 345 | 346 | -- opts.fzf_opts["--no-multi"] = '' 347 | opts.fzf_opts["--preview-window"] = 'hidden:right:0' 348 | 349 | opts = core.set_fzf_field_index(opts, 3, "{}") 350 | 351 | core.fzf_wrap(opts, contents, function(selected) 352 | 353 | if not selected then return end 354 | 355 | actions.act(opts.actions, selected, opts) 356 | 357 | end)() 358 | end 359 | 360 | return M 361 | -------------------------------------------------------------------------------- /lua/fzf-lua/make_entry.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local path = require "fzf-lua.path" 4 | local utils = require "fzf-lua.utils" 5 | local config = nil 6 | 7 | -- attempt to load the current config 8 | -- should fail if we're running headless 9 | do 10 | local ok, module = pcall(require, "fzf-lua.config") 11 | if ok then config = module end 12 | end 13 | 14 | -- These globals are set by spawn.fn_transform loadstring 15 | ---@diagnostic disable-next-line: undefined-field 16 | M._fzf_lua_server = _G._fzf_lua_server 17 | ---@diagnostic disable-next-line: undefined-field 18 | M._devicons_path = _G._devicons_path 19 | ---@diagnostic disable-next-line: undefined-field 20 | M._devicons_setup = _G._devicons_setup 21 | 22 | local function load_config_section(s, datatype) 23 | if config then 24 | local keys = utils.strsplit(s, '.') 25 | local iter, sect = config, nil 26 | for i=1,#keys do 27 | iter = iter[keys[i]] 28 | if not iter then break end 29 | if i == #keys and type(iter) == datatype then 30 | sect = iter 31 | end 32 | end 33 | return sect 34 | elseif M._fzf_lua_server then 35 | -- load config from our running instance 36 | local res = nil 37 | local ok, errmsg = pcall(function() 38 | local chan_id = vim.fn.sockconnect("pipe", M._fzf_lua_server, { rpc = true }) 39 | res = vim.rpcrequest(chan_id, "nvim_exec_lua", ([[ 40 | return require'fzf-lua'.config.%s 41 | ]]):format(s), {}) 42 | vim.fn.chanclose(chan_id) 43 | end) 44 | if not ok then 45 | io.stderr:write(("Error loading remote config section '%s': %s\n") 46 | :format(s, errmsg)) 47 | elseif type(res) == datatype then 48 | return res 49 | end 50 | end 51 | end 52 | 53 | local function set_config_section(s, data) 54 | if M._fzf_lua_server then 55 | -- save config in our running instance 56 | local ok, errmsg = pcall(function() 57 | local chan_id = vim.fn.sockconnect("pipe", M._fzf_lua_server, { rpc = true }) 58 | vim.rpcrequest(chan_id, "nvim_exec_lua", ([[ 59 | local data = select(1, ...) 60 | require'fzf-lua'.config.%s = data 61 | ]]):format(s), { data }) 62 | vim.fn.chanclose(chan_id) 63 | end) 64 | if not ok then 65 | io.stderr:write(("Error setting remote config section '%s': %s\n") 66 | :format(s, errmsg)) 67 | end 68 | return ok 69 | elseif config then 70 | local keys = utils.strsplit(s, '.') 71 | local iter = config 72 | for i=1,#keys do 73 | iter = iter[keys[i]] 74 | if not iter then break end 75 | if i == #keys-1 then 76 | iter[keys[i+1]] = data 77 | return iter 78 | end 79 | end 80 | end 81 | end 82 | 83 | -- Setup the terminal colors codes for nvim-web-devicons colors 84 | local setup_devicon_term_hls = function() 85 | local function hex(hexstr) 86 | local r,g,b = hexstr:match('.(..)(..)(..)') 87 | r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16) 88 | return r, g, b 89 | end 90 | 91 | for _, info in pairs(M._devicons.get_icons()) do 92 | local r, g, b = hex(info.color) 93 | utils.add_ansi_code('DevIcon' .. info.name, string.format('[38;2;%s;%s;%sm', r, g, b)) 94 | end 95 | end 96 | 97 | local function load_devicons() 98 | if config and config._has_devicons then 99 | -- file was called from the primary instance 100 | -- acquire nvim-web-devicons from config 101 | M._devicons = config._devicons 102 | elseif M._devicons_path and vim.loop.fs_stat(M._devicons_path) then 103 | -- file was called from a headless instance 104 | -- load nvim-web-devicons manually 105 | -- add nvim-web-devicons path to `package.path` 106 | -- so `require("nvim-web-devicons")` can find it 107 | package.path = (";%s/?.lua;"):format(vim.fn.fnamemodify(M._devicons_path, ':h')) 108 | .. package.path 109 | M._devicons = require("nvim-web-devicons") 110 | -- WE NO LONGER USE THIS, LEFT FOR DOCUMENTATION 111 | -- loading with 'require' is needed, 'loadfile' 112 | -- cannot load a custom setup function as it's 113 | -- considered a separate instance and the inner 114 | -- 'require' in the setup fill will create an 115 | -- additional 'nvim-web-devicons' instance 116 | --[[ local file = loadfile(M._devicons_path) 117 | M._devicons = file and file() ]] 118 | -- did caller specify a custom setup function? 119 | -- must be called before the next step as `setup` 120 | -- is ignored when called the second time 121 | M._devicons_setup = M._devicons_setup and vim.fn.expand(M._devicons_setup) 122 | if M._devicons and M._devicons_setup and vim.loop.fs_stat(M._devicons_setup) then 123 | local file = loadfile(M._devicons_setup) 124 | if file then file() end 125 | end 126 | end 127 | if M._devicons and M._devicons.setup and not M._devicons.has_loaded() then 128 | -- if the caller has devicons lazy loaded 129 | -- running without calling setup will generate an error: 130 | -- nvim-web-devicons.lua:972: E5560: 131 | -- nvim_command must not be called in a lua loop callback 132 | M._devicons.setup() 133 | end 134 | if M._devicons and M._devicons.has_loaded() then 135 | -- Setup devicon terminal ansi color codes 136 | setup_devicon_term_hls() 137 | end 138 | end 139 | 140 | -- Load remote config and devicons 141 | pcall(load_devicons) 142 | 143 | if not config then 144 | local _config = { globals = { git = {}, files = {} } } 145 | _config.globals.git.icons = load_config_section('globals.git.icons', 'table') or {} 146 | _config.globals.file_icon_colors = load_config_section('globals.file_icon_colors', 'table') or {} 147 | _config.globals.file_icon_padding = load_config_section('globals.file_icon_padding', 'string') 148 | _config.globals.files.git_status_cmd = load_config_section('globals.files.git_status_cmd', 'table') 149 | 150 | _config.globals.nbsp = load_config_section('globals.nbsp', 'string') 151 | if _config.globals.nbsp then utils.nbsp = _config.globals.nbsp end 152 | 153 | config = _config 154 | end 155 | 156 | M.get_devicon = function(file, ext) 157 | local icon, hl 158 | if M._devicons then 159 | icon, hl = M._devicons.get_icon(file, ext:lower(), {default = true}) 160 | else 161 | icon, hl = '', 'dark_grey' 162 | end 163 | 164 | -- allow user override of the color 165 | local override = config.globals.file_icon_colors 166 | and config.globals.file_icon_colors[ext] 167 | if override then 168 | hl = override 169 | end 170 | 171 | if config.globals.file_icon_padding and 172 | #config.globals.file_icon_padding>0 then 173 | icon = icon .. config.globals.file_icon_padding 174 | end 175 | 176 | return icon, hl 177 | end 178 | 179 | M.get_diff_files = function(opts) 180 | local diff_files = {} 181 | local cmd = opts.git_status_cmd or config.globals.files.git_status_cmd 182 | if not cmd then return {} end 183 | local ok, status, err = pcall(utils.io_systemlist, path.git_cwd(cmd, opts.cwd)) 184 | if ok and err == 0 then 185 | for i = 1, #status do 186 | local icon = status[i]:match("[MUDARC?]+") 187 | local file = status[i]:match("[^ ]*$") 188 | if icon and file then 189 | diff_files[file] = icon 190 | end 191 | end 192 | end 193 | 194 | return diff_files 195 | end 196 | 197 | M.preprocess = function(opts) 198 | if opts.cwd_only and not opts.cwd then 199 | opts.cwd = vim.loop.cwd() 200 | end 201 | 202 | if opts.git_icons then 203 | opts.diff_files = M.get_diff_files(opts) 204 | end 205 | 206 | local argv = function(i, debug) 207 | -- argv1 is actually the 7th argument if we count 208 | -- arguments already supplied by 'wrap_spawn_stdio' 209 | -- if no index was supplied use the last argument 210 | local idx = tonumber(i) and tonumber(i)+6 or #vim.v.argv 211 | if debug then 212 | io.stdout:write(("[DEBUG]: argv(%d) = %s\n") 213 | :format(idx, vim.fn.shellescape(vim.v.argv[idx]))) 214 | end 215 | return vim.v.argv[idx] 216 | end 217 | 218 | -- live_grep replace pattern with last argument 219 | local argvz = "{argvz}" 220 | 221 | -- save our last search argument for resume 222 | if opts.argv_expr and opts.cmd:match(argvz) then 223 | local query = argv(nil, opts.debug) 224 | set_config_section('globals.grep._last_search', 225 | { query = query, no_esc = true }) 226 | set_config_section('__resume_data.last_query', query) 227 | end 228 | 229 | -- did the caller request rg with glob support? 230 | -- mannipulation needs to be done before the argv hack 231 | if opts.rg_glob then 232 | local query = argv() 233 | if query and query:find(opts.glob_separator) then 234 | local glob_args = "" 235 | local search_query, glob_str = query:match("(.*)"..opts.glob_separator.."(.*)") 236 | for _, s in ipairs(utils.strsplit(glob_str, "%s")) do 237 | glob_args = glob_args .. ("%s %s ") 238 | :format(opts.glob_flag, vim.fn.shellescape(s)) 239 | end 240 | -- gsub doesn't like single % on rhs 241 | search_query = search_query:gsub("%%", "%%%%") 242 | -- reset argvz so it doesn't get replaced again below 243 | opts.cmd = opts.cmd:gsub(argvz, 244 | glob_args .. vim.fn.shellescape(search_query)) 245 | end 246 | end 247 | 248 | -- nifty hack to avoid having to double escape quotations 249 | -- see my comment inside 'live_grep' initial_command code 250 | if opts.argv_expr then 251 | opts.cmd = opts.cmd:gsub("{argv.*}", 252 | function(x) 253 | local idx = x:match("{argv(.*)}") 254 | return vim.fn.shellescape(argv(idx)) 255 | end) 256 | end 257 | 258 | return opts 259 | end 260 | 261 | M.file = function(opts, x) 262 | local ret = {} 263 | local icon, hl 264 | local file = utils.strip_ansi_coloring(string.match(x, '[^:]*')) 265 | -- TODO: this can cause issues with files/grep/live_grep 266 | -- process_lines gsub will replace the entry with nil 267 | -- **low priority as we never use 'cwd_only' with files/grep 268 | if opts.cwd_only and path.starts_with_separator(file) then 269 | local cwd = opts.cwd or vim.loop.cwd() 270 | if not path.is_relative(file, cwd) then 271 | return nil 272 | end 273 | end 274 | -- fd v8.3 requires adding '--strip-cwd-prefix' to remove 275 | -- the './' prefix, will not work with '--color=always' 276 | -- https://github.com/sharkdp/fd/blob/master/CHANGELOG.md 277 | if not (opts.strip_cwd_prefix == false) and path.starts_with_cwd(x) then 278 | x = path.strip_cwd_prefix(x) 279 | -- this is required to fix git icons not showing 280 | -- since `git status -s` does not prepend './' 281 | -- we can assume no ANSI coloring is present 282 | -- since 'path.starts_with_cwd == true' 283 | file = x 284 | end 285 | if opts.cwd and #opts.cwd > 0 then 286 | -- TODO: does this work if there are ANSI escape codes in x? 287 | x = path.relative(x, opts.cwd) 288 | end 289 | if opts.file_icons then 290 | local filename = path.tail(file) 291 | local ext = path.extension(filename) 292 | icon, hl = M.get_devicon(filename, ext) 293 | if opts.color_icons then 294 | -- extra workaround for issue #119 (or similars) 295 | -- use default if we can't find the highlight ansi 296 | local fn = utils.ansi_codes[hl] or utils.ansi_codes['dark_grey'] 297 | icon = fn(icon) 298 | end 299 | ret[#ret+1] = icon 300 | ret[#ret+1] = utils.nbsp 301 | end 302 | if opts.git_icons then 303 | local indicators = opts.diff_files and opts.diff_files[file] or utils.nbsp 304 | for i=1,#indicators do 305 | icon = indicators:sub(i,i) 306 | local git_icon = config.globals.git.icons[icon] 307 | if git_icon then 308 | icon = git_icon.icon 309 | if opts.color_icons then 310 | icon = utils.ansi_codes[git_icon.color or "dark_grey"](icon) 311 | end 312 | end 313 | ret[#ret+1] = icon 314 | end 315 | ret[#ret+1] = utils.nbsp 316 | end 317 | ret[#ret+1] = x 318 | return table.concat(ret) 319 | end 320 | 321 | M.tag = function(opts, x) 322 | local name, file, text = x:match("([^\t]+)\t([^\t]+)\t(.*)") 323 | if not file or not name or not text then return x end 324 | text = text:match('(.*);"') or text -- remove ctag comments 325 | local line, tag = text:gsub("\\/", "/"):match("(%d-);?(/.*/)") 326 | line = line and #line>0 and tonumber(line) 327 | return ("%s%s: %s %s"):format( 328 | M.file(opts, file), 329 | not line and "" or ":"..utils.ansi_codes.green(tostring(line)), 330 | utils.ansi_codes.magenta(name), 331 | utils.ansi_codes.green(tag)) 332 | , line 333 | end 334 | 335 | return M 336 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/nvim.lua: -------------------------------------------------------------------------------- 1 | local core = require "fzf-lua.core" 2 | local path = require "fzf-lua.path" 3 | local utils = require "fzf-lua.utils" 4 | local shell = require "fzf-lua.shell" 5 | local config = require "fzf-lua.config" 6 | local actions = require "fzf-lua.actions" 7 | 8 | local M = {} 9 | 10 | M.commands = function(opts) 11 | 12 | opts = config.normalize_opts(opts, config.globals.nvim.commands) 13 | if not opts then return end 14 | 15 | local commands = vim.api.nvim_get_commands {} 16 | 17 | local prev_act = shell.action(function (args) 18 | local cmd = args[1] 19 | if commands[cmd] then 20 | cmd = vim.inspect(commands[cmd]) 21 | end 22 | return cmd 23 | end) 24 | 25 | local entries = {} 26 | for k, _ in pairs(commands) do 27 | table.insert(entries, utils.ansi_codes.magenta(k)) 28 | end 29 | 30 | table.sort(entries, function(a, b) return a", "", "execute") 78 | history(opts, "cmd") 79 | end 80 | 81 | M.search_history = function(opts) 82 | opts = config.normalize_opts(opts, config.globals.nvim.search_history) 83 | if not opts then return end 84 | opts.fzf_opts['--header'] = arg_header("", "", "search") 85 | history(opts, "search") 86 | end 87 | 88 | M.changes = function(opts) 89 | opts = opts or {} 90 | opts.cmd = "changes" 91 | opts.prompt = opts.prompt or "Changes> " 92 | return M.jumps(opts) 93 | end 94 | 95 | M.jumps = function(opts) 96 | opts = config.normalize_opts(opts, config.globals.nvim.jumps) 97 | if not opts then return end 98 | 99 | local jumps = vim.fn.execute(opts.cmd) 100 | jumps = vim.split(jumps, "\n") 101 | 102 | local entries = {} 103 | for i = #jumps-1, 3, -1 do 104 | local jump, line, col, text = jumps[i]:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)") 105 | table.insert(entries, string.format("%-15s %-15s %-15s %s", 106 | utils.ansi_codes.yellow(jump), 107 | utils.ansi_codes.blue(line), 108 | utils.ansi_codes.green(col), 109 | text)) 110 | end 111 | 112 | opts.fzf_opts['--no-multi'] = '' 113 | 114 | core.fzf_wrap(opts, entries, function(selected) 115 | 116 | if not selected then return end 117 | actions.act(opts.actions, selected, opts) 118 | 119 | end)() 120 | end 121 | 122 | M.tagstack = function(opts) 123 | opts = config.normalize_opts(opts, config.globals.nvim.tagstack) 124 | if not opts then return end 125 | 126 | local tagstack = vim.fn.gettagstack().items 127 | 128 | local tags = {} 129 | for i = #tagstack, 1, -1 do 130 | local tag = tagstack[i] 131 | tag.bufnr = tag.from[1] 132 | if vim.api.nvim_buf_is_valid(tag.bufnr) then 133 | tags[#tags + 1] = tag 134 | tag.filename = vim.fn.bufname(tag.bufnr) 135 | tag.lnum = tag.from[2] 136 | tag.col = tag.from[3] 137 | 138 | tag.text = vim.api.nvim_buf_get_lines(tag.bufnr, tag.lnum - 1, tag.lnum, false)[1] or "" 139 | end 140 | end 141 | 142 | if vim.tbl_isempty(tags) then 143 | utils.info("No tagstack available") 144 | return 145 | end 146 | 147 | local entries = {} 148 | for i, tag in ipairs(tags) do 149 | local bufname = tag.filename 150 | local buficon, hl 151 | if opts.file_icons then 152 | local filename = path.tail(bufname) 153 | local extension = path.extension(filename) 154 | buficon, hl = core.get_devicon(filename, extension) 155 | if opts.color_icons then 156 | buficon = utils.ansi_codes[hl](buficon) 157 | end 158 | end 159 | -- table.insert(entries, ("%s)%s[%s]%s%s%s%s:%s:%s: %s %s"):format( 160 | table.insert(entries, ("%s)%s%s%s%s:%s:%s: %s %s"):format( 161 | utils.ansi_codes.yellow(tostring(i)), 162 | utils.nbsp, 163 | -- utils.ansi_codes.yellow(tostring(tag.bufnr)), 164 | -- utils.nbsp, 165 | buficon or '', 166 | buficon and utils.nbsp or '', 167 | utils.ansi_codes.magenta(#bufname>0 and bufname or "[No Name]"), 168 | utils.ansi_codes.green(tostring(tag.lnum)), 169 | tag.col, 170 | utils.ansi_codes.red("["..tag.tagname.."]"), 171 | tag.text)) 172 | end 173 | 174 | opts.fzf_opts['--no-multi'] = '' 175 | 176 | core.fzf_wrap(opts, entries, function(selected) 177 | 178 | if not selected then return end 179 | actions.act(opts.actions, selected, opts) 180 | 181 | end)() 182 | end 183 | 184 | 185 | M.marks = function(opts) 186 | opts = config.normalize_opts(opts, config.globals.nvim.marks) 187 | if not opts then return end 188 | 189 | local marks = vim.fn.execute("marks") 190 | marks = vim.split(marks, "\n") 191 | 192 | --[[ local prev_act = shell.action(function (args, fzf_lines, _) 193 | local mark = args[1]:match("[^ ]+") 194 | local bufnr, lnum, _, _ = unpack(vim.fn.getpos("'"..mark)) 195 | if vim.api.nvim_buf_is_loaded(bufnr) then 196 | return vim.api.nvim_buf_get_lines(bufnr, lnum, fzf_lines+lnum, false) 197 | else 198 | local name = vim.fn.expand(args[1]:match(".* (.*)")) 199 | if vim.fn.filereadable(name) ~= 0 then 200 | return vim.fn.readfile(name, "", fzf_lines) 201 | end 202 | return "UNLOADED: " .. name 203 | end 204 | end) ]] 205 | 206 | local entries = {} 207 | for i = #marks, 3, -1 do 208 | local mark, line, col, text = marks[i]:match("(.)%s+(%d+)%s+(%d+)%s+(.*)") 209 | table.insert(entries, string.format("%-15s %-15s %-15s %s", 210 | utils.ansi_codes.yellow(mark), 211 | utils.ansi_codes.blue(line), 212 | utils.ansi_codes.green(col), 213 | text)) 214 | end 215 | 216 | table.sort(entries, function(a, b) return a 248 | ["\27"] = "^[", -- 249 | ["\18"] = "^R", -- 250 | } 251 | for k, v in pairs(gsub_map) do 252 | reg = reg:gsub(k, utils.ansi_codes.magenta(v)) 253 | end 254 | return not nl and reg or 255 | reg:gsub("\n", utils.ansi_codes.magenta("\\n")) 256 | end 257 | 258 | local prev_act = shell.action(function (args) 259 | local r = args[1]:match("%[(.*)%] ") 260 | local _, contents = pcall(vim.fn.getreg, r) 261 | return contents and register_escape_special(contents) or args[1] 262 | end) 263 | 264 | local entries = {} 265 | for _, r in ipairs(registers) do 266 | -- pcall as this could fail with: 267 | -- E5108: Error executing lua Vim:clipboard: 268 | -- provider returned invalid data 269 | local _, contents = pcall(vim.fn.getreg, r) 270 | contents = register_escape_special(contents, true) 271 | if (contents and #contents > 0) or not opts.ignore_empty then 272 | table.insert(entries, string.format("[%s] %s", 273 | utils.ansi_codes.yellow(r), contents)) 274 | end 275 | end 276 | 277 | opts.fzf_opts['--no-multi'] = '' 278 | opts.fzf_opts['--preview'] = prev_act 279 | 280 | core.fzf_wrap(opts, entries, function(selected) 281 | 282 | if not selected then return end 283 | actions.act(opts.actions, selected) 284 | 285 | end)() 286 | end 287 | 288 | M.keymaps = function(opts) 289 | 290 | opts = config.normalize_opts(opts, config.globals.nvim.keymaps) 291 | if not opts then return end 292 | 293 | local modes = { "n", "i", "c" } 294 | local keymaps = {} 295 | 296 | local add_keymap = function(keymap) 297 | -- hijack fields 298 | keymap.str = string.format("[%s:%s:%s]", 299 | utils.ansi_codes.yellow(tostring(keymap.buffer)), 300 | utils.ansi_codes.green(keymap.mode), 301 | utils.ansi_codes.magenta(keymap.lhs:gsub("%s", ""))) 302 | local k = string.format("[%s:%s:%s]", 303 | keymap.buffer, keymap.mode, keymap.lhs) 304 | keymaps[k] = keymap 305 | end 306 | 307 | for _, mode in pairs(modes) do 308 | local global = vim.api.nvim_get_keymap(mode) 309 | for _, keymap in pairs(global) do 310 | add_keymap(keymap) 311 | end 312 | local buf_local = vim.api.nvim_buf_get_keymap(0, mode) 313 | for _, keymap in pairs(buf_local) do 314 | add_keymap(keymap) 315 | end 316 | end 317 | 318 | local prev_act = shell.action(function (args) 319 | local k = args[1]:match("(%[.*%]) ") 320 | local v = keymaps[k] 321 | if v then 322 | -- clear hijacked field 323 | v.str = nil 324 | k = vim.inspect(v) 325 | end 326 | return k 327 | end) 328 | 329 | local entries = {} 330 | for _, v in pairs(keymaps) do 331 | table.insert(entries, string.format("%-50s %s", 332 | v.str, v.rhs)) 333 | end 334 | 335 | opts.fzf_opts['--no-multi'] = '' 336 | opts.fzf_opts['--preview'] = prev_act 337 | 338 | core.fzf_wrap(opts, entries, function(selected) 339 | 340 | if not selected then return end 341 | actions.act(opts.actions, selected) 342 | 343 | end)() 344 | end 345 | 346 | M.spell_suggest = function(opts) 347 | 348 | -- if not vim.wo.spell then return false end 349 | opts = config.normalize_opts(opts, config.globals.nvim.spell_suggest) 350 | if not opts then return end 351 | 352 | local cursor_word = vim.fn.expand "" 353 | local entries = vim.fn.spellsuggest(cursor_word) 354 | 355 | if vim.tbl_isempty(entries) then return end 356 | 357 | opts.fzf_opts['--no-multi'] = '' 358 | opts.fzf_opts['--preview-window'] = 'hidden:right:0' 359 | 360 | core.fzf_wrap(opts, entries, function(selected) 361 | 362 | if not selected then return end 363 | actions.act(opts.actions, selected) 364 | 365 | end)() 366 | 367 | end 368 | 369 | M.filetypes = function(opts) 370 | 371 | opts = config.normalize_opts(opts, config.globals.nvim.filetypes) 372 | if not opts then return end 373 | 374 | local entries = vim.fn.getcompletion('', 'filetype') 375 | if vim.tbl_isempty(entries) then return end 376 | 377 | opts.fzf_opts['--no-multi'] = '' 378 | opts.fzf_opts['--preview-window'] = 'hidden:right:0' 379 | 380 | core.fzf_wrap(opts, entries, function(selected) 381 | 382 | if not selected then return end 383 | actions.act(opts.actions, selected) 384 | 385 | end)() 386 | 387 | end 388 | 389 | M.packadd = function(opts) 390 | 391 | opts = config.normalize_opts(opts, config.globals.nvim.packadd) 392 | if not opts then return end 393 | 394 | local entries = vim.fn.getcompletion('', 'packadd') 395 | 396 | if vim.tbl_isempty(entries) then return end 397 | 398 | opts.fzf_opts['--no-multi'] = '' 399 | opts.fzf_opts['--preview-window'] = 'hidden:right:0' 400 | 401 | core.fzf_wrap(opts, entries, function(selected) 402 | 403 | if not selected then return end 404 | actions.act(opts.actions, selected) 405 | 406 | end)() 407 | 408 | end 409 | 410 | return M 411 | -------------------------------------------------------------------------------- /lua/fzf-lua/providers/grep.lua: -------------------------------------------------------------------------------- 1 | local path = require "fzf-lua.path" 2 | local core = require "fzf-lua.core" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local libuv = require "fzf-lua.libuv" 6 | 7 | local function get_last_search() 8 | local last_search = config.globals.grep._last_search or {} 9 | return last_search.query, last_search.no_esc 10 | end 11 | 12 | local function set_last_search(query, no_esc) 13 | config.globals.grep._last_search = { 14 | query = query, 15 | no_esc = no_esc 16 | } 17 | if config.__resume_data then 18 | config.__resume_data.last_query = query 19 | end 20 | end 21 | 22 | local M = {} 23 | 24 | local get_grep_cmd = function(opts, search_query, no_esc) 25 | if opts.cmd_fn and type(opts.cmd_fn) == 'function' then 26 | return opts.cmd_fn(opts, search_query, no_esc) 27 | end 28 | if opts.raw_cmd and #opts.raw_cmd>0 then 29 | return opts.raw_cmd 30 | end 31 | local command = nil 32 | if opts.cmd and #opts.cmd > 0 then 33 | command = opts.cmd 34 | elseif vim.fn.executable("rg") == 1 then 35 | command = string.format("rg %s", opts.rg_opts) 36 | else 37 | command = string.format("grep %s", opts.grep_opts) 38 | end 39 | 40 | -- filename takes precedence over directory 41 | -- filespec takes precedence over all and doesn't shellescape 42 | -- this is so user can send a file populating command instead 43 | local search_path = '' 44 | if opts.filespec and #opts.filespec>0 then 45 | search_path = opts.filespec 46 | elseif opts.filename and #opts.filename>0 then 47 | search_path = vim.fn.shellescape(opts.filename) 48 | end 49 | 50 | search_query = search_query or '' 51 | if not (no_esc or opts.no_esc) then 52 | search_query = utils.rg_escape(search_query) 53 | end 54 | 55 | -- remove column numbers when search term is empty 56 | if not opts.no_column_hide and #search_query==0 then 57 | command = command:gsub("%s%-%-column", "") 58 | end 59 | 60 | -- do not escape at all 61 | if not (no_esc == 2 or opts.no_esc == 2) then 62 | -- we need to use our own version of 'shellescape' 63 | -- that doesn't escape '\' on fish shell (#340) 64 | search_query = libuv.shellescape(search_query) 65 | end 66 | 67 | return string.format('%s %s %s', command, search_query, search_path) 68 | end 69 | 70 | M.grep = function(opts) 71 | 72 | opts = config.normalize_opts(opts, config.globals.grep) 73 | if not opts then return end 74 | 75 | local no_esc = false 76 | if opts.continue_last_search or opts.repeat_last_search then 77 | opts.search, no_esc = get_last_search() 78 | end 79 | 80 | -- if user did not provide a search term 81 | -- provide an input prompt 82 | if not opts.search then 83 | opts.search = vim.fn.input(opts.input_prompt) or '' 84 | end 85 | 86 | --[[ if not opts.search or #opts.search == 0 then 87 | utils.info("Please provide a valid search string") 88 | return 89 | end ]] 90 | 91 | -- search query in header line 92 | opts = core.set_header(opts) 93 | 94 | -- save the search query so the use can 95 | -- call the same search again 96 | set_last_search(opts.search, no_esc or opts.no_esc) 97 | 98 | opts.cmd = get_grep_cmd(opts, opts.search, no_esc) 99 | local contents = core.mt_cmd_wrapper(opts) 100 | -- by redirecting the error stream to stdout 101 | -- we make sure a clear error message is displayed 102 | -- when the user enters bad regex expressions 103 | if type(contents) == 'string' then 104 | contents = contents .. " 2>&1" 105 | end 106 | 107 | opts = core.set_fzf_field_index(opts) 108 | core.fzf_files(opts, contents) 109 | opts.search = nil 110 | end 111 | 112 | -- single threaded version 113 | M.live_grep_st = function(opts) 114 | 115 | opts = config.normalize_opts(opts, config.globals.grep) 116 | if not opts then return end 117 | 118 | assert(not opts.multiprocess) 119 | 120 | local no_esc = false 121 | if opts.continue_last_search or opts.repeat_last_search then 122 | opts.search, no_esc = get_last_search() 123 | end 124 | 125 | opts.query = opts.search or '' 126 | if opts.search and #opts.search>0 then 127 | -- save the search query so the use can 128 | -- call the same search again 129 | set_last_search(opts.search, true) 130 | -- escape unless the user requested not to 131 | if not (no_esc or opts.no_esc) then 132 | opts.query = utils.rg_escape(opts.search) 133 | end 134 | end 135 | 136 | -- search query in header line 137 | opts = core.set_header(opts, 2) 138 | 139 | opts._reload_command = function(query) 140 | if query and not (opts.save_last_search == false) then 141 | set_last_search(query, true) 142 | end 143 | -- can be nill when called as fzf initial command 144 | query = query or '' 145 | -- TODO: need to empty filespec 146 | -- fix this collision, rename to _filespec 147 | opts.no_esc = nil 148 | opts.filespec = nil 149 | return get_grep_cmd(opts, query, true) 150 | end 151 | 152 | if opts.requires_processing or opts.git_icons or opts.file_icons then 153 | opts._fn_transform = opts._fn_transform 154 | or function(x) 155 | return core.make_entry_file(opts, x) 156 | end 157 | end 158 | 159 | -- disable global resume 160 | -- conflicts with 'change:reload' event 161 | opts.global_resume_query = false 162 | opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() 163 | opts = core.set_fzf_field_index(opts) 164 | opts = core.set_fzf_interactive_cmd(opts) 165 | core.fzf_files(opts) 166 | end 167 | 168 | 169 | -- multi threaded (multi-process actually) version 170 | M.live_grep_mt = function(opts) 171 | 172 | opts = config.normalize_opts(opts, config.globals.grep) 173 | if not opts then return end 174 | 175 | assert(opts.multiprocess) 176 | 177 | local no_esc = false 178 | if opts.continue_last_search or opts.repeat_last_search then 179 | opts.search, no_esc = get_last_search() 180 | end 181 | 182 | local query = opts.search or '' 183 | if opts.search and #opts.search>0 then 184 | -- save the search query so the use can 185 | -- call the same search again 186 | set_last_search(opts.search, no_esc or opts.no_esc) 187 | -- escape unless the user requested not to 188 | if not (no_esc or opts.no_esc) then 189 | query = utils.rg_escape(opts.search) 190 | end 191 | end 192 | 193 | -- search query in header line 194 | opts = core.set_header(opts, 2) 195 | 196 | -- signal to preprocess we are looking to replace {argvz} 197 | opts.argv_expr = true 198 | 199 | -- fzf already adds single quotes around the placeholder when expanding 200 | -- for skim we surround it with double quotes or single quote searches fail 201 | local placeholder = utils._if(opts._is_skim, '"{}"', '{q}') 202 | opts.cmd = get_grep_cmd(opts , placeholder, 2) 203 | local initial_command = core.mt_cmd_wrapper(opts) 204 | if initial_command ~= opts.cmd then 205 | -- this means mt_cmd_wrapper wrapped the command 206 | -- since now the `rg` command is wrapped inside 207 | -- the shell escaped '--headless .. --cmd' we won't 208 | -- be able to search single quotes as it will break 209 | -- the escape sequence so we use a nifty trick 210 | -- * replace the placeholder with {argv1} 211 | -- * re-add the placeholder at the end of the command 212 | -- * preprocess then relaces it with vim.fn.argv(1) 213 | -- NOTE: since we cannot guarantee the positional index 214 | -- of arguments (#291) we use the last argument instead 215 | initial_command = initial_command:gsub(placeholder, "{argvz}") 216 | .. " " .. placeholder 217 | end 218 | -- by redirecting the error stream to stdout 219 | -- we make sure a clear error message is displayed 220 | -- when the user enters bad regex expressions 221 | initial_command = initial_command .. " 2>&1" 222 | local reload_command = initial_command 223 | if not opts.exec_empty_query then 224 | reload_command = ('[ -z %s ] || %s'):format(placeholder, reload_command) 225 | end 226 | if opts._is_skim then 227 | -- skim interactive mode does not need a piped command 228 | opts.fzf_fn = nil 229 | opts.fzf_opts['--prompt'] = '*' .. opts.prompt 230 | opts.fzf_opts['--cmd-prompt'] = vim.fn.shellescape(opts.prompt) 231 | opts.prompt = nil 232 | -- since we surrounded the skim placeholder with quotes 233 | -- we need to escape them in the initial query 234 | opts.fzf_opts['--cmd-query'] = libuv.shellescape(utils.sk_escape(query)) 235 | opts._fzf_cli_args = string.format("-i -c %s", 236 | vim.fn.shellescape(reload_command)) 237 | else 238 | opts.fzf_fn = {} 239 | if opts.exec_empty_query or (opts.search and #opts.search > 0) then 240 | opts.fzf_fn = initial_command:gsub(placeholder, 241 | libuv.shellescape(query:gsub("%%", "%%%%"))) 242 | end 243 | opts.fzf_opts['--phony'] = '' 244 | opts.fzf_opts['--query'] = libuv.shellescape(query) 245 | opts._fzf_cli_args = string.format('--bind=%s', 246 | vim.fn.shellescape(("change:reload:%s"):format( 247 | ("%s || true"):format(reload_command)))) 248 | end 249 | 250 | -- disable global resume 251 | -- conflicts with 'change:reload' event 252 | opts.global_resume_query = false 253 | opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() 254 | opts = core.set_fzf_field_index(opts) 255 | core.fzf_files(opts) 256 | opts.search = nil 257 | end 258 | 259 | M.live_grep_glob_st = function(opts) 260 | if not opts then opts = {} end 261 | if vim.fn.executable("rg") ~= 1 then 262 | utils.warn("'--glob|iglob' flags requires 'rg' (https://github.com/BurntSushi/ripgrep)") 263 | return 264 | end 265 | opts.cmd_fn = function(o, query, no_esc) 266 | 267 | local glob_arg, glob_str = "", "" 268 | local search_query = query or "" 269 | if query:find(o.glob_separator) then 270 | search_query, glob_str = query:match("(.*)"..o.glob_separator.."(.*)") 271 | for _, s in ipairs(utils.strsplit(glob_str, "%s")) do 272 | glob_arg = glob_arg .. (" %s %s") 273 | :format(o.glob_flag, vim.fn.shellescape(s)) 274 | end 275 | end 276 | 277 | -- copied over from get_grep_cmd 278 | local search_path = '' 279 | if o.filespec and #o.filespec>0 then 280 | search_path = o.filespec 281 | elseif o.filename and #o.filename>0 then 282 | search_path = vim.fn.shellescape(o.filename) 283 | end 284 | 285 | if not (no_esc or o.no_esc) then 286 | search_query = utils.rg_escape(search_query) 287 | end 288 | 289 | -- do not escape at all 290 | if not (no_esc == 2 or o.no_esc == 2) then 291 | search_query = libuv.shellescape(search_query) 292 | end 293 | 294 | local cmd = ("rg %s %s -- %s %s") 295 | :format(o.rg_opts, glob_arg, search_query, search_path) 296 | return cmd 297 | end 298 | return M.live_grep_st(opts) 299 | end 300 | 301 | M.live_grep_glob_mt = function(opts) 302 | 303 | if vim.fn.executable("rg") ~= 1 then 304 | utils.warn("'--glob|iglob' flags requires 'rg' (https://github.com/BurntSushi/ripgrep)") 305 | return 306 | end 307 | 308 | -- 'rg_glob = true' enables the glob processsing in 309 | -- 'make_entry.preprocess', only supported with multiprocess 310 | opts = opts or {} 311 | opts.rg_glob = true 312 | opts.requires_processing = true 313 | return M.live_grep_mt(opts) 314 | end 315 | 316 | M.live_grep_native = function(opts) 317 | 318 | -- backward compatibility, by setting git|files icons to false 319 | -- we forces mt_cmd_wrapper to pipe the command as is so fzf 320 | -- runs the command directly in the 'change:reload' event 321 | opts = opts or {} 322 | opts.git_icons = false 323 | opts.file_icons = false 324 | opts.__FNCREF__ = utils.__FNCREF__() 325 | return M.live_grep_mt(opts) 326 | end 327 | 328 | M.live_grep = function(opts) 329 | opts = config.normalize_opts(opts, config.globals.grep) 330 | if not opts then return end 331 | 332 | opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() 333 | 334 | if opts.multiprocess then 335 | return M.live_grep_mt(opts) 336 | else 337 | return M.live_grep_st(opts) 338 | end 339 | end 340 | 341 | M.live_grep_glob = function(opts) 342 | opts = config.normalize_opts(opts, config.globals.grep) 343 | if not opts then return end 344 | 345 | opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() 346 | 347 | if opts.multiprocess then 348 | return M.live_grep_glob_mt(opts) 349 | else 350 | return M.live_grep_glob_st(opts) 351 | end 352 | end 353 | 354 | M.live_grep_resume = function(opts) 355 | if not opts then opts = {} end 356 | if not opts.search then 357 | opts.continue_last_search = 358 | (opts.continue_last_search == nil and 359 | opts.repeat_last_search == nil and true) or 360 | (opts.continue_last_search or opts.repeat_last_search) 361 | end 362 | return M.live_grep(opts) 363 | end 364 | 365 | M.grep_last = function(opts) 366 | if not opts then opts = {} end 367 | opts.continue_last_search = true 368 | return M.grep(opts) 369 | end 370 | 371 | M.grep_cword = function(opts) 372 | if not opts then opts = {} end 373 | opts.search = vim.fn.expand("") 374 | return M.grep(opts) 375 | end 376 | 377 | M.grep_cWORD = function(opts) 378 | if not opts then opts = {} end 379 | opts.search = vim.fn.expand("") 380 | return M.grep(opts) 381 | end 382 | 383 | M.grep_visual = function(opts) 384 | if not opts then opts = {} end 385 | opts.search = utils.get_visual_selection() 386 | return M.grep(opts) 387 | end 388 | 389 | M.grep_project = function(opts) 390 | if not opts then opts = {} end 391 | if not opts.search then opts.search = '' end 392 | -- by default, do not include filename in search 393 | if not opts.fzf_opts or opts.fzf_opts["--nth"] == nil then 394 | opts.fzf_opts = opts.fzf_opts or {} 395 | opts.fzf_opts["--nth"] = '2..' 396 | end 397 | return M.grep(opts) 398 | end 399 | 400 | M.grep_curbuf = function(opts) 401 | if not opts then opts = {} end 402 | opts.rg_opts = config.globals.grep.rg_opts .. " --with-filename" 403 | opts.grep_opts = config.globals.grep.grep_opts .. " --with-filename" 404 | if opts.exec_empty_query == nil then 405 | opts.exec_empty_query = true 406 | end 407 | opts.fzf_opts = vim.tbl_extend("keep", 408 | opts.fzf_opts or {}, config.globals.blines.fzf_opts) 409 | opts.filename = vim.api.nvim_buf_get_name(0) 410 | if #opts.filename > 0 and vim.loop.fs_stat(opts.filename) then 411 | opts.filename = path.relative(opts.filename, vim.loop.cwd()) 412 | if opts.lgrep then 413 | return M.live_grep(opts) 414 | else 415 | opts.search = '' 416 | return M.grep(opts) 417 | end 418 | else 419 | utils.info("Rg current buffer requires file on disk") 420 | return 421 | end 422 | end 423 | 424 | M.lgrep_curbuf = function(opts) 425 | if not opts then opts = {} end 426 | opts.lgrep = true 427 | opts.__FNCREF__ = utils.__FNCREF__() 428 | return M.grep_curbuf(opts) 429 | end 430 | 431 | return M 432 | -------------------------------------------------------------------------------- /lua/fzf-lua/libuv.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local M = {} 4 | 5 | -- path to current file 6 | local __FILE__ = debug.getinfo(1, 'S').source:gsub("^@", "") 7 | 8 | -- if loading this file as standalone ('--headless --clean') 9 | -- add the current folder to package.path so we can 'require' 10 | if not vim.g.fzf_lua_directory then 11 | -- prepend this folder first so our modules always get first 12 | -- priority over some unknown random module with the same name 13 | package.path = (";%s/?.lua;"):format(vim.fn.fnamemodify(__FILE__, ':h')) 14 | .. package.path 15 | 16 | -- override require to remove the 'fzf-lua.' part 17 | -- since all files are going to be loaded locally 18 | local _require = require 19 | require = function(s) return _require(s:gsub("^fzf%-lua%.", "")) end 20 | 21 | -- due to 'os.exit' neovim doesn't delete the temporary 22 | -- directory, save it so we can delete prior to exit (#329) 23 | -- NOTE: opted to delete the temp dir at the start due to: 24 | -- (1) spawn_stdio doesn't need a temp directory 25 | -- (2) avoid dangling temp dirs on process kill (i.e. live_grep) 26 | local tmpdir = vim.fn.fnamemodify(vim.fn.tempname(), ':h') 27 | if tmpdir and #tmpdir>0 then 28 | vim.fn.delete(tmpdir, "rf") 29 | -- io.stdout:write("[DEBUG]: "..tmpdir.."\n") 30 | end 31 | end 32 | 33 | -- save to upvalue for performance reasons 34 | local string_byte = string.byte 35 | local string_sub = string.sub 36 | 37 | local function find_last_newline(str) 38 | for i=#str,1,-1 do 39 | if string_byte(str, i) == 10 then 40 | return i 41 | end 42 | end 43 | end 44 | 45 | --[[ local function find_next_newline(str, start_idx) 46 | for i=start_idx or 1,#str do 47 | if string_byte(str, i) == 10 then 48 | return i 49 | end 50 | end 51 | end ]] 52 | 53 | local function process_kill(pid, signal) 54 | if not pid or not tonumber(pid) then return false end 55 | if type(uv.os_getpriority(pid)) == 'number' then 56 | uv.kill(pid, signal or 9) 57 | return true 58 | end 59 | return false 60 | end 61 | 62 | M.process_kill = process_kill 63 | 64 | local function coroutine_callback(fn) 65 | local co = coroutine.running() 66 | local callback = function(...) 67 | if coroutine.status(co) == 'suspended' then 68 | coroutine.resume(co, ...) 69 | else 70 | local pid = unpack({...}) 71 | process_kill(pid) 72 | end 73 | end 74 | fn(callback) 75 | return coroutine.yield() 76 | end 77 | 78 | local function coroutinify(fn) 79 | return function(...) 80 | local args = {...} 81 | return coroutine.wrap(function() 82 | return coroutine_callback(function(cb) 83 | table.insert(args, cb) 84 | fn(unpack(args)) 85 | end) 86 | end)() 87 | end 88 | end 89 | 90 | 91 | M.spawn = function(opts, fn_transform, fn_done) 92 | local output_pipe = uv.new_pipe(false) 93 | local error_pipe = uv.new_pipe(false) 94 | local write_cb_count = 0 95 | local prev_line_content = nil 96 | -- local num_lines = 0 97 | 98 | if opts.fn_transform then fn_transform = opts.fn_transform end 99 | 100 | local finish = function(code, sig, from, pid) 101 | output_pipe:shutdown() 102 | error_pipe:shutdown() 103 | if opts.cb_finish then 104 | opts.cb_finish(code, sig, from, pid) 105 | end 106 | -- coroutinify callback 107 | if fn_done then 108 | fn_done(pid) 109 | end 110 | end 111 | 112 | -- https://github.com/luvit/luv/blob/master/docs.md 113 | -- uv.spawn returns tuple: handle, pid 114 | local handle, pid = uv.spawn(vim.env.SHELL or "sh", { 115 | args = { "-c", opts.cmd }, 116 | stdio = { nil, output_pipe, error_pipe }, 117 | cwd = opts.cwd 118 | }, function(code, signal) 119 | output_pipe:read_stop() 120 | error_pipe:read_stop() 121 | output_pipe:close() 122 | error_pipe :close() 123 | if write_cb_count==0 then 124 | -- only close if all our uv.write 125 | -- calls are completed 126 | finish(code, signal, 1) 127 | end 128 | end) 129 | 130 | -- save current process pid 131 | if opts.cb_pid then opts.cb_pid(pid) end 132 | if opts.pid_cb then opts.pid_cb(pid) end 133 | if opts._pid_cb then opts._pid_cb(pid) end 134 | 135 | local function write_cb(data) 136 | write_cb_count = write_cb_count + 1 137 | opts.cb_write(data, function(err) 138 | write_cb_count = write_cb_count - 1 139 | if err then 140 | -- can fail with premature process kill 141 | -- assert(not err) 142 | finish(130, 0, 2, pid) 143 | elseif write_cb_count == 0 and uv.is_closing(output_pipe) then 144 | -- spawn callback already called and did not close the pipe 145 | -- due to write_cb_count>0, since this is the last call 146 | -- we can close the fzf pipe 147 | finish(0, 0, 3, pid) 148 | end 149 | end) 150 | end 151 | 152 | local function process_lines(data) 153 | -- assert(#data<=66560) -- 65K 154 | write_cb(data:gsub("[^\n]+", 155 | function(x) 156 | return fn_transform(x) 157 | end)) 158 | end 159 | 160 | --[[ local function process_lines(data) 161 | local start_idx = 1 162 | repeat 163 | num_lines = num_lines + 1 164 | local nl_idx = find_next_newline(data, start_idx) 165 | local line = data:sub(start_idx, nl_idx) 166 | if #line > 1024 then 167 | local msg = 168 | ("long line detected, consider adding '--max-columns=512' to ripgrep options:\n %s") 169 | :format(utils.strip_ansi_coloring(line):sub(1,60)) 170 | vim.defer_fn(function() 171 | utils.warn(msg) 172 | end, 0) 173 | line = line:sub(1,512) .. '\n' 174 | end 175 | write_cb(fn_transform(line)) 176 | start_idx = nl_idx + 1 177 | until start_idx >= #data 178 | end --]] 179 | 180 | local read_cb = function(err, data) 181 | 182 | if err then 183 | assert(not err) 184 | finish(130, 0, 4, pid) 185 | end 186 | if not data then 187 | return 188 | end 189 | 190 | if prev_line_content then 191 | if #prev_line_content > 1024 then 192 | -- chunk size is 64K, limit previous line length to 1K 193 | -- max line length is therefor 1K + 64K (leftover + full chunk) 194 | -- without this we can memory fault on extremely long lines (#185) 195 | -- or have UI freezes (#211) 196 | prev_line_content = prev_line_content:sub(1, 1024) 197 | end 198 | data = prev_line_content .. data 199 | prev_line_content = nil 200 | end 201 | 202 | if not fn_transform then 203 | write_cb(data) 204 | elseif string_byte(data, #data) == 10 then 205 | process_lines(data) 206 | else 207 | local nl_index = find_last_newline(data) 208 | if not nl_index then 209 | prev_line_content = data 210 | else 211 | prev_line_content = string_sub(data, nl_index + 1) 212 | local stripped_with_newline = string_sub(data, 1, nl_index) 213 | process_lines(stripped_with_newline) 214 | end 215 | end 216 | 217 | end 218 | 219 | local err_cb = function(err, data) 220 | if err then 221 | finish(130, 0, 9, pid) 222 | end 223 | if not data then 224 | return 225 | end 226 | if opts.cb_err then 227 | opts.cb_err(data) 228 | else 229 | write_cb(data) 230 | end 231 | end 232 | 233 | if not handle then 234 | -- uv.spawn failed, error will be in 'pid' 235 | -- call once to output the error message 236 | -- and second time to signal EOF (data=nil) 237 | err_cb(nil, pid.."\n") 238 | err_cb(pid, nil) 239 | else 240 | output_pipe:read_start(read_cb) 241 | error_pipe:read_start(err_cb) 242 | end 243 | end 244 | 245 | M.async_spawn = coroutinify(M.spawn) 246 | 247 | 248 | M.spawn_nvim_fzf_cmd = function(opts, fn_transform, fn_preprocess) 249 | 250 | assert(not fn_transform or type(fn_transform) == 'function') 251 | 252 | if fn_preprocess and type(fn_preprocess) == 'function' then 253 | -- run the preprocessing fn 254 | fn_preprocess(opts) 255 | end 256 | 257 | return function(_, fzf_cb, _) 258 | 259 | local function on_finish(_, _) 260 | fzf_cb(nil) 261 | end 262 | 263 | local function on_write(data, cb) 264 | -- passthrough the data exactly as received from the pipe 265 | -- using the 2nd 'fzf_cb' arg instructs raw_fzf to not add "\n" 266 | -- 267 | -- below not relevant anymore, will delete comment in future 268 | -- if 'fn_transform' was specified the last char must be EOL 269 | -- otherwise something went terribly wrong 270 | -- without 'fn_transform' EOL isn't guaranteed at the end 271 | -- assert(not fn_transform or string_byte(data, #data) == 10) 272 | fzf_cb(data, cb) 273 | end 274 | 275 | return M.spawn({ 276 | cwd = opts.cwd, 277 | cmd = opts.cmd, 278 | cb_finish = on_finish, 279 | cb_write = on_write, 280 | cb_pid = opts.pid_cb, 281 | }, fn_transform) 282 | end 283 | end 284 | 285 | M.spawn_stdio = function(opts, fn_transform, fn_preprocess) 286 | 287 | local function load_fn(fn_str) 288 | if type(fn_str) ~= 'string' then return end 289 | local fn_loaded = nil 290 | local fn = loadstring(fn_str) or load(fn_str) 291 | if fn then fn_loaded = fn() end 292 | if type(fn_loaded) ~= 'function' then 293 | fn_loaded = nil 294 | end 295 | return fn_loaded 296 | end 297 | 298 | fn_transform = load_fn(fn_transform) 299 | fn_preprocess = load_fn(fn_preprocess) 300 | 301 | -- if opts.argv_expr and opts.debug then 302 | -- io.stdout:write("[DEBUG]: "..opts.cmd.."\n") 303 | -- end 304 | 305 | -- run the preprocessing fn 306 | if fn_preprocess then fn_preprocess(opts) end 307 | 308 | -- for i=0,8 do 309 | -- io.stdout:write(("%d %s\n"):format(i, vim.v.argv[i])) 310 | -- end 311 | 312 | local is_darwin = vim.loop.os_uname().sysname == 'Darwin' 313 | 314 | if opts.debug then 315 | io.stdout:write("[DEBUG]: "..opts.cmd.."\n") 316 | end 317 | 318 | local stderr, stdout = nil, nil 319 | 320 | local function stderr_write(msg) 321 | -- prioritize writing errors to stderr 322 | if stderr then stderr:write(msg) 323 | else io.stderr:write(msg) end 324 | end 325 | 326 | local function exit(exit_code, msg) 327 | if msg then stderr_write(msg) end 328 | os.exit(exit_code) 329 | end 330 | 331 | local function pipe_open(pipename) 332 | if not pipename then return end 333 | local fd = uv.fs_open(pipename, "w", -1) 334 | if type(fd) ~= 'number' then 335 | exit(1, ("error opening '%s': %s\n"):format(pipename, fd)) 336 | end 337 | local pipe = uv.new_pipe(false) 338 | pipe:open(fd) 339 | return pipe 340 | end 341 | 342 | local function pipe_close(pipe) 343 | if pipe and not pipe:is_closing() then 344 | pipe:close() 345 | end 346 | end 347 | 348 | local function pipe_write(pipe, data, cb) 349 | if not pipe or pipe:is_closing() then return end 350 | pipe:write(data, 351 | function(err) 352 | -- if the user cancels the call prematurely with 353 | -- err will be either EPIPE or ECANCELED 354 | -- don't really need to do anything since the 355 | -- processs will be killed anyways with os.exit() 356 | if err then 357 | stderr_write(("pipe:write error: %s\n"):format(err)) 358 | end 359 | if cb then cb(err) end 360 | end) 361 | end 362 | 363 | if opts.stderr then 364 | stderr = pipe_open(opts.stderr) 365 | end 366 | if opts.stdout then 367 | stdout = pipe_open(opts.stdout) 368 | end 369 | 370 | local on_finish = opts.on_finish or 371 | function(code) 372 | pipe_close(stdout) 373 | pipe_close(stderr) 374 | exit(code) 375 | end 376 | 377 | local on_write = opts.on_write or 378 | function(data, cb) 379 | if stdout then 380 | pipe_write(stdout, data, cb) 381 | else 382 | -- on success: rc=true, err=nil 383 | -- on failure: rc=nil, err="Broken pipe" 384 | -- cb with an err ends the process 385 | local rc, err = io.stdout:write(data) 386 | if not rc then 387 | stderr_write(("io.stdout:write error: %s\n"):format(err)) 388 | cb(err or true) 389 | else 390 | cb(nil) 391 | end 392 | end 393 | end 394 | 395 | local on_err = opts.on_err or 396 | function(data) 397 | if stderr then 398 | pipe_write(stderr, data) 399 | else 400 | if is_darwin then 401 | -- for some reason io:stderr causes 402 | -- weird rendering issues on Mac (#316, #287) 403 | io.stdout:write(data) 404 | else 405 | io.stderr:write(data) 406 | end 407 | end 408 | end 409 | 410 | return M.spawn({ 411 | cwd = opts.cwd, 412 | cmd = opts.cmd, 413 | cb_finish = on_finish, 414 | cb_write = on_write, 415 | cb_err = on_err, 416 | }, 417 | fn_transform and function(x) 418 | return fn_transform(opts, x) 419 | end) 420 | end 421 | 422 | -- our own version of vim.fn.shellescape compatibile with fish shells 423 | -- * don't double-escape '\' (#340) 424 | -- * if possible, replace surrounding single quote with double 425 | -- from ':help shellescape': 426 | -- If 'shell' contains "fish" in the tail, the "\" character will 427 | -- be escaped because in fish it is used as an escape character 428 | -- inside single quotes. 429 | -- this function is a better fit for utils but we're 430 | -- trying to avoid having any 'require' in this file 431 | M.shellescape = function(s) 432 | local shell = vim.o.shell 433 | if not shell or not shell:match("fish$") then 434 | return vim.fn.shellescape(s) 435 | else 436 | local ret = nil 437 | vim.o.shell = "sh" 438 | if not s:match([["]]) and not s:match([[\]]) then 439 | -- if the original string does not contain double quotes 440 | -- replace surrounding single quote with double quotes 441 | -- temporarily replace all single quotes with double 442 | -- quotes and restore after the call to shellescape 443 | ret = vim.fn.shellescape(s:gsub([[']], [["]])) 444 | ret = [["]] .. ret:gsub([["]], [[']]):sub(2, #ret-1) .. [["]] 445 | else 446 | ret = vim.fn.shellescape(s) 447 | end 448 | vim.o.shell = shell 449 | return ret 450 | end 451 | end 452 | 453 | M.wrap_spawn_stdio = function(opts, fn_transform, fn_preprocess) 454 | assert(opts and type(opts) == 'string') 455 | assert(not fn_transform or type(fn_transform) == 'string') 456 | local nvim_bin = vim.v.argv[1] 457 | local call_args = opts 458 | for _, fn in ipairs({ fn_transform, fn_preprocess }) do 459 | if type(fn) == 'string' then 460 | call_args = ("%s,[[%s]]"):format(call_args, fn) 461 | end 462 | end 463 | local cmd_str = ("%s -n --headless --clean --cmd %s"):format( 464 | vim.fn.shellescape(nvim_bin), 465 | M.shellescape(("lua loadfile([[%s]])().spawn_stdio(%s)") 466 | :format(__FILE__, call_args))) 467 | return cmd_str 468 | end 469 | 470 | return M 471 | -------------------------------------------------------------------------------- /lua/fzf-lua/actions.lua: -------------------------------------------------------------------------------- 1 | local utils = require "fzf-lua.utils" 2 | local path = require "fzf-lua.path" 3 | 4 | local M = {} 5 | 6 | -- default action map key 7 | local _default_action = "default" 8 | 9 | -- return fzf '--expect=' string from actions keyval tbl 10 | M.expect = function(actions) 11 | if not actions then return nil end 12 | local keys = {} 13 | for k, v in pairs(actions) do 14 | if k ~= _default_action and v ~= false then 15 | table.insert(keys, k) 16 | end 17 | end 18 | if #keys > 0 then 19 | return string.format("--expect=%s", table.concat(keys, ',')) 20 | end 21 | return nil 22 | end 23 | 24 | M.normalize_selected = function(actions, selected) 25 | -- 1. If there are no additional actions but the default 26 | -- the selected table will contain the selected item(s) 27 | -- 2. If multiple actions where defined the first item 28 | -- will contain the action keybind string 29 | -- 30 | -- The below makes separates the keybind from the item(s) 31 | -- and makes sure 'selected' contains only items or {} 32 | -- so it can always be enumerated safely 33 | if not actions or not selected then return end 34 | local action = _default_action 35 | if utils.tbl_length(actions)>1 then 36 | -- keybind should be in item #1 37 | -- default keybind is an empty string 38 | -- so we leave that as "default" 39 | if #selected[1] > 0 then 40 | action = selected[1] 41 | end 42 | -- entries are items #2+ 43 | local entries = {} 44 | for i = 2, #selected do 45 | table.insert(entries, selected[i]) 46 | end 47 | return action, entries 48 | else 49 | return action, selected 50 | end 51 | end 52 | 53 | M.act = function(actions, selected, opts) 54 | if not actions or not selected then return end 55 | local keybind, entries = M.normalize_selected(actions, selected) 56 | local action = actions[keybind] 57 | if type(action) == 'table' then 58 | for _, f in ipairs(action) do 59 | f(entries, opts) 60 | end 61 | elseif type(action) == 'function' then 62 | action(entries, opts) 63 | elseif type(action) == 'string' then 64 | vim.cmd(action) 65 | else 66 | utils.warn(("unsupported action: '%s', type:%s") 67 | :format(action, type(action))) 68 | end 69 | end 70 | 71 | M.resume = function(_, _) 72 | -- must call via vim.cmd or we create 73 | -- circular 'require' 74 | -- TODO: is this really a big deal? 75 | vim.cmd("lua require'fzf-lua'.resume()") 76 | end 77 | 78 | M.vimcmd = function(vimcmd, selected) 79 | for i = 1, #selected do 80 | vim.cmd(vimcmd .. " " .. vim.fn.fnameescape(selected[i])) 81 | end 82 | end 83 | 84 | M.vimcmd_file = function(vimcmd, selected, opts) 85 | local curbuf = vim.api.nvim_buf_get_name(0) 86 | local is_term = utils.is_term_buffer(0) 87 | for i = 1, #selected do 88 | local entry = path.entry_to_file(selected[i], opts.cwd, opts.force_uri) 89 | entry.ctag = opts._ctag and path.entry_to_ctag(selected[i]) 90 | local fullpath = entry.path or entry.uri and entry.uri:match("^%a+://(.*)") 91 | if not path.starts_with_separator(fullpath) then 92 | fullpath = path.join({opts.cwd or vim.loop.cwd(), fullpath}) 93 | end 94 | if vimcmd == 'e' 95 | and curbuf ~= fullpath 96 | and not vim.o.hidden and 97 | utils.buffer_is_dirty(nil, true) then 98 | -- warn the user when trying to switch from a dirty buffer 99 | -- when `:set nohidden` 100 | return 101 | end 102 | -- add current location to jumplist 103 | if not is_term then vim.cmd("normal! m`") end 104 | -- only change buffer if we need to (issue #122) 105 | if vimcmd ~= "e" or curbuf ~= fullpath then 106 | if entry.path then 107 | -- do not run ': ' for uri entries (#341) 108 | vim.cmd(vimcmd .. " " .. vim.fn.fnameescape(entry.path)) 109 | elseif vimcmd ~= 'e' then 110 | -- uri entries only execute new buffers (new|vnew|tabnew) 111 | vim.cmd(vimcmd) 112 | end 113 | end 114 | -- Java LSP entries, 'jdt://...' or LSP locations 115 | if entry.uri then 116 | vim.lsp.util.jump_to_location(entry, "utf-16") 117 | elseif entry.ctag then 118 | vim.api.nvim_win_set_cursor(0, {1, 0}) 119 | vim.fn.search(entry.ctag, "W") 120 | elseif entry.line>1 or entry.col>1 then 121 | -- make sure we have valid column 122 | -- 'nvim-dap' for example sets columns to 0 123 | entry.col = entry.col and entry.col>0 and entry.col or 1 124 | vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1}) 125 | end 126 | if not is_term then vim.cmd("norm! zvzz") end 127 | end 128 | end 129 | 130 | -- file actions 131 | M.file_edit = function(selected, opts) 132 | local vimcmd = "e" 133 | M.vimcmd_file(vimcmd, selected, opts) 134 | end 135 | 136 | M.file_split = function(selected, opts) 137 | local vimcmd = "new" 138 | M.vimcmd_file(vimcmd, selected, opts) 139 | end 140 | 141 | M.file_vsplit = function(selected, opts) 142 | local vimcmd = "vnew" 143 | M.vimcmd_file(vimcmd, selected, opts) 144 | end 145 | 146 | M.file_tabedit = function(selected, opts) 147 | local vimcmd = "tabnew" 148 | M.vimcmd_file(vimcmd, selected, opts) 149 | end 150 | 151 | M.file_open_in_background = function(selected, opts) 152 | local vimcmd = "badd" 153 | M.vimcmd_file(vimcmd, selected, opts) 154 | end 155 | 156 | M.file_sel_to_qf = function(selected, _) 157 | local qf_list = {} 158 | for i = 1, #selected do 159 | local file = path.entry_to_file(selected[i]) 160 | local text = selected[i]:match(":%d+:%d?%d?%d?%d?:?(.*)$") 161 | table.insert(qf_list, { 162 | filename = file.path, 163 | lnum = file.line, 164 | col = file.col, 165 | text = text, 166 | }) 167 | end 168 | vim.fn.setqflist(qf_list) 169 | vim.cmd 'copen' 170 | end 171 | 172 | M.file_edit_or_qf = function(selected, opts) 173 | if #selected>1 then 174 | return M.file_sel_to_qf(selected, opts) 175 | else 176 | return M.file_edit(selected, opts) 177 | end 178 | end 179 | 180 | M.file_switch = function(selected, opts) 181 | local bufnr = nil 182 | local entry = path.entry_to_file(selected[1]) 183 | local fullpath = entry.path 184 | if not path.starts_with_separator(fullpath) then 185 | fullpath = path.join({opts.cwd or vim.loop.cwd(), fullpath}) 186 | end 187 | for _, b in ipairs(vim.api.nvim_list_bufs()) do 188 | local bname = vim.api.nvim_buf_get_name(b) 189 | if bname and bname == fullpath then 190 | bufnr = b 191 | break 192 | end 193 | end 194 | if not bufnr then return false end 195 | local is_term = utils.is_term_buffer(0) 196 | if not is_term then vim.cmd("normal! m`") end 197 | local winid = utils.winid_from_tab_buf(0, bufnr) 198 | if winid then vim.api.nvim_set_current_win(winid) end 199 | if entry.line>1 or entry.col>1 then 200 | vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1}) 201 | end 202 | if not is_term then vim.cmd("norm! zvzz") end 203 | return true 204 | end 205 | 206 | M.file_switch_or_edit = function(...) 207 | M.file_switch(...) 208 | M.file_edit(...) 209 | end 210 | 211 | -- buffer actions 212 | M.vimcmd_buf = function(vimcmd, selected, _) 213 | local curbuf = vim.api.nvim_get_current_buf() 214 | for i = 1, #selected do 215 | local bufnr = string.match(selected[i], "%[(%d+)") 216 | if bufnr then 217 | if vimcmd == 'b' 218 | and curbuf ~= tonumber(bufnr) 219 | and not vim.o.hidden and 220 | utils.buffer_is_dirty(nil, true) then 221 | -- warn the user when trying to switch from a dirty buffer 222 | -- when `:set nohidden` 223 | return 224 | end 225 | if vimcmd ~= "b" or curbuf ~= tonumber(bufnr) then 226 | local cmd = vimcmd .. " " .. bufnr 227 | local ok, res = pcall(vim.cmd, cmd) 228 | if not ok then 229 | utils.warn(("':%s' failed: %s"):format(cmd, res)) 230 | end 231 | end 232 | end 233 | end 234 | end 235 | 236 | M.buf_edit = function(selected, opts) 237 | local vimcmd = "b" 238 | M.vimcmd_buf(vimcmd, selected, opts) 239 | end 240 | 241 | M.buf_split = function(selected, opts) 242 | local vimcmd = "split | b" 243 | M.vimcmd_buf(vimcmd, selected, opts) 244 | end 245 | 246 | M.buf_vsplit = function(selected, opts) 247 | local vimcmd = "vertical split | b" 248 | M.vimcmd_buf(vimcmd, selected, opts) 249 | end 250 | 251 | M.buf_tabedit = function(selected, opts) 252 | local vimcmd = "tab split | b" 253 | M.vimcmd_buf(vimcmd, selected, opts) 254 | end 255 | 256 | M.buf_del = function(selected, opts) 257 | local vimcmd = "bd" 258 | local bufnrs = vim.tbl_filter(function(line) 259 | local b = tonumber(line:match("%[(%d+)")) 260 | return not utils.buffer_is_dirty(b, true) 261 | end, selected) 262 | M.vimcmd_buf(vimcmd, bufnrs, opts) 263 | end 264 | 265 | M.buf_switch = function(selected, _) 266 | local tabnr = selected[1]:match("(%d+)%)") 267 | if tabnr then 268 | vim.cmd("tabn " .. tabnr) 269 | else 270 | tabnr = vim.api.nvim_win_get_tabpage(0) 271 | end 272 | local bufnr = tonumber(string.match(selected[1], "%[(%d+)")) 273 | if bufnr then 274 | local winid = utils.winid_from_tab_buf(tabnr, bufnr) 275 | if winid then vim.api.nvim_set_current_win(winid) end 276 | end 277 | end 278 | 279 | M.buf_switch_or_edit = function(...) 280 | M.buf_switch(...) 281 | M.buf_edit(...) 282 | end 283 | 284 | M.colorscheme = function(selected) 285 | local colorscheme = selected[1] 286 | vim.cmd("colorscheme " .. colorscheme) 287 | end 288 | 289 | M.ensure_insert_mode = function() 290 | -- not sure what is causing this, tested with 291 | -- 'NVIM v0.6.0-dev+575-g2ef9d2a66' 292 | -- vim.cmd("startinsert") doesn't start INSERT mode 293 | -- 'mode' returns { blocking = false, mode = "t" } 294 | -- manually input 'i' seems to workaround this issue 295 | -- **only if fzf term window was succefully opened (#235) 296 | -- this is only required after the 'nt' (normal-terminal) 297 | -- mode was introduced along with the 'ModeChanged' event 298 | -- https://github.com/neovim/neovim/pull/15878 299 | -- https://github.com/neovim/neovim/pull/15840 300 | local has_mode_nt = not vim.tbl_isempty( 301 | vim.fn.getcompletion('ModeChanged', 'event')) 302 | or vim.fn.has('nvim-0.6') == 1 303 | if has_mode_nt then 304 | local mode = vim.api.nvim_get_mode() 305 | local wininfo = vim.fn.getwininfo(vim.api.nvim_get_current_win())[1] 306 | if vim.bo.ft == 'fzf' 307 | and wininfo.terminal == 1 308 | and mode and mode.mode == 't' then 309 | vim.cmd[[noautocmd lua vim.api.nvim_feedkeys('i', 'n', true)]] 310 | end 311 | end 312 | end 313 | 314 | M.run_builtin = function(selected) 315 | local method = selected[1] 316 | vim.cmd(string.format("lua require'fzf-lua'.%s()", method)) 317 | M.ensure_insert_mode() 318 | end 319 | 320 | M.ex_run = function(selected) 321 | local cmd = selected[1] 322 | vim.cmd("stopinsert") 323 | vim.fn.feedkeys(string.format(":%s", cmd), "n") 324 | return cmd 325 | end 326 | 327 | M.ex_run_cr = function(selected) 328 | local cmd = M.ex_run(selected) 329 | utils.feed_keys_termcodes("") 330 | vim.fn.histadd("cmd", cmd) 331 | end 332 | 333 | M.search = function(selected) 334 | local query = selected[1] 335 | vim.cmd("stopinsert") 336 | vim.fn.feedkeys(string.format("/%s", query), "n") 337 | return query 338 | end 339 | 340 | M.search_cr = function(selected) 341 | local query = M.search(selected) 342 | utils.feed_keys_termcodes("") 343 | vim.fn.histadd("search", query) 344 | end 345 | 346 | M.goto_mark = function(selected) 347 | local mark = selected[1] 348 | mark = mark:match("[^ ]+") 349 | vim.cmd("stopinsert") 350 | vim.cmd("normal! '" .. mark) 351 | -- vim.fn.feedkeys(string.format("'%s", mark)) 352 | end 353 | 354 | M.goto_jump = function(selected, opts) 355 | if opts.jump_using_norm then 356 | local jump, _, _, _ = selected[1]:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)") 357 | if tonumber(jump) then 358 | vim.cmd(("normal! %d"):format(jump)) 359 | end 360 | else 361 | local _, lnum, col, filepath = selected[1]:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)") 362 | local ok, res = pcall(vim.fn.expand, filepath) 363 | if not ok then filepath = '' 364 | else filepath = res end 365 | if not filepath or not vim.loop.fs_stat(filepath) then 366 | -- no accessible file 367 | -- jump is in current 368 | filepath = vim.api.nvim_buf_get_name(0) 369 | end 370 | local entry = ("%s:%d:%d:"):format(filepath, tonumber(lnum), tonumber(col)+1) 371 | M.file_edit({ entry }, opts) 372 | end 373 | end 374 | 375 | M.spell_apply = function(selected) 376 | local word = selected[1] 377 | vim.cmd("normal! ciw" .. word) 378 | vim.cmd("stopinsert") 379 | end 380 | 381 | M.set_filetype = function(selected) 382 | vim.api.nvim_buf_set_option(0, 'filetype', selected[1]) 383 | end 384 | 385 | M.packadd = function(selected) 386 | for i = 1, #selected do 387 | vim.cmd("packadd " .. selected[i]) 388 | end 389 | end 390 | 391 | M.help = function(selected) 392 | local vimcmd = "help" 393 | M.vimcmd(vimcmd, selected) 394 | end 395 | 396 | M.help_vert = function(selected) 397 | local vimcmd = "vert help" 398 | M.vimcmd(vimcmd, selected) 399 | end 400 | 401 | M.help_tab = function(selected) 402 | local vimcmd = "tab help" 403 | M.vimcmd(vimcmd, selected) 404 | end 405 | 406 | M.man = function(selected) 407 | local vimcmd = "Man" 408 | M.vimcmd(vimcmd, selected) 409 | end 410 | 411 | M.man_vert = function(selected) 412 | local vimcmd = "vert Man" 413 | M.vimcmd(vimcmd, selected) 414 | end 415 | 416 | M.man_tab = function(selected) 417 | local vimcmd = "tab Man" 418 | M.vimcmd(vimcmd, selected) 419 | end 420 | 421 | 422 | M.git_switch = function(selected, opts) 423 | local cmd = path.git_cwd({"git", "checkout"}, opts.cwd) 424 | local git_ver = utils.git_version() 425 | -- git switch was added with git version 2.23 426 | if git_ver and git_ver >= 2.23 then 427 | cmd = path.git_cwd({"git", "switch"}, opts.cwd) 428 | end 429 | -- remove anything past space 430 | local branch = selected[1]:match("[^ ]+") 431 | -- do nothing for active branch 432 | if branch:find("%*") ~= nil then return end 433 | if branch:find("^remotes/") then 434 | table.insert(cmd, "--detach") 435 | end 436 | table.insert(cmd, branch) 437 | local output = utils.io_systemlist(cmd) 438 | if utils.shell_error() then 439 | utils.err(unpack(output)) 440 | else 441 | utils.info(unpack(output)) 442 | vim.cmd("edit!") 443 | end 444 | end 445 | 446 | M.git_checkout = function(selected, opts) 447 | local cmd_checkout = path.git_cwd({"git", "checkout"}, opts.cwd) 448 | local cmd_cur_commit = path.git_cwd({"git", "rev-parse", "--short HEAD"}, opts.cwd) 449 | local commit_hash = selected[1]:match("[^ ]+") 450 | if vim.fn.input("Checkout commit " .. commit_hash .. "? [y/n] ") == "y" then 451 | local current_commit = utils.io_systemlist(cmd_cur_commit) 452 | if(commit_hash == current_commit) then return end 453 | table.insert(cmd_checkout, commit_hash) 454 | local output = utils.io_systemlist(cmd_checkout) 455 | if utils.shell_error() then 456 | utils.err(unpack(output)) 457 | else 458 | utils.info(unpack(output)) 459 | vim.cmd("edit!") 460 | end 461 | end 462 | end 463 | 464 | local git_exec = function(selected, opts, cmd) 465 | for _, e in ipairs(selected) do 466 | local file = path.relative(path.entry_to_file(e, opts.cwd).path, opts.cwd) 467 | local _cmd = vim.deepcopy(cmd) 468 | table.insert(_cmd, file) 469 | local output = utils.io_systemlist(_cmd) 470 | if utils.shell_error() then 471 | utils.err(unpack(output)) 472 | -- elseif not vim.tbl_isempty(output) then 473 | -- utils.info(unpack(output)) 474 | end 475 | end 476 | end 477 | 478 | M.git_stage = function(selected, opts) 479 | local cmd = path.git_cwd({"git", "add", "--"}, opts.cwd) 480 | git_exec(selected, opts, cmd) 481 | end 482 | 483 | M.git_unstage = function(selected, opts) 484 | local cmd = path.git_cwd({"git", "reset", "--"}, opts.cwd) 485 | git_exec(selected, opts, cmd) 486 | end 487 | 488 | M.git_buf_edit = function(selected, opts) 489 | local cmd = path.git_cwd({"git", "show"}, opts.cwd) 490 | local git_root = path.git_root(opts.cwd, true) 491 | local win = vim.api.nvim_get_current_win() 492 | local buffer_filetype = vim.bo.filetype 493 | local file = path.relative(vim.fn.expand("%:p"), git_root) 494 | local commit_hash = selected[1]:match("[^ ]+") 495 | table.insert(cmd, commit_hash .. ":" .. file) 496 | local git_file_contents = utils.io_systemlist(cmd) 497 | local buf = vim.api.nvim_create_buf(true, true) 498 | local file_name = string.gsub(file,"$","[" .. commit_hash .. "]") 499 | vim.api.nvim_buf_set_lines(buf,0,0,true,git_file_contents) 500 | vim.api.nvim_buf_set_name(buf,file_name) 501 | vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') 502 | vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe') 503 | vim.api.nvim_buf_set_option(buf, 'filetype', buffer_filetype) 504 | vim.api.nvim_buf_set_option(buf, 'modifiable', false) 505 | vim.api.nvim_win_set_buf(win, buf) 506 | end 507 | 508 | M.git_buf_tabedit = function(selected, opts) 509 | vim.cmd('tab split') 510 | M.git_buf_edit(selected, opts) 511 | end 512 | 513 | M.git_buf_split = function(selected, opts) 514 | vim.cmd('split') 515 | M.git_buf_edit(selected, opts) 516 | end 517 | 518 | M.git_buf_vsplit = function(selected, opts) 519 | vim.cmd('vsplit') 520 | M.git_buf_edit(selected, opts) 521 | end 522 | 523 | M.arg_add = function(selected, opts) 524 | local vimcmd = "argadd" 525 | M.vimcmd_file(vimcmd, selected, opts) 526 | end 527 | 528 | M.arg_del = function(selected, opts) 529 | local vimcmd = "argdel" 530 | M.vimcmd_file(vimcmd, selected, opts) 531 | end 532 | 533 | return M 534 | -------------------------------------------------------------------------------- /lua/fzf-lua/utils.lua: -------------------------------------------------------------------------------- 1 | -- help to inspect results, e.g.: 2 | -- ':lua _G.dump(vim.fn.getwininfo())' 3 | -- use ':messages' to see the dump 4 | function _G.dump(...) 5 | local objects = vim.tbl_map(vim.inspect, { ... }) 6 | print(unpack(objects)) 7 | end 8 | 9 | local M = {} 10 | 11 | function M.__FILE__() return debug.getinfo(2, 'S').source end 12 | function M.__LINE__() return debug.getinfo(2, 'l').currentline end 13 | function M.__FNC__() return debug.getinfo(2, 'n').name end 14 | function M.__FNCREF__() return debug.getinfo(2, 'f').func end 15 | 16 | -- sets an invisible unicode character as icon seaprator 17 | -- the below was reached after many iterations, a short summary of everything 18 | -- that was tried and why it failed: 19 | -- 20 | -- nbsp, U+00a0: the original separator, fails with files that contain nbsp 21 | -- nbsp + zero-width space (U+200b): works only with `sk` (`fzf` shows <200b>) 22 | -- word joiner (U+2060): display works fine, messes up fuzzy search highlights 23 | -- line separator (U+2028), paragraph separator (U+2029): created extra space 24 | -- EN space (U+2002): seems to work well 25 | -- 26 | -- For more unicode SPACE options see: 27 | -- http://unicode-search.net/unicode-namesearch.pl?term=SPACE&.submit=Search 28 | 29 | -- DO NOT USE '\u{}' escape, it will fail with 30 | -- "invalid escape sequence" if Lua < 5.3 31 | -- '\x' escape sequence requires Lua 5.2 32 | -- M.nbsp = "\xc2\xa0" -- "\u{00a0}" 33 | M.nbsp = "\xe2\x80\x82" -- "\u{2002}" 34 | 35 | -- Lua 5.1 compatibility, not sure if required since we're running LuaJIT 36 | -- but it's harmless anyways since if the '\x' escape worked it will do nothing 37 | -- https://stackoverflow.com/questions/29966782/how-to-embed-hex-values-in-a-lua-string-literal-i-e-x-equivalent 38 | if _VERSION and type(_VERSION) == 'string' then 39 | local ver= tonumber(_VERSION:match("%d+.%d+")) 40 | if ver< 5.2 then 41 | M.nbsp = M.nbsp:gsub("\\x(%x%x)", 42 | function (x) return string.char(tonumber(x,16)) 43 | end) 44 | end 45 | end 46 | 47 | M._if = function(bool, a, b) 48 | if bool then 49 | return a 50 | else 51 | return b 52 | end 53 | end 54 | 55 | M.strsplit = function(inputstr, sep) 56 | local t={} 57 | for str in string.gmatch(inputstr, "([^"..sep.."]+)") do 58 | table.insert(t, str) 59 | end 60 | return t 61 | end 62 | 63 | function M.round(num, limit) 64 | if not num then return nil end 65 | if not limit then limit = 0.5 end 66 | local fraction = num - math.floor(num) 67 | if fraction > limit then return math.ceil(num) end 68 | return math.floor(num) 69 | end 70 | 71 | function M.nvim_has_option(option) 72 | return vim.fn.exists('&' .. option) == 1 73 | end 74 | 75 | function M._echo_multiline(msg) 76 | for _, s in ipairs(vim.fn.split(msg, "\n")) do 77 | vim.cmd("echom '" .. s:gsub("'", "''").."'") 78 | end 79 | end 80 | 81 | function M.info(msg) 82 | vim.cmd('echohl Directory') 83 | M._echo_multiline("[Fzf-lua] " .. msg) 84 | vim.cmd('echohl None') 85 | end 86 | 87 | function M.warn(msg) 88 | vim.cmd('echohl WarningMsg') 89 | M._echo_multiline("[Fzf-lua] " .. msg) 90 | vim.cmd('echohl None') 91 | end 92 | 93 | function M.err(msg) 94 | vim.cmd('echohl ErrorMsg') 95 | M._echo_multiline("[Fzf-lua] " .. msg) 96 | vim.cmd('echohl None') 97 | end 98 | 99 | function M.shell_error() 100 | return vim.v.shell_error ~= 0 101 | end 102 | 103 | function M.rg_escape(str) 104 | if not str then return str end 105 | -- [(~'"\/$?'`*&&||;[]<>)] 106 | -- escape "\~$?*|[()^-." 107 | return str:gsub('[\\~$?*|{\\[()^%-%.]', function(x) 108 | return '\\' .. x 109 | end) 110 | end 111 | 112 | function M.sk_escape(str) 113 | if not str then return str end 114 | return str:gsub('["`]', function(x) 115 | return '\\' .. x 116 | end):gsub([[\\]], [[\\\\]]):gsub([[\%$]], [[\\\$]]) 117 | end 118 | 119 | function M.lua_escape(str) 120 | if not str then return str end 121 | return str:gsub('[%%]', function(x) 122 | return '%' .. x 123 | end) 124 | end 125 | 126 | function M.lua_regex_escape(str) 127 | -- escape all lua special chars 128 | -- ( ) % . + - * [ ? ^ $ 129 | if not str then return nil end 130 | return str:gsub('[%(%)%.%+%-%*%[%?%^%$%%]', function(x) 131 | return '%' .. x 132 | end) 133 | end 134 | 135 | function M.pcall_expand(filepath) 136 | -- expand using pcall, this is a workaround to trying to 137 | -- expand certain special chars, more info in issue #285 138 | -- expanding the below fails with: 139 | -- "special[1][98f3a7e3-0d6e-f432-8a18-e1144b53633f][-1].xml" 140 | -- "Vim:E944: Reverse range in character class" 141 | -- this seems to fail with only a single hypen: 142 | -- :lua print(vim.fn.expand("~/file[2-1].ext")) 143 | -- but not when escaping the hypen: 144 | -- :lua print(vim.fn.expand("~/file[2\\-1].ext")) 145 | local ok, expanded = pcall(vim.fn.expand, 146 | filepath:gsub("%-", "\\-")) 147 | if ok and expanded and #expanded>0 then 148 | return expanded 149 | else 150 | return filepath 151 | end 152 | end 153 | 154 | -- TODO: why does `file --dereference --mime` return 155 | -- wrong result for some lua files ('charset=binary')? 156 | M.file_is_binary = function(filepath) 157 | filepath = M.pcall_expand(filepath) 158 | if vim.fn.executable("file") ~= 1 or 159 | not vim.loop.fs_stat(filepath) then 160 | return false 161 | end 162 | local out = M.io_system({"file", "--dereference", "--mime", filepath}) 163 | return out:match("charset=binary") ~= nil 164 | end 165 | 166 | M.perl_file_is_binary = function(filepath) 167 | filepath = M.pcall_expand(filepath) 168 | if vim.fn.executable("perl") ~= 1 or 169 | not vim.loop.fs_stat(filepath) then 170 | return false 171 | end 172 | -- can also use '-T' to test for text files 173 | -- `perldoc -f -x` to learn more about '-B|-T' 174 | M.io_system({"perl", "-E", 'exit((-B $ARGV[0])?0:1);', filepath}) 175 | return not M.shell_error() 176 | end 177 | 178 | M.read_file = function(filepath) 179 | local fd = vim.loop.fs_open(filepath, "r", 438) 180 | if fd == nil then return '' end 181 | local stat = assert(vim.loop.fs_fstat(fd)) 182 | if stat.type ~= 'file' then return '' end 183 | local data = assert(vim.loop.fs_read(fd, stat.size, 0)) 184 | assert(vim.loop.fs_close(fd)) 185 | return data 186 | end 187 | 188 | M.read_file_async = function(filepath, callback) 189 | vim.loop.fs_open(filepath, "r", 438, function(err_open, fd) 190 | if err_open then 191 | -- we must schedule this or we get 192 | -- E5560: nvim_exec must not be called in a lua loop callback 193 | vim.schedule(function() 194 | M.warn(("Unable to open file '%s', error: %s"):format(filepath, err_open)) 195 | end) 196 | return 197 | end 198 | vim.loop.fs_fstat(fd, function(err_fstat, stat) 199 | assert(not err_fstat, err_fstat) 200 | if stat.type ~= 'file' then return callback('') end 201 | vim.loop.fs_read(fd, stat.size, 0, function(err_read, data) 202 | assert(not err_read, err_read) 203 | vim.loop.fs_close(fd, function(err_close) 204 | assert(not err_close, err_close) 205 | return callback(data) 206 | end) 207 | end) 208 | end) 209 | end) 210 | end 211 | 212 | 213 | -- deepcopy can fail with: "Cannot deepcopy object of type userdata" (#353) 214 | -- this can happen when copying items/on_choice params of vim.ui.select 215 | -- run in a pcall and fallback to our poor man's clone 216 | function M.deepcopy(t) 217 | local ok, res = pcall(vim.deepcopy, t) 218 | if ok then 219 | return res 220 | else 221 | return M.tbl_deep_clone(t) 222 | end 223 | end 224 | 225 | function M.tbl_deep_clone(t) 226 | if not t then return end 227 | local clone = {} 228 | 229 | for k, v in pairs(t) do 230 | if type(v) == "table" then 231 | clone[k] = M.tbl_deep_clone(v) 232 | else 233 | clone[k] = v 234 | end 235 | end 236 | 237 | return clone 238 | end 239 | 240 | function M.tbl_length(T) 241 | local count = 0 242 | for _ in pairs(T) do count = count + 1 end 243 | return count 244 | end 245 | 246 | function M.tbl_isempty(T) 247 | if not T or not next(T) then return true end 248 | return false 249 | end 250 | 251 | function M.tbl_concat(...) 252 | local result = {} 253 | local n = 0 254 | 255 | for _, t in ipairs({...}) do 256 | for i, v in ipairs(t) do 257 | result[n + i] = v 258 | end 259 | n = n + #t 260 | end 261 | 262 | return result 263 | end 264 | 265 | function M.tbl_pack(...) 266 | return {n=select('#',...); ...} 267 | end 268 | 269 | function M.tbl_unpack(t, i, j) 270 | return unpack(t, i or 1, j or t.n or #t) 271 | end 272 | 273 | M.ansi_codes = {} 274 | M.ansi_colors = { 275 | -- the "\x1b" esc sequence causes issues 276 | -- with older Lua versions 277 | -- clear = "\x1b[0m", 278 | clear = "", 279 | bold = "", 280 | black = "", 281 | red = "", 282 | green = "", 283 | yellow = "", 284 | blue = "", 285 | magenta = "", 286 | cyan = "", 287 | grey = "", 288 | dark_grey = "", 289 | white = "", 290 | } 291 | 292 | M.add_ansi_code = function(name, escseq) 293 | M.ansi_codes[name] = function(string) 294 | if string == nil or #string == 0 then return '' end 295 | return escseq .. string .. M.ansi_colors.clear 296 | end 297 | end 298 | 299 | for color, escseq in pairs(M.ansi_colors) do 300 | M.add_ansi_code(color, escseq) 301 | end 302 | 303 | 304 | function M.strip_ansi_coloring(str) 305 | if not str then return str end 306 | -- remove escape sequences of the following formats: 307 | -- 1. ^[[34m 308 | -- 2. ^[[0;34m 309 | return str:gsub("%[[%d;]+m", "") 310 | end 311 | 312 | function M.get_visual_selection() 313 | -- this will exit visual mode 314 | -- use 'gv' to reselect the text 315 | local _, csrow, cscol, cerow, cecol 316 | local mode = vim.fn.mode() 317 | if mode == 'v' or mode == 'V' or mode == '' then 318 | -- if we are in visual mode use the live position 319 | _, csrow, cscol, _ = unpack(vim.fn.getpos(".")) 320 | _, cerow, cecol, _ = unpack(vim.fn.getpos("v")) 321 | if mode == 'V' then 322 | -- visual line doesn't provide columns 323 | cscol, cecol = 0, 999 324 | end 325 | -- exit visual mode 326 | vim.api.nvim_feedkeys( 327 | vim.api.nvim_replace_termcodes("", 328 | true, false, true), 'n', true) 329 | else 330 | -- otherwise, use the last known visual position 331 | _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) 332 | _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) 333 | end 334 | -- swap vars if needed 335 | if cerow < csrow then csrow, cerow = cerow, csrow end 336 | if cecol < cscol then cscol, cecol = cecol, cscol end 337 | local lines = vim.fn.getline(csrow, cerow) 338 | -- local n = cerow-csrow+1 339 | local n = M.tbl_length(lines) 340 | if n <= 0 then return '' end 341 | lines[n] = string.sub(lines[n], 1, cecol) 342 | lines[1] = string.sub(lines[1], cscol) 343 | return table.concat(lines, "\n") 344 | end 345 | 346 | function M.send_ctrl_c() 347 | vim.api.nvim_feedkeys( 348 | vim.api.nvim_replace_termcodes("", true, false, true), 'n', true) 349 | end 350 | 351 | function M.feed_keys_termcodes(key) 352 | vim.api.nvim_feedkeys( 353 | vim.api.nvim_replace_termcodes(key, true, false, true), 'n', true) 354 | end 355 | 356 | function M.delayed_cb(cb, fn) 357 | -- HACK: slight delay to prevent missing results 358 | -- otherwise the input stream closes too fast 359 | -- sleep was causing all sorts of issues 360 | -- vim.cmd("sleep! 10m") 361 | if fn == nil then fn = function() end end 362 | vim.defer_fn(function() 363 | cb(nil, fn) 364 | end, 20) 365 | end 366 | 367 | function M.is_term_bufname(bufname) 368 | if bufname and bufname:match("term://") then return true end 369 | return false 370 | end 371 | 372 | function M.is_term_buffer(bufnr) 373 | bufnr = tonumber(bufnr) or 0 374 | local bufname = vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_get_name(bufnr) 375 | return M.is_term_bufname(bufname) 376 | end 377 | 378 | function M.buffer_is_dirty(bufnr, warn) 379 | bufnr = tonumber(bufnr) or vim.api.nvim_get_current_buf() 380 | local info = bufnr and vim.fn.getbufinfo(bufnr)[1] 381 | if info and info.changed ~= 0 then 382 | if warn then 383 | M.warn(('buffer %d has unsaved changes "%s"'):format(bufnr, info.name)) 384 | end 385 | return true 386 | end 387 | return false 388 | end 389 | 390 | 391 | -- returns: 392 | -- 1 for qf list 393 | -- 2 for loc list 394 | function M.win_is_qf(winid, wininfo) 395 | wininfo = wininfo or 396 | (vim.api.nvim_win_is_valid(winid) and vim.fn.getwininfo(winid)[1]) 397 | if wininfo and wininfo.quickfix == 1 then 398 | return wininfo.loclist == 1 and 2 or 1 399 | end 400 | return false 401 | end 402 | 403 | function M.buf_is_qf(bufnr, bufinfo) 404 | bufinfo = bufinfo or 405 | (vim.api.nvim_buf_is_valid(bufnr) and vim.fn.getbufinfo(bufnr)[1]) 406 | if bufinfo and bufinfo.variables and 407 | bufinfo.variables.current_syntax == 'qf' and 408 | not vim.tbl_isempty(bufinfo.windows) then 409 | return M.win_is_qf(bufinfo.windows[1]) 410 | end 411 | return false 412 | end 413 | 414 | function M.winid_from_tab_buf(tabnr, bufnr) 415 | for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tabnr)) do 416 | if bufnr == vim.api.nvim_win_get_buf(w) then 417 | return w 418 | end 419 | end 420 | return nil 421 | end 422 | 423 | function M.nvim_buf_get_name(bufnr, bufinfo) 424 | if not vim.api.nvim_buf_is_valid(bufnr) then return end 425 | if bufinfo and bufinfo.name and #bufinfo.name>0 then 426 | return bufinfo.name 427 | end 428 | local bufname = vim.api.nvim_buf_get_name(bufnr) 429 | if not bufname or #bufname==0 then 430 | local is_qf = M.buf_is_qf(bufnr, bufinfo) 431 | if is_qf then 432 | bufname = is_qf==1 and "[Quickfix List]" or "[Location List]" 433 | else 434 | bufname = "[No Name]" 435 | end 436 | end 437 | assert(#bufname>0) 438 | return bufname 439 | end 440 | 441 | function M.zz() 442 | -- skip for terminal buffers 443 | if M.is_term_buffer() then return end 444 | local lnum1 = vim.api.nvim_win_get_cursor(0)[1] 445 | local lcount = vim.api.nvim_buf_line_count(0) 446 | local zb = 'keepj norm! %dzb' 447 | if lnum1 == lcount then 448 | vim.fn.execute(zb:format(lnum1)) 449 | return 450 | end 451 | vim.cmd('norm! zvzz') 452 | lnum1 = vim.api.nvim_win_get_cursor(0)[1] 453 | vim.cmd('norm! L') 454 | local lnum2 = vim.api.nvim_win_get_cursor(0)[1] 455 | if lnum2 + vim.fn.getwinvar(0, '&scrolloff') >= lcount then 456 | vim.fn.execute(zb:format(lnum2)) 457 | end 458 | if lnum1 ~= lnum2 then 459 | vim.cmd('keepj norm! ``') 460 | end 461 | end 462 | 463 | function M.nvim_win_call(winid, func) 464 | vim.validate({ 465 | winid = { 466 | winid, function(w) 467 | return w and vim.api.nvim_win_is_valid(w) 468 | end, 'a valid window' 469 | }, 470 | func = {func, 'function'} 471 | }) 472 | 473 | local cur_winid = vim.api.nvim_get_current_win() 474 | local noa_set_win = 'noa call nvim_set_current_win(%d)' 475 | if cur_winid ~= winid then 476 | vim.cmd(noa_set_win:format(winid)) 477 | end 478 | local ret = func() 479 | if cur_winid ~= winid then 480 | vim.cmd(noa_set_win:format(cur_winid)) 481 | end 482 | return ret 483 | end 484 | 485 | function M.ft_detect(ext) 486 | local ft = '' 487 | if not ext then return ft end 488 | local tmp_buf = vim.api.nvim_create_buf(false, true) 489 | vim.api.nvim_buf_set_option(tmp_buf, 'bufhidden', 'wipe') 490 | pcall(vim.api.nvim_buf_call, tmp_buf, function() 491 | local filename = (vim.fn.tempname() .. '.' .. ext) 492 | vim.cmd("file " .. filename) 493 | vim.cmd("doautocmd BufEnter") 494 | vim.cmd("filetype detect") 495 | ft = vim.api.nvim_buf_get_option(tmp_buf, 'filetype') 496 | end) 497 | if vim.api.nvim_buf_is_valid(tmp_buf) then 498 | vim.api.nvim_buf_delete(tmp_buf, {force=true}) 499 | end 500 | return ft 501 | end 502 | 503 | -- speed up exteral commands (issue #126) 504 | local _use_lua_io = false 505 | function M.set_lua_io(b) 506 | _use_lua_io = b 507 | if _use_lua_io then 508 | M.warn("using experimental feature 'lua_io'") 509 | end 510 | end 511 | 512 | function M.io_systemlist(cmd, use_lua_io) 513 | if not use_lua_io then use_lua_io = _use_lua_io end 514 | -- only supported with string cmds (no tables) 515 | if use_lua_io and cmd == 'string' then 516 | local rc = 0 517 | local stdout = '' 518 | local handle = io.popen(cmd .. " 2>&1; echo $?", "r") 519 | if handle then 520 | stdout = {} 521 | for h in handle:lines() do 522 | stdout[#stdout + 1] = h 523 | end 524 | -- last line contains the exit status 525 | rc = tonumber(stdout[#stdout]) 526 | stdout[#stdout] = nil 527 | end 528 | handle:close() 529 | return stdout, rc 530 | else 531 | return vim.fn.systemlist(cmd), vim.v.shell_error 532 | end 533 | end 534 | 535 | function M.io_system(cmd, use_lua_io) 536 | if not use_lua_io then use_lua_io = _use_lua_io end 537 | if use_lua_io then 538 | local stdout, rc = M.io_systemlist(cmd, true) 539 | if type(stdout) == 'table' then 540 | stdout = table.concat(stdout, "\n") 541 | end 542 | return stdout, rc 543 | else 544 | return vim.fn.system(cmd), vim.v.shell_error 545 | end 546 | end 547 | 548 | function M.fzf_bind_to_neovim(key) 549 | local conv_map = { 550 | ['alt'] = 'A', 551 | ['ctrl'] = 'C', 552 | ['shift'] = 'S', 553 | } 554 | key = key:lower() 555 | for k, v in pairs(conv_map) do 556 | key = key:gsub(k, v) 557 | end 558 | return ("<%s>"):format(key) 559 | end 560 | 561 | function M.neovim_bind_to_fzf(key) 562 | local conv_map = { 563 | ['a'] = 'alt', 564 | ['c'] = 'ctrl', 565 | ['s'] = 'shift', 566 | } 567 | key = key:lower():gsub("[<>]", "") 568 | for k, v in pairs(conv_map) do 569 | key = key:gsub(k..'%-', v..'-') 570 | end 571 | return key 572 | end 573 | 574 | function M.git_version() 575 | local out = M.io_system({"git", "--version"}) 576 | return tonumber(out:match("(%d+.%d+).")) 577 | end 578 | 579 | function M.find_version() 580 | local out, rc = M.io_systemlist({"find", "--version"}) 581 | return rc==0 and tonumber(out[1]:match("(%d+.%d+)")) or nil 582 | end 583 | 584 | return M 585 | -------------------------------------------------------------------------------- /lua/fzf-lua/core.lua: -------------------------------------------------------------------------------- 1 | local fzf = require "fzf-lua.fzf" 2 | local path = require "fzf-lua.path" 3 | local utils = require "fzf-lua.utils" 4 | local config = require "fzf-lua.config" 5 | local actions = require "fzf-lua.actions" 6 | local win = require "fzf-lua.win" 7 | local libuv = require "fzf-lua.libuv" 8 | local shell = require "fzf-lua.shell" 9 | local make_entry = require "fzf-lua.make_entry" 10 | 11 | local M = {} 12 | 13 | M.fzf_resume = function(opts) 14 | if not config.__resume_data or not config.__resume_data.opts then 15 | utils.info("No resume data available, is 'global_resume' enabled?") 16 | return 17 | end 18 | opts = vim.tbl_deep_extend("force", config.__resume_data.opts, opts or {}) 19 | local last_query = config.__resume_data.last_query 20 | if last_query and #last_query>0 then 21 | last_query = vim.fn.shellescape(last_query) 22 | else 23 | -- in case we continue from another resume 24 | -- reset the previous query which was saved 25 | -- inside "fzf_opts['--query']" argument 26 | last_query = false 27 | end 28 | opts.__resume = true 29 | if opts.__FNCREF__ then 30 | -- HACK for 'live_grep' and 'lsp_live_workspace_symbols' 31 | opts.cmd = nil 32 | opts.continue_last_search = true 33 | opts.__FNCREF__(opts) 34 | else 35 | opts.fzf_opts['--query'] = last_query 36 | M.fzf_wrap(opts, config.__resume_data.contents)() 37 | end 38 | end 39 | 40 | M.fzf_wrap = function(opts, contents, fn_selected) 41 | return coroutine.wrap(function() 42 | opts.fn_selected = opts.fn_selected or fn_selected 43 | local selected = M.fzf(opts, contents) 44 | if opts.fn_selected then 45 | opts.fn_selected(selected) 46 | end 47 | end) 48 | end 49 | 50 | M.fzf = function(opts, contents) 51 | -- normalize with globals if not already normalized 52 | if not opts._normalized then 53 | opts = config.normalize_opts(opts, {}) 54 | end 55 | if opts.fn_pre_win then 56 | opts.fn_pre_win(opts) 57 | end 58 | -- support global resume? 59 | if opts.global_resume then 60 | config.__resume_data = config.__resume_data or {} 61 | config.__resume_data.opts = utils.deepcopy(opts) 62 | config.__resume_data.contents = contents and utils.deepcopy(contents) or nil 63 | if not opts.__resume then 64 | -- since the shell callback isn't called 65 | -- until the user first types something 66 | -- delete the stored query unless called 67 | -- from within 'fzf_resume', this prevents 68 | -- using the stored query between different 69 | -- providers 70 | config.__resume_data.last_query = nil 71 | end 72 | if opts.global_resume_query then 73 | -- We use this option to print the query on line 1 74 | -- later to be removed from the result by M.fzf() 75 | -- this providers a solution for saving the query 76 | -- when the user pressed a valid bind but not when 77 | -- aborting with or , see next comment 78 | opts.fzf_opts['--print-query'] = '' 79 | -- Signals to the win object resume is enabled 80 | -- so we can setup the keypress event monitoring 81 | -- since we already have the query on valid 82 | -- exit codes we only need to monitor , 83 | opts.fn_save_query = function(query) 84 | config.__resume_data.last_query = query and #query>0 and query or nil 85 | end 86 | -- 'au InsertCharPre' would be the best option here 87 | -- but it does not work for terminals: 88 | -- https://github.com/neovim/neovim/issues/5018 89 | -- this is causing lag when typing too fast (#271) 90 | -- also not possible with skim (no 'change' event) 91 | --[[ if not opts._is_skim then 92 | local raw_act = shell.raw_action(function(args) 93 | opts.fn_save_query(args[1]) 94 | end, "{q}") 95 | opts._fzf_cli_args = ('--bind=change:execute-silent:%s'): 96 | format(vim.fn.shellescape(raw_act)) 97 | end ]] 98 | end 99 | end 100 | -- setup the fzf window and preview layout 101 | local fzf_win = win(opts) 102 | if not fzf_win then return end 103 | -- instantiate the previewer 104 | local previewer, preview_opts = nil, nil 105 | if opts.previewer and type(opts.previewer) == 'string' then 106 | preview_opts = config.globals.previewers[opts.previewer] 107 | if not preview_opts then 108 | utils.warn(("invalid previewer '%s'"):format(opts.previewer)) 109 | end 110 | elseif opts.previewer and type(opts.previewer) == 'table' then 111 | preview_opts = opts.previewer 112 | end 113 | if preview_opts and type(preview_opts.new) == 'function' then 114 | previewer = preview_opts:new(preview_opts, opts, fzf_win) 115 | elseif preview_opts and type(preview_opts._new) == 'function' then 116 | previewer = preview_opts._new()(preview_opts, opts, fzf_win) 117 | elseif preview_opts and type(preview_opts._ctor) == 'function' then 118 | previewer = preview_opts._ctor()(preview_opts, opts, fzf_win) 119 | end 120 | if previewer then 121 | opts.fzf_opts['--preview'] = previewer:cmdline() 122 | if type(previewer.preview_window) == 'function' then 123 | -- do we need to override the preview_window args? 124 | -- this can happen with the builtin previewer 125 | -- (1) when using a split we use the previewer as placeholder 126 | -- (2) we use 'nohidden:right:0' to trigger preview function 127 | -- calls without displaying the native fzf previewer split 128 | opts.fzf_opts['--preview-window'] = previewer:preview_window(opts.preview_window) 129 | end 130 | -- provides preview offset when using native previewers 131 | -- (bat/cat/etc) with providers that supply line numbers 132 | -- (grep/quickfix/LSP) 133 | if type(previewer.fzf_delimiter) == 'function' then 134 | opts.fzf_opts["--delimiter"] = previewer:fzf_delimiter() 135 | end 136 | if type(previewer.preview_offset) == 'function' then 137 | opts.preview_offset = previewer:preview_offset() 138 | end 139 | elseif not opts.preview and not opts.fzf_opts['--preview'] then 140 | -- no preview available, override incase $FZF_DEFAULT_OPTS 141 | -- contains a preview which will most likely fail 142 | opts.fzf_opts['--preview-window'] = 'hidden:right:0' 143 | end 144 | 145 | if opts.fn_pre_fzf then 146 | -- some functions such as buffers|tabs 147 | -- need to reacquire current buffer|tab state 148 | opts.fn_pre_fzf(opts) 149 | end 150 | 151 | fzf_win:attach_previewer(previewer) 152 | fzf_win:create() 153 | -- save the normalized winopts, otherwise we 154 | -- lose overrides by 'winopts_fn|winopts_raw' 155 | opts.winopts = fzf_win.winopts 156 | local selected, exit_code = fzf.raw_fzf(contents, M.build_fzf_cli(opts), 157 | { fzf_binary = opts.fzf_bin, fzf_cwd = opts.cwd }) 158 | -- This was added by 'resume': 159 | -- when '--print-query' is specified 160 | -- we are guaranteed to have the query 161 | -- in the first line, save&remove it 162 | if selected and #selected>0 and 163 | opts.fzf_opts['--print-query'] ~= nil then 164 | if opts.fn_save_query then 165 | opts.fn_save_query(selected[1]) 166 | end 167 | table.remove(selected, 1) 168 | end 169 | if opts.fn_post_fzf then 170 | opts.fn_post_fzf(opts, selected) 171 | end 172 | libuv.process_kill(opts._pid) 173 | fzf_win:check_exit_status(exit_code) 174 | -- retrieve the future action and check: 175 | -- * if it's a single function we can close the window 176 | -- * if it's a table of functions we do not close the window 177 | local keybind = actions.normalize_selected(opts.actions, selected) 178 | local action = keybind and opts.actions and opts.actions[keybind] 179 | -- only close the window if autoclose wasn't specified or is 'true' 180 | if (not fzf_win:autoclose() == false) and type(action) ~= 'table' then 181 | fzf_win:close() 182 | end 183 | return selected 184 | end 185 | 186 | 187 | M.preview_window = function(o) 188 | local preview_args = ("%s:%s:%s:"):format( 189 | o.winopts.preview.hidden, o.winopts.preview.border, o.winopts.preview.wrap) 190 | if o.winopts.preview.layout == "horizontal" or 191 | o.winopts.preview.layout == "flex" and 192 | vim.o.columns>o.winopts.preview.flip_columns then 193 | preview_args = preview_args .. o.winopts.preview.horizontal 194 | else 195 | preview_args = preview_args .. o.winopts.preview.vertical 196 | end 197 | return preview_args 198 | end 199 | 200 | M.get_color = function(hl_group, what) 201 | return vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(hl_group)), what) 202 | end 203 | 204 | -- Create fzf --color arguments from a table of vim highlight groups. 205 | M.create_fzf_colors = function(colors) 206 | if not colors then 207 | return "" 208 | end 209 | 210 | local tbl = {} 211 | for highlight, list in pairs(colors) do 212 | local value = M.get_color(list[2], list[1]) 213 | local col = value:match("#[%x]+") or value:match("^[0-9]+") 214 | if col then 215 | table.insert(tbl, ("%s:%s"):format(highlight, col)) 216 | end 217 | end 218 | 219 | return string.format("--color=%s", table.concat(tbl, ",")) 220 | end 221 | 222 | M.create_fzf_binds = function(binds) 223 | if not binds or vim.tbl_isempty(binds) then return end 224 | local tbl = {} 225 | local dedup = {} 226 | for k, v in pairs(binds) do 227 | -- backward compatibility to when binds 228 | -- where defined as one string ':' 229 | if v then 230 | local key, action = v:match("(.*):(.*)") 231 | if action then k, v = key, action end 232 | dedup[k] = v 233 | end 234 | end 235 | for key, action in pairs(dedup) do 236 | table.insert(tbl, string.format("%s:%s", key, action)) 237 | end 238 | return vim.fn.shellescape(table.concat(tbl, ",")) 239 | end 240 | 241 | M.build_fzf_cli = function(opts) 242 | opts.fzf_opts = vim.tbl_extend("force", config.globals.fzf_opts, opts.fzf_opts or {}) 243 | -- copy from globals 244 | for _, o in ipairs({ 245 | 'fzf_info', 246 | 'fzf_ansi', 247 | 'fzf_colors', 248 | 'fzf_layout', 249 | 'fzf_args', 250 | 'fzf_raw_args', 251 | 'fzf_cli_args', 252 | 'keymap', 253 | }) do 254 | opts[o] = opts[o] or config.globals[o] 255 | end 256 | opts.fzf_opts["--bind"] = M.create_fzf_binds(opts.keymap.fzf) 257 | if opts.fzf_colors then 258 | opts.fzf_opts["--color"] = M.create_fzf_colors(opts.fzf_colors) 259 | end 260 | opts.fzf_opts["--expect"] = actions.expect(opts.actions) 261 | opts.fzf_opts["--preview"] = opts.preview or opts.fzf_opts["--preview"] 262 | if opts.fzf_opts["--preview-window"] == nil then 263 | opts.fzf_opts["--preview-window"] = M.preview_window(opts) 264 | end 265 | if opts.preview_offset and #opts.preview_offset>0 then 266 | opts.fzf_opts["--preview-window"] = 267 | opts.fzf_opts["--preview-window"] .. ":" .. opts.preview_offset 268 | end 269 | -- shell escape the prompt 270 | opts.fzf_opts["--prompt"] = 271 | vim.fn.shellescape(opts.prompt or opts.fzf_opts["--prompt"]) 272 | -- multi | no-multi (select) 273 | if opts.nomulti or opts.fzf_opts["--no-multi"] then 274 | opts.fzf_opts["--multi"] = nil 275 | opts.fzf_opts["--no-multi"] = '' 276 | else 277 | opts.fzf_opts["--multi"] = '' 278 | opts.fzf_opts["--no-multi"] = nil 279 | end 280 | -- backward compatibility, add all previously known options 281 | for k, v in pairs({ 282 | ['--ansi'] = 'fzf_ansi', 283 | ['--layout'] = 'fzf_layout' 284 | }) do 285 | if opts[v] and #opts[v]==0 then 286 | opts.fzf_opts[k] = nil 287 | elseif opts[v] then 288 | opts.fzf_opts[k] = opts[v] 289 | end 290 | end 291 | local extra_args = '' 292 | for _, o in ipairs({ 293 | 'fzf_args', 294 | 'fzf_raw_args', 295 | 'fzf_cli_args', 296 | '_fzf_cli_args', 297 | }) do 298 | if opts[o] then extra_args = extra_args .. " " .. opts[o] end 299 | end 300 | if opts._is_skim then 301 | local info = opts.fzf_opts["--info"] 302 | -- skim (rust version of fzf) doesn't 303 | -- support the '--info=' flag 304 | opts.fzf_opts["--info"] = nil 305 | if info == 'inline' then 306 | -- inline for skim is defined as: 307 | opts.fzf_opts["--inline-info"] = '' 308 | end 309 | end 310 | -- build the clip args 311 | local cli_args = '' 312 | for k, v in pairs(opts.fzf_opts) do 313 | if v then 314 | v = v:gsub(k .. '=', '') 315 | cli_args = cli_args .. 316 | (" %s%s"):format(k,#v>0 and "="..v or '') 317 | end 318 | end 319 | return cli_args .. extra_args 320 | end 321 | 322 | M.mt_cmd_wrapper = function(opts) 323 | assert(opts and opts.cmd) 324 | 325 | local str_to_str = function(s) 326 | return "[[" .. s:gsub('[%]]', function(x) return "\\"..x end) .. "]]" 327 | end 328 | 329 | local opts_to_str = function(o) 330 | local names = { 331 | "debug", 332 | "argv_expr", 333 | "cmd", 334 | "cwd", 335 | "git_icons", 336 | "file_icons", 337 | "color_icons", 338 | "strip_cwd_prefix", 339 | "rg_glob", 340 | } 341 | -- caller reqested rg with glob support 342 | if o.rg_glob then 343 | table.insert(names, "glob_flag") 344 | table.insert(names, "glob_separator") 345 | end 346 | local str = "" 347 | for _, name in ipairs(names) do 348 | if o[name] ~= nil then 349 | if #str>0 then str = str..',' end 350 | local val = o[name] 351 | if type(val) == 'string' then 352 | val = str_to_str(val) 353 | end 354 | if type(val) == 'table' then 355 | val = vim.inspect(val) 356 | end 357 | str = str .. ("%s=%s"):format(name, val) 358 | end 359 | end 360 | return '{'..str..'}' 361 | end 362 | 363 | if not opts.requires_processing and 364 | not opts.git_icons and not opts.file_icons then 365 | -- command does not require any processing 366 | return opts.cmd 367 | elseif opts.multiprocess then 368 | local fn_preprocess = opts._fn_preprocess_str or [[return require("make_entry").preprocess]] 369 | local fn_transform = opts._fn_transform_str or [[return require("make_entry").file]] 370 | -- replace all below 'fn.shellescape' with our version 371 | -- replacing the surrounding single quotes with double 372 | -- as this was causing resume to fail with fish shell 373 | -- due to fzf replacing ' with \ (no idea why) 374 | if not opts.no_remote_config then 375 | fn_transform = ([[_G._fzf_lua_server=%s; %s]]):format( 376 | libuv.shellescape(vim.g.fzf_lua_server), 377 | fn_transform) 378 | end 379 | if config._devicons_setup then 380 | fn_transform = ([[_G._devicons_setup=%s; %s]]) :format( 381 | libuv.shellescape(config._devicons_setup), 382 | fn_transform) 383 | end 384 | if config._devicons_path then 385 | fn_transform = ([[_G._devicons_path=%s; %s]]) :format( 386 | libuv.shellescape(config._devicons_path), 387 | fn_transform) 388 | end 389 | local cmd = libuv.wrap_spawn_stdio(opts_to_str(opts), 390 | fn_transform, fn_preprocess) 391 | if opts.debug_cmd or opts.debug and not (opts.debug_cmd==false) then 392 | print(cmd) 393 | end 394 | return cmd 395 | else 396 | return libuv.spawn_nvim_fzf_cmd(opts, 397 | function(x) 398 | return opts._fn_transform 399 | and opts._fn_transform(opts, x) 400 | or make_entry.file(opts, x) 401 | end, 402 | function(o) 403 | -- setup opts.cwd and git diff files 404 | return opts._fn_preprocess 405 | and opts._fn_preprocess(o) 406 | or make_entry.preprocess(o) 407 | end) 408 | end 409 | end 410 | 411 | -- shortcuts to make_entry 412 | M.get_devicon = make_entry.get_devicon 413 | M.make_entry_file = make_entry.file 414 | M.make_entry_preprocess = make_entry.preprocess 415 | 416 | M.make_entry_lcol = function(opts, entry) 417 | if not entry then return nil end 418 | local filename = entry.filename or vim.api.nvim_buf_get_name(entry.bufnr) 419 | return string.format("%s:%s:%s:%s%s", 420 | -- uncomment to test URIs 421 | -- "file://" .. filename, 422 | filename, --utils.ansi_codes.magenta(filename), 423 | utils.ansi_codes.green(tostring(entry.lnum)), 424 | utils.ansi_codes.blue(tostring(entry.col)), 425 | entry.text and #entry.text>0 and " " or "", 426 | not entry.text and "" or 427 | (opts.trim_entry and vim.trim(entry.text)) or entry.text) 428 | end 429 | 430 | -- given the default delimiter ':' this is the 431 | -- fzf experssion field index for the line number 432 | -- when entry format is 'file:line:col: text' 433 | -- this is later used with native fzf previewers 434 | -- for setting the preview offset (and on some 435 | -- cases the highlighted line) 436 | M.set_fzf_field_index = function(opts, default_idx, default_expr) 437 | opts.line_field_index = opts.line_field_index or default_idx or 2 438 | -- when entry contains lines we set the fzf FIELD INDEX EXPRESSION 439 | -- to the below so that only the filename is sent to the preview 440 | -- action, otherwise we will have issues with entries with text 441 | -- containing '--' as fzf won't know how to interpret the cmd 442 | -- this works when the delimiter is only ':', when using multiple 443 | -- or different delimiters (e.g. in 'lines') we need to use a different 444 | -- field index experssion such as "{..-2}" (all fields but the last 2) 445 | opts.field_index_expr = opts.field_index_expr or default_expr or "{1}" 446 | return opts 447 | end 448 | 449 | M.set_header = function(opts, type) 450 | if not opts then opts = {} end 451 | if opts.no_header then return opts end 452 | if not opts.cwd_header then opts.cwd_header = "cwd:" end 453 | if not opts.search_header then opts.search_header = "Searching for:" end 454 | if not opts.cwd and opts.show_cwd_header then opts.cwd = vim.loop.cwd() end 455 | local header_str 456 | local cwd_str = 457 | opts.cwd and (opts.show_cwd_header ~= false) and 458 | (opts.show_cwd_header or opts.cwd ~= vim.loop.cwd()) and 459 | ("%s %s"):format(opts.cwd_header, opts.cwd:gsub("^"..vim.env.HOME, "~")) 460 | local search_str = opts.search and #opts.search > 0 and 461 | ("%s %s"):format(opts.search_header, opts.search) 462 | -- 1: only search 463 | -- 2: only cwd 464 | -- otherwise, all 465 | if type == 1 then header_str = search_str or '' 466 | elseif type == 2 then header_str = cwd_str or '' 467 | else 468 | header_str = search_str or '' 469 | if #header_str>0 and cwd_str and #cwd_str>0 then 470 | header_str = header_str .. ", " 471 | end 472 | header_str = header_str .. (cwd_str or '') 473 | end 474 | if not header_str or #header_str==0 then return opts end 475 | opts.fzf_opts['--header'] = libuv.shellescape(header_str) 476 | return opts 477 | end 478 | 479 | 480 | M.fzf_files = function(opts, contents) 481 | 482 | if not opts then return end 483 | 484 | 485 | M.fzf_wrap(opts, contents or opts.fzf_fn, function(selected) 486 | 487 | if opts.post_select_cb then 488 | opts.post_select_cb() 489 | end 490 | 491 | if not selected then return end 492 | 493 | if #selected > 1 then 494 | local idx = utils.tbl_length(opts.actions)>1 and 2 or 1 495 | for i = idx, #selected do 496 | selected[i] = path.entry_to_file(selected[i], opts.cwd).stripped 497 | end 498 | end 499 | 500 | actions.act(opts.actions, selected, opts) 501 | 502 | end)() 503 | 504 | end 505 | 506 | M.set_fzf_interactive_cmd = function(opts) 507 | 508 | if not opts then return end 509 | 510 | -- fzf already adds single quotes around the placeholder when expanding 511 | -- for skim we surround it with double quotes or single quote searches fail 512 | local placeholder = utils._if(opts._is_skim, '"{}"', '{q}') 513 | local raw_async_act = shell.reload_action_cmd(opts, placeholder) 514 | return M.set_fzf_interactive(opts, raw_async_act, placeholder) 515 | end 516 | 517 | M.set_fzf_interactive_cb = function(opts) 518 | 519 | if not opts then return end 520 | 521 | -- fzf already adds single quotes around the placeholder when expanding 522 | -- for skim we surround it with double quotes or single quote searches fail 523 | local placeholder = utils._if(opts._is_skim, '"{}"', '{q}') 524 | 525 | local uv = vim.loop 526 | local raw_async_act = shell.raw_async_action(function(pipe, args) 527 | 528 | coroutine.wrap(function() 529 | 530 | local co = coroutine.running() 531 | local results = opts._reload_action(args[1]) 532 | 533 | local close_pipe = function() 534 | if pipe and not uv.is_closing(pipe) then 535 | uv.close(pipe) 536 | pipe = nil 537 | end 538 | coroutine.resume(co) 539 | end 540 | 541 | if type(results) == 'table' and not vim.tbl_isempty(results) then 542 | uv.write(pipe, 543 | vim.tbl_map(function(x) return x.."\n" end, results), 544 | function(_) 545 | close_pipe() 546 | end) 547 | -- wait for write to finish 548 | coroutine.yield() 549 | end 550 | -- does nothing if write finished successfully 551 | close_pipe() 552 | 553 | end)() 554 | end, placeholder) 555 | 556 | return M.set_fzf_interactive(opts, raw_async_act, placeholder) 557 | end 558 | 559 | M.set_fzf_interactive = function(opts, act_cmd, placeholder) 560 | 561 | if not opts or not act_cmd or not placeholder then return end 562 | 563 | -- cannot be nil 564 | local query = opts.query or '' 565 | 566 | if opts._is_skim then 567 | -- do not run an empty string query unless the user requested 568 | if not opts.exec_empty_query then 569 | act_cmd = "sh -c " .. vim.fn.shellescape( 570 | ("[ -z %s ] || %s"):format(placeholder, act_cmd)) 571 | else 572 | act_cmd = vim.fn.shellescape(act_cmd) 573 | end 574 | -- skim interactive mode does not need a piped command 575 | opts.fzf_fn = nil 576 | opts.fzf_opts['--prompt'] = '*' .. opts.prompt 577 | opts.fzf_opts['--cmd-prompt'] = vim.fn.shellescape(opts.prompt) 578 | opts.prompt = nil 579 | -- since we surrounded the skim placeholder with quotes 580 | -- we need to escape them in the initial query 581 | opts.fzf_opts['--cmd-query'] = vim.fn.shellescape(utils.sk_escape(query)) 582 | opts._fzf_cli_args = string.format( "-i -c %s", act_cmd) 583 | else 584 | -- fzf already adds single quotes 585 | -- around the place holder 586 | opts.fzf_fn = {} 587 | if opts.exec_empty_query or (query and #query>0) then 588 | opts.fzf_fn = act_cmd:gsub(placeholder, 589 | #query>0 and utils.lua_escape(vim.fn.shellescape(query)) or "''") 590 | end 591 | opts.fzf_opts['--phony'] = '' 592 | opts.fzf_opts['--query'] = vim.fn.shellescape(query) 593 | opts._fzf_cli_args = string.format('--bind=%s', 594 | vim.fn.shellescape(string.format("change:reload:%s || true", act_cmd))) 595 | end 596 | 597 | return opts 598 | 599 | end 600 | 601 | 602 | return M 603 | --------------------------------------------------------------------------------