-- slimmed down version of nvim-fzf's 'raw_fzf', changes include: -- DOES NOT SUPPORT WINDOWS -- does not close the pipe before all writes are complete -- option to not add '\n' on content function callbacks -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf.lua local uv = vim.loop local M = {} -- workaround to a potential 'tempname' bug? (#222) -- neovim doesn't guarantee the existence of the -- parent temp dir potentially failing `mkfifo` -- https://github.com/neovim/neovim/issues/1432 -- https://github.com/neovim/neovim/pull/11284 local function tempname() local tmpname = vim.fn.tempname() local parent = vim.fn.fnamemodify(tmpname, ':h') -- parent must exist for `mkfifo` to succeed -- if the neovim temp dir was deleted or the -- tempname already exists we use 'os.tmpname' if not uv.fs_stat(parent) or uv.fs_stat(tmpname) then tmpname = os.tmpname() -- 'os.tmpname' touches the file which -- will also fail `mkfifo`, delete it vim.fn.delete(tmpname) end return tmpname end -- contents can be either a table with tostring()able items, or a function that -- can be called repeatedly for values. the latter can use coroutines for async -- behavior. function M.raw_fzf(contents, fzf_cli_args, opts) if not coroutine.running() then error("[Fzf-lua] function must be called inside a coroutine.") end if not opts then opts = {} end local cwd = opts.fzf_cwd or opts.cwd local cmd = opts.fzf_bin or 'fzf' local fifotmpname = tempname() local outputtmpname = tempname() -- we use a temporary env $FZF_DEFAULT_COMMAND instead of piping -- the command to fzf, this way fzf kills the command when it exits -- this is especially important with our shell helper as io.write fails -- to delect when the pipe is broken (EPIPE) so the neovim headless -- instance never terminates which hangs fzf on exit local FZF_DEFAULT_COMMAND = nil if fzf_cli_args then cmd = cmd .. " " .. fzf_cli_args end if opts.fzf_cli_args then cmd = cmd .. " " .. opts.fzf_cli_args end if contents then if type(contents) == "string" and #contents>0 then if opts.silent_fail ~= false then contents = ("%s || true"):format(contents) end FZF_DEFAULT_COMMAND = contents else cmd = ("%s < %s"):format(cmd, vim.fn.shellescape(fifotmpname)) end end cmd = ("%s > %s"):format(cmd, vim.fn.shellescape(outputtmpname)) local fd, output_pipe = nil, nil local finish_called = false local write_cb_count = 0 -- Create the output pipe -- We use tbl for perf reasons, from ':help system': -- If {cmd} is a List it runs directly (no 'shell') -- If {cmd} is a String it runs in the 'shell' vim.fn.system({"mkfifo", fifotmpname}) local function finish(_) -- mark finish if once called finish_called = true -- close pipe if there are no outstanding writes if output_pipe and write_cb_count == 0 then output_pipe:close() output_pipe = nil end end local function write_cb(data, cb) if not output_pipe then return end write_cb_count = write_cb_count + 1 output_pipe:write(data, function(err) -- decrement write call count write_cb_count = write_cb_count - 1 -- this will call the user's cb if cb then cb(err) end if err then -- can fail with premature process kill finish(2) elseif finish_called and write_cb_count == 0 then -- 'termopen.on_exit' already called and did not close the -- pipe due to write_cb_count>0, since this is the last call -- we can close the fzf pipe finish(3) end end) end -- nvim-fzf compatibility, builds the user callback functions -- 1st argument: callback function that adds newline to each write -- 2nd argument: callback function thhat writes the data as is -- 3rd argument: direct access to the pipe object local function usr_write_cb(nl) local function end_of_data(usrdata, cb) if usrdata == nil then if cb then cb(nil) end finish(5) return true end return false end if nl then return function(usrdata, cb) if not end_of_data(usrdata, cb) then write_cb(tostring(usrdata).."\n", cb) end end else return function(usrdata, cb) if not end_of_data(usrdata, cb) then write_cb(usrdata, cb) end end end end local co = coroutine.running() vim.fn.termopen({"sh", "-c", cmd}, { cwd = cwd, env = { ['SHELL'] = 'sh', ['FZF_DEFAULT_COMMAND'] = FZF_DEFAULT_COMMAND, ['SKIM_DEFAULT_COMMAND'] = FZF_DEFAULT_COMMAND, }, on_exit = function(_, rc, _) local output = {} local f = io.open(outputtmpname) if f then for v in f:lines() do table.insert(output, v) end f:close() end finish(1) vim.fn.delete(fifotmpname) vim.fn.delete(outputtmpname) if #output == 0 then output = nil end coroutine.resume(co, output, rc) end }) vim.cmd[[set ft=fzf]] -- terminal behavior seem to have changed after the introduction -- of 'nt' mode (terminal-normal mode) which is included in 0.6 -- https://github.com/neovim/neovim/pull/15878 -- preferably I'd like to check if the vim patch is included using -- vim.fn.has('patch-8.2.3461') -- but this doesn't work for vim patches > 8.1 as explained in: -- https://github.com/neovim/neovim/issues/9635 -- however, since this patch was included in 0.6 we can test -- for neovim version 0.6 -- beats me why 'nvim_get_mode().mode' still returns 'nt' even -- after we're clearly in insert mode or why `:startinsert` -- won't change the mode from 'nt' to 't' so we use feedkeys() -- instead. -- this "retires" 'actions.ensure_insert_mode' and solves the -- issue of calling an fzf-lua mapping from insert mode (#429) if vim.fn.has('nvim-0.6') == 1 then vim.cmd([[noautocmd lua vim.api.nvim_feedkeys(]] .. [[vim.api.nvim_replace_termcodes("i", true, false, true)]] .. [[, 'n', true)]]) else vim.cmd[[startinsert]] end if not contents or type(contents) == "string" then goto wait_for_fzf end -- have to open this after there is a reader (termopen) -- otherwise this will block fd = uv.fs_open(fifotmpname, "w", -1) output_pipe = uv.new_pipe(false) output_pipe:open(fd) -- print(output_pipe:getpeername()) -- this part runs in the background, when the user has selected, it will -- error out, but that doesn't matter so we just break out of the loop. if contents then if type(contents) == "table" then if not vim.tbl_isempty(contents) then write_cb(vim.tbl_map(function(x) return x.."\n" end, contents)) end finish(4) else contents(usr_write_cb(true), usr_write_cb(false), output_pipe) end end ::wait_for_fzf:: return coroutine.yield() end return M