├── .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 = "[0m",
279 | bold = "[1m",
280 | black = "[0;30m",
281 | red = "[0;31m",
282 | green = "[0;32m",
283 | yellow = "[0;33m",
284 | blue = "[0;34m",
285 | magenta = "[0;35m",
286 | cyan = "[0;36m",
287 | grey = "[0;90m",
288 | dark_grey = "[0;97m",
289 | white = "[0;98m",
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 |
--------------------------------------------------------------------------------